Compare commits
3 commits
dev
...
Release-v2
Author | SHA1 | Date | |
---|---|---|---|
|
e848940733 | ||
|
f3a00ab186 | ||
|
00e3545ad2 |
292 changed files with 6968 additions and 10215 deletions
22
.eslintrc.js
22
.eslintrc.js
|
@ -9,21 +9,24 @@ module.exports = {
|
||||||
"plugin:react-hooks/recommended",
|
"plugin:react-hooks/recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"airbnb",
|
'airbnb',
|
||||||
"prettier",
|
'prettier',
|
||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
},
|
},
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: 'latest',
|
||||||
sourceType: "module",
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: ["react", "@typescript-eslint"],
|
plugins: [
|
||||||
|
'react',
|
||||||
|
'@typescript-eslint'
|
||||||
|
],
|
||||||
rules: {
|
rules: {
|
||||||
"linebreak-style": 0,
|
'linebreak-style': 0,
|
||||||
"no-underscore-dangle": 0,
|
'no-underscore-dangle': 0,
|
||||||
|
|
||||||
"import/prefer-default-export": "off",
|
"import/prefer-default-export": "off",
|
||||||
"import/extensions": "off",
|
"import/extensions": "off",
|
||||||
|
@ -35,7 +38,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
"react/no-unstable-nested-components": ["error", { allowAsProps: true }],
|
'react/no-unstable-nested-components': [
|
||||||
|
'error',
|
||||||
|
{ allowAsProps: true },
|
||||||
|
],
|
||||||
"react/jsx-filename-extension": [
|
"react/jsx-filename-extension": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,9 +1,9 @@
|
||||||
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||||
|
|
||||||
|
|
||||||
Fixes #
|
Fixes #
|
||||||
|
|
||||||
#### Type of change
|
#### Type of change
|
||||||
|
|
9
.github/renovate.json
vendored
9
.github/renovate.json
vendored
|
@ -1,10 +1,13 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:base", ":dependencyDashboardApproval"],
|
"extends": [
|
||||||
"labels": ["Dependencies"],
|
"config:base",
|
||||||
|
":dependencyDashboardApproval"
|
||||||
|
],
|
||||||
|
"labels": [ "Dependencies" ],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["lockFileMaintenance"]
|
"matchUpdateTypes": [ "lockFileMaintenance" ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lockFileMaintenance": { "enabled": true },
|
"lockFileMaintenance": { "enabled": true },
|
||||||
|
|
2
.github/workflows/build-pull-request.yml
vendored
2
.github/workflows/build-pull-request.yml
vendored
|
@ -2,7 +2,7 @@ name: Build pull request
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: ["opened", "synchronize"]
|
types: ['opened', 'synchronize']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-pull-request:
|
build-pull-request:
|
||||||
|
|
10
.github/workflows/cla.yml
vendored
10
.github/workflows/cla.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: "CLA Assistant"
|
name: 'CLA Assistant'
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
@ -9,7 +9,7 @@ jobs:
|
||||||
CLAssistant:
|
CLAssistant:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: "CLA Assistant"
|
- name: 'CLA Assistant'
|
||||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||||
# Beta Release
|
# Beta Release
|
||||||
uses: cla-assistant/github-action@v2.2.1
|
uses: cla-assistant/github-action@v2.2.1
|
||||||
|
@ -18,10 +18,10 @@ jobs:
|
||||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
|
||||||
with:
|
with:
|
||||||
path-to-signatures: "signatures.json"
|
path-to-signatures: 'signatures.json'
|
||||||
path-to-document: "https://github.com/cinnyapp/cla/blob/main/cla.md" # e.g. a CLA or a DCO document
|
path-to-document: 'https://github.com/cinnyapp/cla/blob/main/cla.md' # e.g. a CLA or a DCO document
|
||||||
# branch should not be protected
|
# branch should not be protected
|
||||||
branch: "main"
|
branch: 'main'
|
||||||
allowlist: ajbura,bot*
|
allowlist: ajbura,bot*
|
||||||
|
|
||||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||||
|
|
6
.github/workflows/docker-pr.yml
vendored
6
.github/workflows/docker-pr.yml
vendored
|
@ -1,10 +1,10 @@
|
||||||
name: "Docker check"
|
name: 'Docker check'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "Dockerfile"
|
- 'Dockerfile'
|
||||||
- ".github/workflows/docker-pr.yml"
|
- '.github/workflows/docker-pr.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build:
|
||||||
|
|
2
.github/workflows/lockfile.yml
vendored
2
.github/workflows/lockfile.yml
vendored
|
@ -3,7 +3,7 @@ name: NPM Lockfile Changes
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "package-lock.json"
|
- 'package-lock.json'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lockfile_changes:
|
lockfile_changes:
|
||||||
|
|
2
.github/workflows/netlify-dev.yml
vendored
2
.github/workflows/netlify-dev.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
production-deploy: true
|
production-deploy: true
|
||||||
github-deployment-environment: nightly
|
github-deployment-environment: nightly
|
||||||
github-deployment-description: "Nightly deployment on each commit to dev branch"
|
github-deployment-description: 'Nightly deployment on each commit to dev branch'
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_DEV }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_DEV }}
|
||||||
|
|
2
.github/workflows/prod-deploy.yml
vendored
2
.github/workflows/prod-deploy.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
production-deploy: true
|
production-deploy: true
|
||||||
github-deployment-environment: stable
|
github-deployment-environment: stable
|
||||||
github-deployment-description: "Stable deployment on each release"
|
github-deployment-description: 'Stable deployment on each release'
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"printWidth": 100,
|
||||||
"semi": true,
|
"singleQuote": true
|
||||||
"useTabs": false,
|
|
||||||
"singleQuote": false,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"printWidth": 80
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ First off, thanks for taking the time to contribute! ❤️
|
||||||
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
||||||
|
|
||||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||||
>
|
|
||||||
> - Star the project
|
> - Star the project
|
||||||
> - Tweet about it (tag @cinnyapp)
|
> - Tweet about it (tag @cinnyapp)
|
||||||
> - Refer this project in your project's readme
|
> - Refer this project in your project's readme
|
||||||
|
@ -19,7 +18,6 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
> ### Legal Notice
|
> ### Legal Notice
|
||||||
>
|
|
||||||
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
|
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
|
||||||
|
|
||||||
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
|
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
|
||||||
|
@ -28,9 +26,9 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
| Not ideal | Better |
|
|Not ideal|Better|
|
||||||
| ----------------------------------- | --------------------------------------------- |
|
|---|----|
|
||||||
| Fixed markAllAsRead in RoomTimeline | Fix read marker when paginating room timeline |
|
|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
|
||||||
|
|
||||||
It is not always possible to phrase every change in such a manner, but it is desired.
|
It is not always possible to phrase every change in such a manner, but it is desired.
|
||||||
|
|
||||||
|
@ -41,7 +39,6 @@ Also, we use [ESLint](https://eslint.org/) for clean and stylistically consisten
|
||||||
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
||||||
|
|
||||||
## Helpful links
|
## Helpful links
|
||||||
|
|
||||||
- [BEM methodology](http://getbem.com/introduction/)
|
- [BEM methodology](http://getbem.com/introduction/)
|
||||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"schemaVersion": 2,
|
|
||||||
"dockerfilePath": "./Dockerfile"
|
|
||||||
}
|
|
12
index.html
12
index.html
|
@ -18,10 +18,7 @@
|
||||||
|
|
||||||
<meta property="og:title" content="Cinny" />
|
<meta property="og:title" content="Cinny" />
|
||||||
<meta property="og:url" content="https://cinny.in" />
|
<meta property="og:url" content="https://cinny.in" />
|
||||||
<meta
|
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
||||||
property="og:image"
|
|
||||||
content="https://cinny.in/assets/favicon-48x48.png"
|
|
||||||
/>
|
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
||||||
|
@ -35,10 +32,7 @@
|
||||||
<meta name="application-name" content="Cinny" />
|
<meta name="application-name" content="Cinny" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
|
||||||
content="black-translucent"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
|
@ -102,6 +96,6 @@
|
||||||
<audio id="inviteSound">
|
<audio id="inviteSound">
|
||||||
<source src="./public/sound/invite.ogg" type="audio/ogg" />
|
<source src="./public/sound/invite.ogg" type="audio/ogg" />
|
||||||
</audio>
|
</audio>
|
||||||
<script type="module" src="./src/index.tsx"></script>
|
<script type="module" src="./src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
22
package-lock.json
generated
22
package-lock.json
generated
|
@ -14,7 +14,6 @@
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@types/flux": "3.1.11",
|
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
"dateformat": "5.0.3",
|
"dateformat": "5.0.3",
|
||||||
|
@ -44,7 +43,6 @@
|
||||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||||
"@rollup/plugin-inject": "5.0.3",
|
"@rollup/plugin-inject": "5.0.3",
|
||||||
"@rollup/plugin-wasm": "6.1.1",
|
"@rollup/plugin-wasm": "6.1.1",
|
||||||
"@types/dateformat": "5.0.0",
|
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/react-dom": "18.0.9",
|
||||||
|
@ -1117,12 +1115,6 @@
|
||||||
"react-dom": ">=16.8"
|
"react-dom": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/dateformat": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/dateformat/-/dateformat-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-SZg4JdHIWHQGEokbYGZSDvo5wA4TLYPXaqhigs/wH+REDOejcJzgH+qyY+HtEUtWOZxEUkbhbdYPqQDiEgrXeA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
||||||
|
@ -1134,20 +1126,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
||||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/fbemitter": {
|
|
||||||
"version": "2.0.32",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/fbemitter/-/fbemitter-2.0.32.tgz",
|
|
||||||
"integrity": "sha512-Hwq28bBlbmfCgLnNJvjl5ssTrbZCTSblI4vqPpqZrbbEL8vn5l2UivxhlMYfUY7a4SR8UB6RKoLjOZfljqAa6g=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/flux": {
|
|
||||||
"version": "3.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/flux/-/flux-3.1.11.tgz",
|
|
||||||
"integrity": "sha512-Aq4UB1ZqAKcPbhB0GpgMw2sntvOh71he9tjz53TLKrI7rw3Y3LxCW5pTYY9IV455hQapm4pmxFjpqlWOs308Yg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/fbemitter": "*",
|
|
||||||
"@types/react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.11",
|
"version": "7.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "npm run check:eslint && npm run check:prettier",
|
"lint": "yarn check:eslint && yarn check:prettier",
|
||||||
"check:eslint": "eslint src/*",
|
"check:eslint": "eslint src/*",
|
||||||
"check:prettier": "prettier --check .",
|
"check:prettier": "prettier --check .",
|
||||||
"fix:prettier": "prettier --write .",
|
"fix:prettier": "prettier --write .",
|
||||||
|
@ -24,7 +24,6 @@
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@types/flux": "3.1.11",
|
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
"dateformat": "5.0.3",
|
"dateformat": "5.0.3",
|
||||||
|
@ -54,7 +53,6 @@
|
||||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||||
"@rollup/plugin-inject": "5.0.3",
|
"@rollup/plugin-inject": "5.0.3",
|
||||||
"@rollup/plugin-wasm": "6.1.1",
|
"@rollup/plugin-wasm": "6.1.1",
|
||||||
"@types/dateformat": "5.0.0",
|
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/react-dom": "18.0.9",
|
||||||
|
|
|
@ -1,69 +1,64 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Avatar.scss";
|
import './Avatar.scss';
|
||||||
|
|
||||||
import { twemojify } from "../../../util/twemojify";
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
import RawIcon from "../system-icons/RawIcon";
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
|
|
||||||
import ImageBrokenSVG from "../../../../public/res/svg/image-broken.svg";
|
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
|
||||||
import { avatarInitials } from "../../../util/common";
|
import { avatarInitials } from '../../../util/common';
|
||||||
|
|
||||||
const Avatar = React.forwardRef(
|
const Avatar = React.forwardRef(({
|
||||||
({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
|
text, bgColor, iconSrc, iconColor, imageSrc, size,
|
||||||
let textSize = "s1";
|
}, ref) => {
|
||||||
if (size === "large") textSize = "h1";
|
let textSize = 's1';
|
||||||
if (size === "small") textSize = "b1";
|
if (size === 'large') textSize = 'h1';
|
||||||
if (size === "extra-small") textSize = "b3";
|
if (size === 'small') textSize = 'b1';
|
||||||
|
if (size === 'extra-small') textSize = 'b3';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
|
||||||
ref={ref}
|
{
|
||||||
className={`avatar-container avatar-container__${size} noselect`}
|
imageSrc !== null
|
||||||
>
|
? (
|
||||||
{imageSrc !== null ? (
|
|
||||||
<img
|
<img
|
||||||
draggable="false"
|
draggable="false"
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
onLoad={(e) => {
|
onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
|
||||||
e.target.style.backgroundColor = "transparent";
|
onError={(e) => { e.target.src = ImageBrokenSVG; }}
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
e.target.src = ImageBrokenSVG;
|
|
||||||
}}
|
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
||||||
backgroundColor: iconSrc === null ? bgColor : "transparent",
|
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
|
||||||
}}
|
|
||||||
className={`avatar__border${iconSrc !== null ? "--active" : ""}`}
|
|
||||||
>
|
>
|
||||||
{iconSrc !== null ? (
|
{
|
||||||
<RawIcon size={size} src={iconSrc} color={iconColor} />
|
iconSrc !== null
|
||||||
) : (
|
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||||
text !== null && (
|
: text !== null && (
|
||||||
<Text variant={textSize} primary>
|
<Text variant={textSize} primary>
|
||||||
{twemojify(avatarInitials(text))}
|
{twemojify(avatarInitials(text))}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
)}
|
}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
Avatar.defaultProps = {
|
Avatar.defaultProps = {
|
||||||
text: null,
|
text: null,
|
||||||
bgColor: "transparent",
|
bgColor: 'transparent',
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
iconColor: null,
|
iconColor: null,
|
||||||
imageSrc: null,
|
imageSrc: null,
|
||||||
size: "normal",
|
size: 'normal',
|
||||||
};
|
};
|
||||||
|
|
||||||
Avatar.propTypes = {
|
Avatar.propTypes = {
|
||||||
|
@ -72,7 +67,7 @@ Avatar.propTypes = {
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
iconColor: PropTypes.string,
|
iconColor: PropTypes.string,
|
||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
size: PropTypes.oneOf(["large", "normal", "small", "extra-small"]),
|
size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Avatar;
|
export default Avatar;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
import { avatarInitials, cssVar } from "../../../util/common";
|
import { avatarInitials, cssVar } from '../../../util/common';
|
||||||
|
|
||||||
// renders the avatar and returns it as an URL
|
// renders the avatar and returns it as an URL
|
||||||
export default async function renderAvatar({
|
export default async function renderAvatar({
|
||||||
text,
|
text, bgColor, imageSrc, size, borderRadius, scale,
|
||||||
bgColor,
|
|
||||||
imageSrc,
|
|
||||||
size,
|
|
||||||
borderRadius,
|
|
||||||
scale,
|
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = size * scale;
|
canvas.width = size * scale;
|
||||||
canvas.height = size * scale;
|
canvas.height = size * scale;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
ctx.scale(scale, scale);
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
@ -32,7 +27,7 @@ export default async function renderAvatar({
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = "anonymous";
|
img.crossOrigin = 'anonymous';
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
img.onerror = reject;
|
img.onerror = reject;
|
||||||
img.onload = resolve;
|
img.onload = resolve;
|
||||||
|
@ -47,10 +42,10 @@ export default async function renderAvatar({
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// centered letter
|
// centered letter
|
||||||
ctx.fillStyle = "#fff";
|
ctx.fillStyle = '#fff';
|
||||||
ctx.font = `${cssVar("--fs-s1")} ${cssVar("--font-primary")}`;
|
ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = 'middle';
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(avatarInitials(text), size / 2, size / 2);
|
ctx.fillText(avatarInitials(text), size / 2, size / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./NotificationBadge.scss";
|
import './NotificationBadge.scss';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
|
|
||||||
function NotificationBadge({ alert, content }) {
|
function NotificationBadge({ alert, content }) {
|
||||||
const notificationClass = alert ? " notification-badge--alert" : "";
|
const notificationClass = alert ? ' notification-badge--alert' : '';
|
||||||
return (
|
return (
|
||||||
<div className={`notification-badge${notificationClass}`}>
|
<div className={`notification-badge${notificationClass}`}>
|
||||||
{content !== null && (
|
{content !== null && <Text variant="b3" weight="bold">{content}</Text>}
|
||||||
<Text variant="b3" weight="bold">
|
|
||||||
{content}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +20,10 @@ NotificationBadge.defaultProps = {
|
||||||
|
|
||||||
NotificationBadge.propTypes = {
|
NotificationBadge.propTypes = {
|
||||||
alert: PropTypes.bool,
|
alert: PropTypes.bool,
|
||||||
content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
content: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotificationBadge;
|
export default NotificationBadge;
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Button.scss";
|
import './Button.scss';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
import RawIcon from "../system-icons/RawIcon";
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
import { blurOnBubbling } from "./script";
|
import { blurOnBubbling } from './script';
|
||||||
|
|
||||||
const Button = React.forwardRef(
|
const Button = React.forwardRef(({
|
||||||
(
|
id, className, variant, iconSrc,
|
||||||
{ id, className, variant, iconSrc, type, onClick, children, disabled },
|
type, onClick, children, disabled,
|
||||||
ref
|
}, ref) => {
|
||||||
) => {
|
const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`;
|
||||||
const iconClass = iconSrc === null ? "" : `btn-${variant}--icon`;
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={id === "" ? undefined : id}
|
id={id === '' ? undefined : id}
|
||||||
className={`${
|
className={`${className ? `${className} ` : ''}btn-${variant} ${iconClass} noselect`}
|
||||||
className ? `${className} ` : ""
|
|
||||||
}btn-${variant} ${iconClass} noselect`}
|
|
||||||
onMouseUp={(e) => blurOnBubbling(e, `.btn-${variant}`)}
|
onMouseUp={(e) => blurOnBubbling(e, `.btn-${variant}`)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
// eslint-disable-next-line react/button-has-type
|
// eslint-disable-next-line react/button-has-type
|
||||||
|
@ -26,19 +23,18 @@ const Button = React.forwardRef(
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{iconSrc !== null && <RawIcon size="small" src={iconSrc} />}
|
{iconSrc !== null && <RawIcon size="small" src={iconSrc} />}
|
||||||
{typeof children === "string" && <Text variant="b1">{children}</Text>}
|
{typeof children === 'string' && <Text variant="b1">{ children }</Text>}
|
||||||
{typeof children !== "string" && children}
|
{typeof children !== 'string' && children }
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
Button.defaultProps = {
|
Button.defaultProps = {
|
||||||
id: "",
|
id: '',
|
||||||
className: null,
|
className: null,
|
||||||
variant: "surface",
|
variant: 'surface',
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
type: "button",
|
type: 'button',
|
||||||
onClick: null,
|
onClick: null,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
@ -46,15 +42,9 @@ Button.defaultProps = {
|
||||||
Button.propTypes = {
|
Button.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
variant: PropTypes.oneOf([
|
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||||
"surface",
|
|
||||||
"primary",
|
|
||||||
"positive",
|
|
||||||
"caution",
|
|
||||||
"danger",
|
|
||||||
]),
|
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
type: PropTypes.oneOf(["button", "submit", "reset"]),
|
type: PropTypes.oneOf(['button', 'submit', 'reset']),
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@use "state";
|
@use 'state';
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
|
|
||||||
.btn-surface,
|
.btn-surface,
|
||||||
.btn-primary,
|
.btn-primary,
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
&--icon {
|
&--icon {
|
||||||
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
||||||
|
|
||||||
}
|
}
|
||||||
.ic-raw {
|
.ic-raw {
|
||||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.btn-surface {
|
.btn-surface {
|
||||||
box-shadow: var(--bs-surface-border);
|
box-shadow: var(--bs-surface-border);
|
||||||
@include color(var(--tc-surface-high), var(--ic-surface-normal));
|
@include color(var(--tc-surface-high), var(--ic-surface-normal));
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Checkbox.scss";
|
import './Checkbox.scss';
|
||||||
|
|
||||||
function Checkbox({ variant, isActive, onToggle, disabled, tabIndex }) {
|
function Checkbox({
|
||||||
const className = `checkbox checkbox-${variant}${
|
variant, isActive, onToggle,
|
||||||
isActive ? " checkbox--active" : ""
|
disabled, tabIndex,
|
||||||
}`;
|
}) {
|
||||||
|
const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`;
|
||||||
if (onToggle === null) return <span className={className} />;
|
if (onToggle === null) return <span className={className} />;
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||||
|
@ -20,7 +21,7 @@ function Checkbox({ variant, isActive, onToggle, disabled, tabIndex }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Checkbox.defaultProps = {
|
Checkbox.defaultProps = {
|
||||||
variant: "primary",
|
variant: 'primary',
|
||||||
isActive: false,
|
isActive: false,
|
||||||
onToggle: null,
|
onToggle: null,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
@ -28,7 +29,7 @@ Checkbox.defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
Checkbox.propTypes = {
|
Checkbox.propTypes = {
|
||||||
variant: PropTypes.oneOf(["primary", "positive", "caution", "danger"]),
|
variant: PropTypes.oneOf(['primary', 'positive', 'caution', 'danger']),
|
||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
onToggle: PropTypes.func,
|
onToggle: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
@use "./state";
|
@use './state';
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
|
|
@ -1,29 +1,18 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./IconButton.scss";
|
import './IconButton.scss';
|
||||||
|
|
||||||
import RawIcon from "../system-icons/RawIcon";
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
import Tooltip from "../tooltip/Tooltip";
|
import Tooltip from '../tooltip/Tooltip';
|
||||||
import { blurOnBubbling } from "./script";
|
import { blurOnBubbling } from './script';
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
|
|
||||||
const IconButton = React.forwardRef(
|
const IconButton = React.forwardRef(({
|
||||||
(
|
variant, size, type,
|
||||||
{
|
tooltip, tooltipPlacement, src,
|
||||||
variant,
|
onClick, tabIndex, disabled, isImage,
|
||||||
size,
|
|
||||||
type,
|
|
||||||
tooltip,
|
|
||||||
tooltipPlacement,
|
|
||||||
src,
|
|
||||||
onClick,
|
|
||||||
tabIndex,
|
|
||||||
disabled,
|
|
||||||
isImage,
|
|
||||||
className,
|
className,
|
||||||
},
|
}, ref) => {
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const btn = (
|
const btn = (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -47,34 +36,27 @@ const IconButton = React.forwardRef(
|
||||||
{btn}
|
{btn}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
IconButton.defaultProps = {
|
IconButton.defaultProps = {
|
||||||
variant: "surface",
|
variant: 'surface',
|
||||||
size: "normal",
|
size: 'normal',
|
||||||
type: "button",
|
type: 'button',
|
||||||
tooltip: null,
|
tooltip: null,
|
||||||
tooltipPlacement: "top",
|
tooltipPlacement: 'top',
|
||||||
onClick: null,
|
onClick: null,
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
isImage: false,
|
isImage: false,
|
||||||
className: "",
|
className: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
IconButton.propTypes = {
|
IconButton.propTypes = {
|
||||||
variant: PropTypes.oneOf([
|
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||||
"surface",
|
size: PropTypes.oneOf(['normal', 'small', 'extra-small']),
|
||||||
"primary",
|
type: PropTypes.oneOf(['button', 'submit', 'reset']),
|
||||||
"positive",
|
|
||||||
"caution",
|
|
||||||
"danger",
|
|
||||||
]),
|
|
||||||
size: PropTypes.oneOf(["normal", "small", "extra-small"]),
|
|
||||||
type: PropTypes.oneOf(["button", "submit", "reset"]),
|
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
tooltipPlacement: PropTypes.oneOf(["top", "right", "bottom", "left"]),
|
tooltipPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||||
src: PropTypes.string.isRequired,
|
src: PropTypes.string.isRequired,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
tabIndex: PropTypes.number,
|
tabIndex: PropTypes.number,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "state";
|
@use 'state';
|
||||||
|
|
||||||
.ic-btn {
|
.ic-btn {
|
||||||
padding: var(--sp-extra-tight);
|
padding: var(--sp-extra-tight);
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./RadioButton.scss";
|
import './RadioButton.scss';
|
||||||
|
|
||||||
function RadioButton({ isActive, onToggle, disabled }) {
|
function RadioButton({ isActive, onToggle, disabled }) {
|
||||||
if (onToggle === null)
|
if (onToggle === null) return <span className={`radio-btn${isActive ? ' radio-btn--active' : ''}`} />;
|
||||||
return (
|
|
||||||
<span className={`radio-btn${isActive ? " radio-btn--active" : ""}`} />
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||||
<button
|
<button
|
||||||
onClick={() => onToggle(!isActive)}
|
onClick={() => onToggle(!isActive)}
|
||||||
className={`radio-btn${isActive ? " radio-btn--active" : ""}`}
|
className={`radio-btn${isActive ? ' radio-btn--active' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
@use "./state";
|
@use './state';
|
||||||
|
|
||||||
.radio-btn {
|
.radio-btn {
|
||||||
@extend .cp-fx__row--c-c;
|
@extend .cp-fx__row--c-c;
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
@include state.disabled;
|
@include state.disabled;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Toggle.scss";
|
import './Toggle.scss';
|
||||||
|
|
||||||
function Toggle({ isActive, onToggle, disabled }) {
|
function Toggle({ isActive, onToggle, disabled }) {
|
||||||
const className = `toggle${isActive ? " toggle--active" : ""}`;
|
const className = `toggle${isActive ? ' toggle--active' : ''}`;
|
||||||
if (onToggle === null) return <span className={className} />;
|
if (onToggle === null) return <span className={className} />;
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
@use "./state";
|
@use './state';
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
|
@ -16,13 +16,14 @@
|
||||||
transition: background 200ms ease-in-out;
|
transition: background 200ms ease-in-out;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background-color: var(--tc-surface-low);
|
background-color: var(--tc-surface-low);
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
border-radius: calc(var(--bo-radius) / 2);
|
||||||
transition: transform 200ms ease-in-out, opacity 200ms ease-in-out;
|
transition: transform 200ms ease-in-out,
|
||||||
|
opacity 200ms ease-in-out;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
@mixin hover($color) {
|
@mixin hover($color) {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -10,10 +10,7 @@ function blurOnBubbling(e, selector) {
|
||||||
|
|
||||||
for (let elIndex = 0; elIndex < bubblingPath.length; elIndex += 1) {
|
for (let elIndex = 0; elIndex < bubblingPath.length; elIndex += 1) {
|
||||||
if (bubblingPath[elIndex] === document) {
|
if (bubblingPath[elIndex] === document) {
|
||||||
console.warn(
|
console.warn(blurOnBubbling, 'blurOnBubbling: not found selector in bubbling path');
|
||||||
blurOnBubbling,
|
|
||||||
"blurOnBubbling: not found selector in bubbling path"
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (bubblingPath[elIndex].matches(selector)) {
|
if (bubblingPath[elIndex].matches(selector)) {
|
||||||
|
|
|
@ -1,28 +1,24 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./InfoCard.scss";
|
import './InfoCard.scss';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
import RawIcon from "../system-icons/RawIcon";
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
import IconButton from "../button/IconButton";
|
import IconButton from '../button/IconButton';
|
||||||
|
|
||||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
function InfoCard({
|
function InfoCard({
|
||||||
className,
|
className, style,
|
||||||
style,
|
variant, iconSrc,
|
||||||
variant,
|
title, content,
|
||||||
iconSrc,
|
rounded, requestClose,
|
||||||
title,
|
|
||||||
content,
|
|
||||||
rounded,
|
|
||||||
requestClose,
|
|
||||||
}) {
|
}) {
|
||||||
const classes = [`info-card info-card--${variant}`];
|
const classes = [`info-card info-card--${variant}`];
|
||||||
if (rounded) classes.push("info-card--rounded");
|
if (rounded) classes.push('info-card--rounded');
|
||||||
if (className) classes.push(className);
|
if (className) classes.push(className);
|
||||||
return (
|
return (
|
||||||
<div className={classes.join(" ")} style={style}>
|
<div className={classes.join(' ')} style={style}>
|
||||||
{iconSrc && (
|
{iconSrc && (
|
||||||
<div className="info-card__icon">
|
<div className="info-card__icon">
|
||||||
<RawIcon color={`var(--ic-${variant}-high)`} src={iconSrc} />
|
<RawIcon color={`var(--ic-${variant}-high)`} src={iconSrc} />
|
||||||
|
@ -42,7 +38,7 @@ function InfoCard({
|
||||||
InfoCard.defaultProps = {
|
InfoCard.defaultProps = {
|
||||||
className: null,
|
className: null,
|
||||||
style: null,
|
style: null,
|
||||||
variant: "surface",
|
variant: 'surface',
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
content: null,
|
content: null,
|
||||||
rounded: false,
|
rounded: false,
|
||||||
|
@ -52,13 +48,7 @@ InfoCard.defaultProps = {
|
||||||
InfoCard.propTypes = {
|
InfoCard.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
style: PropTypes.shape({}),
|
style: PropTypes.shape({}),
|
||||||
variant: PropTypes.oneOf([
|
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||||
"surface",
|
|
||||||
"primary",
|
|
||||||
"positive",
|
|
||||||
"caution",
|
|
||||||
"danger",
|
|
||||||
]),
|
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
content: PropTypes.node,
|
content: PropTypes.node,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use ".././../partials/flex";
|
@use '.././../partials/flex';
|
||||||
@use ".././../partials/dir";
|
@use '.././../partials/dir';
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -34,6 +34,7 @@
|
||||||
&--surface {
|
&--surface {
|
||||||
border-color: var(--bg-surface-border);
|
border-color: var(--bg-surface-border);
|
||||||
background-color: var(--bg-surface-hover);
|
background-color: var(--bg-surface-hover);
|
||||||
|
|
||||||
}
|
}
|
||||||
&--primary {
|
&--primary {
|
||||||
border-color: var(--bg-primary);
|
border-color: var(--bg-primary);
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Chip.scss";
|
import './Chip.scss';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
import RawIcon from "../system-icons/RawIcon";
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
|
|
||||||
function Chip({ iconSrc, iconColor, text, children, onClick }) {
|
function Chip({
|
||||||
|
iconSrc, iconColor, text, children,
|
||||||
|
onClick,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<button className="chip" type="button" onClick={onClick}>
|
<button className="chip" type="button" onClick={onClick}>
|
||||||
{iconSrc != null && (
|
{iconSrc != null && <RawIcon src={iconSrc} color={iconColor} size="extra-small" />}
|
||||||
<RawIcon src={iconSrc} color={iconColor} size="extra-small" />
|
{(text != null && text !== '') && <Text variant="b3">{text}</Text>}
|
||||||
)}
|
|
||||||
{text != null && text !== "" && <Text variant="b3">{text}</Text>}
|
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
padding: var(--sp-ultra-tight) var(--sp-extra-tight);
|
padding: var(--sp-ultra-tight) var(--sp-extra-tight);
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ContextMenu.scss";
|
import './ContextMenu.scss';
|
||||||
|
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from '@tippyjs/react';
|
||||||
import "tippy.js/animations/scale-extreme.css";
|
import 'tippy.js/animations/scale-extreme.css';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
import Button from "../button/Button";
|
import Button from '../button/Button';
|
||||||
import ScrollView from "../scroll/ScrollView";
|
import ScrollView from '../scroll/ScrollView';
|
||||||
|
|
||||||
function ContextMenu({ content, placement, maxWidth, render, afterToggle }) {
|
function ContextMenu({
|
||||||
|
content, placement, maxWidth, render, afterToggle,
|
||||||
|
}) {
|
||||||
const [isVisible, setVisibility] = useState(false);
|
const [isVisible, setVisibility] = useState(false);
|
||||||
const showMenu = () => setVisibility(true);
|
const showMenu = () => setVisibility(true);
|
||||||
const hideMenu = () => setVisibility(false);
|
const hideMenu = () => setVisibility(false);
|
||||||
|
@ -24,11 +26,7 @@ function ContextMenu({ content, placement, maxWidth, render, afterToggle }) {
|
||||||
className="context-menu"
|
className="context-menu"
|
||||||
visible={isVisible}
|
visible={isVisible}
|
||||||
onClickOutside={hideMenu}
|
onClickOutside={hideMenu}
|
||||||
content={
|
content={<ScrollView invisible>{typeof content === 'function' ? content(hideMenu) : content}</ScrollView>}
|
||||||
<ScrollView invisible>
|
|
||||||
{typeof content === "function" ? content(hideMenu) : content}
|
|
||||||
</ScrollView>
|
|
||||||
}
|
|
||||||
placement={placement}
|
placement={placement}
|
||||||
interactive
|
interactive
|
||||||
arrow={false}
|
arrow={false}
|
||||||
|
@ -41,15 +39,21 @@ function ContextMenu({ content, placement, maxWidth, render, afterToggle }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextMenu.defaultProps = {
|
ContextMenu.defaultProps = {
|
||||||
maxWidth: "unset",
|
maxWidth: 'unset',
|
||||||
placement: "right",
|
placement: 'right',
|
||||||
afterToggle: null,
|
afterToggle: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
ContextMenu.propTypes = {
|
ContextMenu.propTypes = {
|
||||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
content: PropTypes.oneOfType([
|
||||||
placement: PropTypes.oneOf(["top", "right", "bottom", "left"]),
|
PropTypes.node,
|
||||||
maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
PropTypes.func,
|
||||||
|
]).isRequired,
|
||||||
|
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||||
|
maxWidth: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
render: PropTypes.func.isRequired,
|
render: PropTypes.func.isRequired,
|
||||||
afterToggle: PropTypes.func,
|
afterToggle: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
@ -57,7 +61,7 @@ ContextMenu.propTypes = {
|
||||||
function MenuHeader({ children }) {
|
function MenuHeader({ children }) {
|
||||||
return (
|
return (
|
||||||
<div className="context-menu__header">
|
<div className="context-menu__header">
|
||||||
<Text variant="b3">{children}</Text>
|
<Text variant="b3">{ children }</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,7 +70,10 @@ MenuHeader.propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function MenuItem({ variant, iconSrc, type, onClick, children, disabled }) {
|
function MenuItem({
|
||||||
|
variant, iconSrc, type,
|
||||||
|
onClick, children, disabled,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="context-menu__item">
|
<div className="context-menu__item">
|
||||||
<Button
|
<Button
|
||||||
|
@ -76,33 +83,33 @@ function MenuItem({ variant, iconSrc, type, onClick, children, disabled }) {
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{children}
|
{ children }
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem.defaultProps = {
|
MenuItem.defaultProps = {
|
||||||
variant: "surface",
|
variant: 'surface',
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
type: "button",
|
type: 'button',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
onClick: null,
|
onClick: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuItem.propTypes = {
|
MenuItem.propTypes = {
|
||||||
variant: PropTypes.oneOf(["surface", "positive", "caution", "danger"]),
|
variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']),
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
type: PropTypes.oneOf(["button", "submit"]),
|
type: PropTypes.oneOf(['button', 'submit']),
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
function MenuBorder() {
|
function MenuBorder() {
|
||||||
return (
|
return <div style={{ borderBottom: '1px solid var(--bg-surface-border)' }}> </div>;
|
||||||
<div style={{ borderBottom: "1px solid var(--bg-surface-border)" }}> </div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ContextMenu as default, MenuHeader, MenuItem, MenuBorder };
|
export {
|
||||||
|
ContextMenu as default, MenuHeader, MenuItem, MenuBorder,
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.context-menu {
|
.context-menu {
|
||||||
background-color: var(--bg-surface);
|
background-color: var(--bg-surface);
|
||||||
|
@ -59,7 +59,11 @@
|
||||||
|
|
||||||
// if item doesn't have icon
|
// if item doesn't have icon
|
||||||
.text:first-child {
|
.text:first-child {
|
||||||
@include dir.side(margin, calc(var(--ic-small) + var(--sp-tight)), 0);
|
@include dir.side(
|
||||||
|
margin,
|
||||||
|
calc(var(--ic-small) + var(--sp-tight)),
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.btn-surface:focus {
|
.btn-surface:focus {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import cons from "../../../client/state/cons";
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from "../../../client/state/navigation";
|
import navigation from '../../../client/state/navigation';
|
||||||
|
|
||||||
import ContextMenu from "./ContextMenu";
|
import ContextMenu from './ContextMenu';
|
||||||
|
|
||||||
let key = null;
|
let key = null;
|
||||||
function ReusableContextMenu() {
|
function ReusableContextMenu() {
|
||||||
|
@ -29,20 +29,14 @@ function ReusableContextMenu() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData({
|
setData({
|
||||||
placement,
|
placement, cords, render, afterClose,
|
||||||
cords,
|
|
||||||
render,
|
|
||||||
afterClose,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
navigation.on(
|
navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen);
|
||||||
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
|
|
||||||
handleContextMenuOpen
|
|
||||||
);
|
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(
|
navigation.removeListener(
|
||||||
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
|
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
|
||||||
handleContextMenuOpen
|
handleContextMenuOpen,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
@ -65,24 +59,24 @@ function ReusableContextMenu() {
|
||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
afterToggle={handleAfterToggle}
|
afterToggle={handleAfterToggle}
|
||||||
placement={data?.placement || "right"}
|
placement={data?.placement || 'right'}
|
||||||
content={data?.render(closeMenu) ?? ""}
|
content={data?.render(closeMenu) ?? ''}
|
||||||
render={(toggleMenu) => (
|
render={(toggleMenu) => (
|
||||||
<input
|
<input
|
||||||
ref={openerRef}
|
ref={openerRef}
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
type="button"
|
type="button"
|
||||||
style={{
|
style={{
|
||||||
width: "32px",
|
width: '32px',
|
||||||
height: "32px",
|
height: '32px',
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent',
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
border: "none",
|
border: 'none',
|
||||||
visibility: "hidden",
|
visibility: 'hidden',
|
||||||
appearance: "none",
|
appearance: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,38 +1,28 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Divider.scss";
|
import './Divider.scss';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
|
|
||||||
function Divider({ text, variant, align }) {
|
function Divider({ text, variant, align }) {
|
||||||
const dividerClass = ` divider--${variant} divider--${align}`;
|
const dividerClass = ` divider--${variant} divider--${align}`;
|
||||||
return (
|
return (
|
||||||
<div className={`divider${dividerClass}`}>
|
<div className={`divider${dividerClass}`}>
|
||||||
{text !== null && (
|
{text !== null && <Text className="divider__text" variant="b3" weight="bold">{text}</Text>}
|
||||||
<Text className="divider__text" variant="b3" weight="bold">
|
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider.defaultProps = {
|
Divider.defaultProps = {
|
||||||
text: null,
|
text: null,
|
||||||
variant: "surface",
|
variant: 'surface',
|
||||||
align: "center",
|
align: 'center',
|
||||||
};
|
};
|
||||||
|
|
||||||
Divider.propTypes = {
|
Divider.propTypes = {
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
variant: PropTypes.oneOf([
|
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||||
"surface",
|
align: PropTypes.oneOf(['left', 'center', 'right']),
|
||||||
"primary",
|
|
||||||
"positive",
|
|
||||||
"caution",
|
|
||||||
"danger",
|
|
||||||
]),
|
|
||||||
align: PropTypes.oneOf(["left", "center", "right"]),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Divider;
|
export default Divider;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.divider-line {
|
.divider-line {
|
||||||
content: "";
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-bottom: 1px solid var(--local-divider-color);
|
border-bottom: 1px solid var(--local-divider-color);
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
.divider--primary {
|
.divider--primary {
|
||||||
--local-divider-color: var(--bg-primary);
|
--local-divider-color: var(--bg-primary);
|
||||||
--local-divider-opacity: 0.8;
|
--local-divider-opacity: .8;
|
||||||
.divider__text {
|
.divider__text {
|
||||||
color: var(--tc-primary-high);
|
color: var(--tc-primary-high);
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
}
|
}
|
||||||
.divider--positive {
|
.divider--positive {
|
||||||
--local-divider-color: var(--bg-positive);
|
--local-divider-color: var(--bg-positive);
|
||||||
--local-divider-opacity: 0.8;
|
--local-divider-opacity: .8;
|
||||||
.divider__text {
|
.divider__text {
|
||||||
color: var(--bg-surface);
|
color: var(--bg-surface);
|
||||||
background-color: var(--bg-positive);
|
background-color: var(--bg-positive);
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
}
|
}
|
||||||
.divider--danger {
|
.divider--danger {
|
||||||
--local-divider-color: var(--bg-danger);
|
--local-divider-color: var(--bg-danger);
|
||||||
--local-divider-opacity: 0.8;
|
--local-divider-opacity: .8;
|
||||||
.divider__text {
|
.divider__text {
|
||||||
color: var(--bg-surface);
|
color: var(--bg-surface);
|
||||||
background-color: var(--bg-danger);
|
background-color: var(--bg-danger);
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
}
|
}
|
||||||
.divider--caution {
|
.divider--caution {
|
||||||
--local-divider-color: var(--bg-caution);
|
--local-divider-color: var(--bg-caution);
|
||||||
--local-divider-opacity: 0.8;
|
--local-divider-opacity: .8;
|
||||||
.divider__text {
|
.divider__text {
|
||||||
color: var(--bg-surface);
|
color: var(--bg-surface);
|
||||||
background-color: var(--bg-caution);
|
background-color: var(--bg-caution);
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Header.scss";
|
import './Header.scss';
|
||||||
|
|
||||||
function Header({ children }) {
|
function Header({ children }) {
|
||||||
return <div className="header">{children}</div>;
|
return (
|
||||||
|
<div className="header">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.propTypes = {
|
Header.propTypes = {
|
||||||
|
@ -11,7 +15,11 @@ Header.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function TitleWrapper({ children }) {
|
function TitleWrapper({ children }) {
|
||||||
return <div className="header__title-wrapper">{children}</div>;
|
return (
|
||||||
|
<div className="header__title-wrapper">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TitleWrapper.propTypes = {
|
TitleWrapper.propTypes = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
@extend .cp-txt__ellipsis;
|
@extend .cp-txt__ellipsis;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
& > .text-b3 {
|
& > .text-b3{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,26 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Input.scss";
|
import './Input.scss';
|
||||||
|
|
||||||
import TextareaAutosize from "react-autosize-textarea";
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
|
|
||||||
function Input({
|
function Input({
|
||||||
id,
|
id, label, name, value, placeholder,
|
||||||
label,
|
required, type, onChange, forwardRef,
|
||||||
name,
|
resizable, minHeight, onResize, state,
|
||||||
value,
|
onKeyDown, disabled, autoFocus,
|
||||||
placeholder,
|
|
||||||
required,
|
|
||||||
type,
|
|
||||||
onChange,
|
|
||||||
forwardRef,
|
|
||||||
resizable,
|
|
||||||
minHeight,
|
|
||||||
onResize,
|
|
||||||
state,
|
|
||||||
onKeyDown,
|
|
||||||
disabled,
|
|
||||||
autoFocus,
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
{label !== "" && (
|
{ label !== '' && <label className="input__label text-b2" htmlFor={id}>{label}</label> }
|
||||||
<label className="input__label text-b2" htmlFor={id}>
|
{ resizable
|
||||||
{label}
|
? (
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
{resizable ? (
|
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style={{ minHeight: `${minHeight}px` }}
|
style={{ minHeight: `${minHeight}px` }}
|
||||||
name={name}
|
name={name}
|
||||||
id={id}
|
id={id}
|
||||||
className={`input input--resizable${
|
className={`input input--resizable${state !== 'normal' ? ` input--${state}` : ''}`}
|
||||||
state !== "normal" ? ` input--${state}` : ""
|
|
||||||
}`}
|
|
||||||
ref={forwardRef}
|
ref={forwardRef}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
@ -56,7 +39,7 @@ function Input({
|
||||||
ref={forwardRef}
|
ref={forwardRef}
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
className={`input ${state !== "normal" ? ` input--${state}` : ""}`}
|
className={`input ${state !== 'normal' ? ` input--${state}` : ''}`}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
required={required}
|
required={required}
|
||||||
|
@ -75,18 +58,18 @@ function Input({
|
||||||
|
|
||||||
Input.defaultProps = {
|
Input.defaultProps = {
|
||||||
id: null,
|
id: null,
|
||||||
name: "",
|
name: '',
|
||||||
label: "",
|
label: '',
|
||||||
value: "",
|
value: '',
|
||||||
placeholder: "",
|
placeholder: '',
|
||||||
type: "text",
|
type: 'text',
|
||||||
required: false,
|
required: false,
|
||||||
onChange: null,
|
onChange: null,
|
||||||
forwardRef: null,
|
forwardRef: null,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
minHeight: 46,
|
minHeight: 46,
|
||||||
onResize: null,
|
onResize: null,
|
||||||
state: "normal",
|
state: 'normal',
|
||||||
onKeyDown: null,
|
onKeyDown: null,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
|
@ -105,7 +88,7 @@ Input.propTypes = {
|
||||||
resizable: PropTypes.bool,
|
resizable: PropTypes.bool,
|
||||||
minHeight: PropTypes.number,
|
minHeight: PropTypes.number,
|
||||||
onResize: PropTypes.func,
|
onResize: PropTypes.func,
|
||||||
state: PropTypes.oneOf(["normal", "success", "error"]),
|
state: PropTypes.oneOf(['normal', 'success', 'error']),
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../atoms/scroll/scrollbar";
|
@use '../../atoms/scroll/scrollbar';
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -47,6 +47,6 @@
|
||||||
box-shadow: var(--bs-primary-border);
|
box-shadow: var(--bs-primary-border);
|
||||||
}
|
}
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--tc-surface-low);
|
color: var(--tc-surface-low)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,27 +1,23 @@
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Math.scss";
|
import './Math.scss';
|
||||||
|
|
||||||
import katex from "katex";
|
import katex from 'katex';
|
||||||
import "katex/dist/katex.min.css";
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
import "katex/dist/contrib/copy-tex";
|
import 'katex/dist/contrib/copy-tex';
|
||||||
|
|
||||||
const Math = React.memo(
|
const Math = React.memo(({
|
||||||
({ content, throwOnError, errorColor, displayMode }) => {
|
content, throwOnError, errorColor, displayMode,
|
||||||
|
}) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
katex.render(content, ref.current, {
|
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
|
||||||
throwOnError,
|
|
||||||
errorColor,
|
|
||||||
displayMode,
|
|
||||||
});
|
|
||||||
}, [content, throwOnError, errorColor, displayMode]);
|
}, [content, throwOnError, errorColor, displayMode]);
|
||||||
|
|
||||||
return <span ref={ref} />;
|
return <span ref={ref} />;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
Math.defaultProps = {
|
Math.defaultProps = {
|
||||||
throwOnError: null,
|
throwOnError: null,
|
||||||
errorColor: null,
|
errorColor: null,
|
||||||
|
|
|
@ -1,43 +1,36 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./RawModal.scss";
|
import './RawModal.scss';
|
||||||
|
|
||||||
import Modal from "react-modal";
|
import Modal from 'react-modal';
|
||||||
|
|
||||||
import navigation from "../../../client/state/navigation";
|
import navigation from '../../../client/state/navigation';
|
||||||
|
|
||||||
Modal.setAppElement("#root");
|
Modal.setAppElement('#root');
|
||||||
|
|
||||||
function RawModal({
|
function RawModal({
|
||||||
className,
|
className, overlayClassName,
|
||||||
overlayClassName,
|
isOpen, size, onAfterOpen, onAfterClose,
|
||||||
isOpen,
|
onRequestClose, closeFromOutside, children,
|
||||||
size,
|
|
||||||
onAfterOpen,
|
|
||||||
onAfterClose,
|
|
||||||
onRequestClose,
|
|
||||||
closeFromOutside,
|
|
||||||
children,
|
|
||||||
}) {
|
}) {
|
||||||
let modalClass = className !== null ? `${className} ` : "";
|
let modalClass = (className !== null) ? `${className} ` : '';
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case "large":
|
case 'large':
|
||||||
modalClass += "raw-modal__large ";
|
modalClass += 'raw-modal__large ';
|
||||||
break;
|
break;
|
||||||
case "medium":
|
case 'medium':
|
||||||
modalClass += "raw-modal__medium ";
|
modalClass += 'raw-modal__medium ';
|
||||||
break;
|
break;
|
||||||
case "small":
|
case 'small':
|
||||||
default:
|
default:
|
||||||
modalClass += "raw-modal__small ";
|
modalClass += 'raw-modal__small ';
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setIsRawModalVisible(isOpen);
|
navigation.setIsRawModalVisible(isOpen);
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const modalOverlayClass =
|
const modalOverlayClass = (overlayClassName !== null) ? `${overlayClassName} ` : '';
|
||||||
overlayClassName !== null ? `${overlayClassName} ` : "";
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={`${modalClass}raw-modal`}
|
className={`${modalClass}raw-modal`}
|
||||||
|
@ -58,7 +51,7 @@ function RawModal({
|
||||||
RawModal.defaultProps = {
|
RawModal.defaultProps = {
|
||||||
className: null,
|
className: null,
|
||||||
overlayClassName: null,
|
overlayClassName: null,
|
||||||
size: "small",
|
size: 'small',
|
||||||
onAfterOpen: null,
|
onAfterOpen: null,
|
||||||
onAfterClose: null,
|
onAfterClose: null,
|
||||||
onRequestClose: null,
|
onRequestClose: null,
|
||||||
|
@ -69,7 +62,7 @@ RawModal.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
overlayClassName: PropTypes.string,
|
overlayClassName: PropTypes.string,
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
size: PropTypes.oneOf(["large", "medium", "small"]),
|
size: PropTypes.oneOf(['large', 'medium', 'small']),
|
||||||
onAfterOpen: PropTypes.func,
|
onAfterOpen: PropTypes.func,
|
||||||
onAfterClose: PropTypes.func,
|
onAfterClose: PropTypes.func,
|
||||||
onRequestClose: PropTypes.func,
|
onRequestClose: PropTypes.func,
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
@keyframes raw-modal--content {
|
@keyframes raw-modal--content {
|
||||||
0% {
|
0% {
|
||||||
transform: translateY(100px);
|
transform: translateY(100px);
|
||||||
opacity: 0.5;
|
opacity: .5;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ScrollView.scss";
|
import './ScrollView.scss';
|
||||||
|
|
||||||
const ScrollView = React.forwardRef(
|
const ScrollView = React.forwardRef(({
|
||||||
({ horizontal, vertical, autoHide, invisible, onScroll, children }, ref) => {
|
horizontal, vertical, autoHide, invisible, onScroll, children,
|
||||||
let scrollbarClasses = "";
|
}, ref) => {
|
||||||
if (horizontal) scrollbarClasses += " scrollbar__h";
|
let scrollbarClasses = '';
|
||||||
if (vertical) scrollbarClasses += " scrollbar__v";
|
if (horizontal) scrollbarClasses += ' scrollbar__h';
|
||||||
if (autoHide) scrollbarClasses += " scrollbar--auto-hide";
|
if (vertical) scrollbarClasses += ' scrollbar__v';
|
||||||
if (invisible) scrollbarClasses += " scrollbar--invisible";
|
if (autoHide) scrollbarClasses += ' scrollbar--auto-hide';
|
||||||
|
if (invisible) scrollbarClasses += ' scrollbar--invisible';
|
||||||
return (
|
return (
|
||||||
<div
|
<div onScroll={onScroll} ref={ref} className={`scrollbar${scrollbarClasses}`}>
|
||||||
onScroll={onScroll}
|
|
||||||
ref={ref}
|
|
||||||
className={`scrollbar${scrollbarClasses}`}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
ScrollView.defaultProps = {
|
ScrollView.defaultProps = {
|
||||||
horizontal: false,
|
horizontal: false,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
@use "_scrollbar";
|
@use '_scrollbar';
|
||||||
|
|
||||||
@mixin paddingForSafari($padding) {
|
@mixin paddingForSafari($padding) {
|
||||||
@media not all and (min-resolution: 0.001dpcm) {
|
@media not all and (min-resolution:.001dpcm) {
|
||||||
@include dir.side(padding, 0, $padding);
|
@include dir.side(padding, 0, $padding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./SegmentedControls.scss";
|
import './SegmentedControls.scss';
|
||||||
|
|
||||||
import { blurOnBubbling } from "../button/script";
|
import { blurOnBubbling } from '../button/script';
|
||||||
|
|
||||||
import Text from "../text/Text";
|
import Text from '../text/Text';
|
||||||
import RawIcon from "../system-icons/RawIcon";
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
|
|
||||||
function SegmentedControls({ selected, segments, onSelect }) {
|
function SegmentedControls({
|
||||||
|
selected, segments, onSelect,
|
||||||
|
}) {
|
||||||
const [select, setSelect] = useState(selected);
|
const [select, setSelect] = useState(selected);
|
||||||
|
|
||||||
function selectSegment(segmentIndex) {
|
function selectSegment(segmentIndex) {
|
||||||
|
@ -21,34 +23,32 @@ function SegmentedControls({ selected, segments, onSelect }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="segmented-controls">
|
<div className="segmented-controls">
|
||||||
{segments.map((segment, index) => (
|
{
|
||||||
|
segments.map((segment, index) => (
|
||||||
<button
|
<button
|
||||||
key={Math.random().toString(20).substr(2, 6)}
|
key={Math.random().toString(20).substr(2, 6)}
|
||||||
className={`segment-btn${
|
className={`segment-btn${select === index ? ' segment-btn--active' : ''}`}
|
||||||
select === index ? " segment-btn--active" : ""
|
|
||||||
}`}
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectSegment(index)}
|
onClick={() => selectSegment(index)}
|
||||||
onMouseUp={(e) => blurOnBubbling(e, ".segment-btn")}
|
onMouseUp={(e) => blurOnBubbling(e, '.segment-btn')}
|
||||||
>
|
>
|
||||||
<div className="segment-btn__base">
|
<div className="segment-btn__base">
|
||||||
{segment.iconSrc && <RawIcon size="small" src={segment.iconSrc} />}
|
{segment.iconSrc && <RawIcon size="small" src={segment.iconSrc} />}
|
||||||
{segment.text && <Text variant="b2">{segment.text}</Text>}
|
{segment.text && <Text variant="b2">{segment.text}</Text>}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SegmentedControls.propTypes = {
|
SegmentedControls.propTypes = {
|
||||||
selected: PropTypes.number.isRequired,
|
selected: PropTypes.number.isRequired,
|
||||||
segments: PropTypes.arrayOf(
|
segments: PropTypes.arrayOf(PropTypes.shape({
|
||||||
PropTypes.shape({
|
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
})
|
})).isRequired,
|
||||||
).isRequired,
|
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../button/state";
|
@use '../button/state';
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.segmented-controls {
|
.segmented-controls {
|
||||||
background-color: var(--bg-surface-low);
|
background-color: var(--bg-surface-low);
|
||||||
|
@ -40,22 +40,18 @@
|
||||||
& + .segment-btn .segment-btn__base {
|
& + .segment-btn .segment-btn__base {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
&:first-child {
|
&:first-child{
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
[dir="rtl"] & {
|
[dir=rtl] & {
|
||||||
border-left: 1px solid var(--bg-surface-border);
|
border-left: 1px solid var(--bg-surface-border);
|
||||||
border-right: 1px solid var(--bg-surface-border);
|
border-right: 1px solid var(--bg-surface-border);
|
||||||
|
|
||||||
&:first-child {
|
&:first-child { border-right: none;}
|
||||||
border-right: none;
|
&:last-child { border-left: none;}
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Spinner.scss";
|
import './Spinner.scss';
|
||||||
|
|
||||||
function Spinner({ size }) {
|
function Spinner({ size }) {
|
||||||
return <div className={`donut-spinner donut-spinner--${size}`}> </div>;
|
return (
|
||||||
|
<div className={`donut-spinner donut-spinner--${size}`}> </div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Spinner.defaultProps = {
|
Spinner.defaultProps = {
|
||||||
size: "normal",
|
size: 'normal',
|
||||||
};
|
};
|
||||||
|
|
||||||
Spinner.propTypes = {
|
Spinner.propTypes = {
|
||||||
size: PropTypes.oneOf(["normal", "small"]),
|
size: PropTypes.oneOf(['normal', 'small']),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Spinner;
|
export default Spinner;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./RawIcon.scss";
|
import './RawIcon.scss';
|
||||||
|
|
||||||
function RawIcon({ color, size, src, isImage }) {
|
function RawIcon({ color, size, src, isImage }) {
|
||||||
const style = {};
|
const style = {};
|
||||||
if (color !== null) style.backgroundColor = color;
|
if (color !== null) style.backgroundColor = color;
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
style.backgroundColor = "transparent";
|
style.backgroundColor = 'transparent';
|
||||||
style.backgroundImage = `url("${src}")`;
|
style.backgroundImage = `url("${src}")`;
|
||||||
} else {
|
} else {
|
||||||
style.WebkitMaskImage = `url("${src}")`;
|
style.WebkitMaskImage = `url("${src}")`;
|
||||||
|
@ -18,13 +18,13 @@ function RawIcon({ color, size, src, isImage }) {
|
||||||
|
|
||||||
RawIcon.defaultProps = {
|
RawIcon.defaultProps = {
|
||||||
color: null,
|
color: null,
|
||||||
size: "normal",
|
size: 'normal',
|
||||||
isImage: false,
|
isImage: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
RawIcon.propTypes = {
|
RawIcon.propTypes = {
|
||||||
color: PropTypes.string,
|
color: PropTypes.string,
|
||||||
size: PropTypes.oneOf(["large", "normal", "small", "extra-small"]),
|
size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']),
|
||||||
src: PropTypes.string.isRequired,
|
src: PropTypes.string.isRequired,
|
||||||
isImage: PropTypes.bool,
|
isImage: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Tabs.scss";
|
import './Tabs.scss';
|
||||||
|
|
||||||
import Button from "../button/Button";
|
import Button from '../button/Button';
|
||||||
import ScrollView from "../scroll/ScrollView";
|
import ScrollView from '../scroll/ScrollView';
|
||||||
|
|
||||||
function TabItem({ selected, iconSrc, onClick, children, disabled }) {
|
function TabItem({
|
||||||
const isSelected = selected ? "tab-item--selected" : "";
|
selected, iconSrc,
|
||||||
|
onClick, children, disabled,
|
||||||
|
}) {
|
||||||
|
const isSelected = selected ? 'tab-item--selected' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -75,7 +78,7 @@ Tabs.propTypes = {
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
})
|
}),
|
||||||
).isRequired,
|
).isRequired,
|
||||||
defaultSelected: PropTypes.number,
|
defaultSelected: PropTypes.number,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
|
|
|
@ -1,51 +1,30 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Text.scss";
|
import './Text.scss';
|
||||||
|
|
||||||
function Text({ className, style, variant, weight, primary, span, children }) {
|
function Text({
|
||||||
|
className, style, variant, weight,
|
||||||
|
primary, span, children,
|
||||||
|
}) {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
if (className) classes.push(className);
|
if (className) classes.push(className);
|
||||||
|
|
||||||
classes.push(`text text-${variant} text-${weight}`);
|
classes.push(`text text-${variant} text-${weight}`);
|
||||||
if (primary) classes.push("font-primary");
|
if (primary) classes.push('font-primary');
|
||||||
|
|
||||||
const textClass = classes.join(" ");
|
const textClass = classes.join(' ');
|
||||||
if (span)
|
if (span) return <span className={textClass} style={style}>{ children }</span>;
|
||||||
return (
|
if (variant === 'h1') return <h1 className={textClass} style={style}>{ children }</h1>;
|
||||||
<span className={textClass} style={style}>
|
if (variant === 'h2') return <h2 className={textClass} style={style}>{ children }</h2>;
|
||||||
{children}
|
if (variant === 's1') return <h4 className={textClass} style={style}>{ children }</h4>;
|
||||||
</span>
|
return <p className={textClass} style={style}>{ children }</p>;
|
||||||
);
|
|
||||||
if (variant === "h1")
|
|
||||||
return (
|
|
||||||
<h1 className={textClass} style={style}>
|
|
||||||
{children}
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
if (variant === "h2")
|
|
||||||
return (
|
|
||||||
<h2 className={textClass} style={style}>
|
|
||||||
{children}
|
|
||||||
</h2>
|
|
||||||
);
|
|
||||||
if (variant === "s1")
|
|
||||||
return (
|
|
||||||
<h4 className={textClass} style={style}>
|
|
||||||
{children}
|
|
||||||
</h4>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<p className={textClass} style={style}>
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text.defaultProps = {
|
Text.defaultProps = {
|
||||||
className: null,
|
className: null,
|
||||||
style: null,
|
style: null,
|
||||||
variant: "b1",
|
variant: 'b1',
|
||||||
weight: "normal",
|
weight: 'normal',
|
||||||
primary: false,
|
primary: false,
|
||||||
span: false,
|
span: false,
|
||||||
};
|
};
|
||||||
|
@ -53,8 +32,8 @@ Text.defaultProps = {
|
||||||
Text.propTypes = {
|
Text.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
style: PropTypes.shape({}),
|
style: PropTypes.shape({}),
|
||||||
variant: PropTypes.oneOf(["h1", "h2", "s1", "b1", "b2", "b3"]),
|
variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']),
|
||||||
weight: PropTypes.oneOf(["light", "normal", "medium", "bold"]),
|
weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']),
|
||||||
primary: PropTypes.bool,
|
primary: PropTypes.bool,
|
||||||
span: PropTypes.bool,
|
span: PropTypes.bool,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
& img.emoji,
|
& img.emoji,
|
||||||
& img[data-mx-emoticon] {
|
& img[data-mx-emoticon] {
|
||||||
height: calc(var(--lh-#{$type}) - 0.25rem);
|
height: calc(var(--lh-#{$type}) - .25rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
margin-right: 2px !important;
|
margin-right: 2px !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.1rem;
|
top: -.1rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
44
src/app/atoms/time/Time.jsx
Normal file
44
src/app/atoms/time/Time.jsx
Normal file
|
@ -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
|
||||||
|
dateTime={date.toISOString()}
|
||||||
|
title={formattedFullTime}
|
||||||
|
>
|
||||||
|
{formattedDate}
|
||||||
|
</time>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Time.defaultProps = {
|
||||||
|
fullTime: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Time.propTypes = {
|
||||||
|
timestamp: PropTypes.number.isRequired,
|
||||||
|
fullTime: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Time;
|
|
@ -1,46 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import dateFormat from "dateformat";
|
|
||||||
import { isInSameDay } from "../../../util/common";
|
|
||||||
|
|
||||||
export interface TimeProps {
|
|
||||||
timestamp: number;
|
|
||||||
fullTime?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Time({ timestamp, fullTime }: TimeProps) {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
|
|
||||||
const formattedFullTime = dateFormat(date, "dd mmmm yyyy, hh:MM TT");
|
|
||||||
let formattedDateTime = formattedFullTime;
|
|
||||||
|
|
||||||
if (!fullTime) {
|
|
||||||
const compareDate = new Date();
|
|
||||||
const isToday = isInSameDay(date, compareDate);
|
|
||||||
compareDate.setDate(compareDate.getDate() - 1);
|
|
||||||
const isYesterday = isInSameDay(date, compareDate);
|
|
||||||
|
|
||||||
const dtf = new Intl.DateTimeFormat();
|
|
||||||
const formattedDate = dtf.format(date);
|
|
||||||
|
|
||||||
formattedDateTime = dateFormat(
|
|
||||||
date,
|
|
||||||
isToday || isYesterday ? "hh:MM TT" : formattedDate
|
|
||||||
);
|
|
||||||
if (isYesterday) {
|
|
||||||
formattedDateTime = `Yesterday, ${formattedDateTime}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<time dateTime={date.toISOString()} title={formattedFullTime}>
|
|
||||||
{formattedDateTime}
|
|
||||||
</time>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Time.defaultProps = {
|
|
||||||
fullTime: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Time;
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Tooltip.scss";
|
import './Tooltip.scss';
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from '@tippyjs/react';
|
||||||
|
|
||||||
function Tooltip({ className, placement, content, delay, children }) {
|
function Tooltip({
|
||||||
|
className, placement, content, delay, children,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Tippy
|
<Tippy
|
||||||
content={content}
|
content={content}
|
||||||
|
@ -21,8 +23,8 @@ function Tooltip({ className, placement, content, delay, children }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip.defaultProps = {
|
Tooltip.defaultProps = {
|
||||||
placement: "top",
|
placement: 'top',
|
||||||
className: "",
|
className: '',
|
||||||
delay: [200, 0],
|
delay: [200, 0],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import initMatrix from "../../client/initMatrix";
|
import initMatrix from '../../client/initMatrix';
|
||||||
|
|
||||||
export function useAccountData(eventType) {
|
export function useAccountData(eventType) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
@ -12,9 +12,9 @@ export function useAccountData(eventType) {
|
||||||
if (mEvent.getType() !== eventType) return;
|
if (mEvent.getType() !== eventType) return;
|
||||||
setEvent(mEvent);
|
setEvent(mEvent);
|
||||||
};
|
};
|
||||||
mx.on("accountData", handleChange);
|
mx.on('accountData', handleChange);
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener("accountData", handleChange);
|
mx.removeListener('accountData', handleChange);
|
||||||
};
|
};
|
||||||
}, [eventType]);
|
}, [eventType]);
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,22 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import initMatrix from "../../client/initMatrix";
|
import initMatrix from '../../client/initMatrix';
|
||||||
import cons from "../../client/state/cons";
|
import cons from '../../client/state/cons';
|
||||||
|
|
||||||
export function useCategorizedSpaces() {
|
export function useCategorizedSpaces() {
|
||||||
const { accountData } = initMatrix;
|
const { accountData } = initMatrix;
|
||||||
const [categorizedSpaces, setCategorizedSpaces] = useState([
|
const [categorizedSpaces, setCategorizedSpaces] = useState([...accountData.categorizedSpaces]);
|
||||||
...accountData.categorizedSpaces,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCategorizedSpaces = () => {
|
const handleCategorizedSpaces = () => {
|
||||||
setCategorizedSpaces([...accountData.categorizedSpaces]);
|
setCategorizedSpaces([...accountData.categorizedSpaces]);
|
||||||
};
|
};
|
||||||
accountData.on(
|
accountData.on(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, handleCategorizedSpaces);
|
||||||
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
|
|
||||||
handleCategorizedSpaces
|
|
||||||
);
|
|
||||||
return () => {
|
return () => {
|
||||||
accountData.removeListener(
|
accountData.removeListener(
|
||||||
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
|
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
|
||||||
handleCategorizedSpaces
|
handleCategorizedSpaces,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import initMatrix from "../../client/initMatrix";
|
import initMatrix from '../../client/initMatrix';
|
||||||
import { hasCrossSigningAccountData } from "../../util/matrixUtil";
|
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
|
||||||
|
|
||||||
export function useCrossSigningStatus() {
|
export function useCrossSigningStatus() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
@ -11,14 +11,14 @@ export function useCrossSigningStatus() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCSEnabled) return null;
|
if (isCSEnabled) return null;
|
||||||
const handleAccountData = (event) => {
|
const handleAccountData = (event) => {
|
||||||
if (event.getType() === "m.cross_signing.master") {
|
if (event.getType() === 'm.cross_signing.master') {
|
||||||
setIsCSEnabled(true);
|
setIsCSEnabled(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mx.on("accountData", handleAccountData);
|
mx.on('accountData', handleAccountData);
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener("accountData", handleAccountData);
|
mx.removeListener('accountData', handleAccountData);
|
||||||
};
|
};
|
||||||
}, [isCSEnabled === false]);
|
}, [isCSEnabled === false]);
|
||||||
return isCSEnabled;
|
return isCSEnabled;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import initMatrix from "../../client/initMatrix";
|
import initMatrix from '../../client/initMatrix';
|
||||||
|
|
||||||
export function useDeviceList() {
|
export function useDeviceList() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
@ -10,8 +10,7 @@ export function useDeviceList() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
const updateDevices = () =>
|
const updateDevices = () => mx.getDevices().then((data) => {
|
||||||
mx.getDevices().then((data) => {
|
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setDeviceList(data.devices || []);
|
setDeviceList(data.devices || []);
|
||||||
});
|
});
|
||||||
|
@ -23,9 +22,9 @@ export function useDeviceList() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mx.on("crypto.devicesUpdated", handleDevicesUpdate);
|
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener("crypto.devicesUpdated", handleDevicesUpdate);
|
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function useForceUpdate() {
|
export function useForceUpdate() {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
return [
|
return [data, function forceUpdateHook() {
|
||||||
data,
|
|
||||||
function forceUpdateHook() {
|
|
||||||
setData({});
|
setData({});
|
||||||
},
|
}];
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function usePermission(name, initial) {
|
export function usePermission(name, initial) {
|
||||||
const [state, setState] = useState(initial);
|
const [state, setState] = useState(initial);
|
||||||
|
@ -15,12 +15,12 @@ export function usePermission(name, initial) {
|
||||||
descriptor = _descriptor;
|
descriptor = _descriptor;
|
||||||
|
|
||||||
update();
|
update();
|
||||||
descriptor.addEventListener("change", update);
|
descriptor.addEventListener('change', update);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (descriptor) descriptor.removeEventListener("change", update);
|
if (descriptor) descriptor.removeEventListener('change', update);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import cons from "../../client/state/cons";
|
import cons from '../../client/state/cons';
|
||||||
import navigation from "../../client/state/navigation";
|
import navigation from '../../client/state/navigation';
|
||||||
|
|
||||||
export function useSelectedSpace() {
|
export function useSelectedSpace() {
|
||||||
const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId);
|
const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId);
|
||||||
|
@ -13,10 +13,7 @@ export function useSelectedSpace() {
|
||||||
};
|
};
|
||||||
navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
|
navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(
|
navigation.removeListener(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
|
||||||
cons.events.navigation.SPACE_SELECTED,
|
|
||||||
onSpaceSelected
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import cons from "../../client/state/cons";
|
import cons from '../../client/state/cons';
|
||||||
import navigation from "../../client/state/navigation";
|
import navigation from '../../client/state/navigation';
|
||||||
|
|
||||||
export function useSelectedTab() {
|
export function useSelectedTab() {
|
||||||
const [selectedTab, setSelectedTab] = useState(navigation.selectedTab);
|
const [selectedTab, setSelectedTab] = useState(navigation.selectedTab);
|
||||||
|
@ -13,10 +13,7 @@ export function useSelectedTab() {
|
||||||
};
|
};
|
||||||
navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
|
navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(
|
navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected);
|
||||||
cons.events.navigation.TAB_SELECTED,
|
|
||||||
onTabSelected
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,22 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import initMatrix from "../../client/initMatrix";
|
import initMatrix from '../../client/initMatrix';
|
||||||
import cons from "../../client/state/cons";
|
import cons from '../../client/state/cons';
|
||||||
|
|
||||||
export function useSpaceShortcut() {
|
export function useSpaceShortcut() {
|
||||||
const { accountData } = initMatrix;
|
const { accountData } = initMatrix;
|
||||||
const [spaceShortcut, setSpaceShortcut] = useState([
|
const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
|
||||||
...accountData.spaceShortcut,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onSpaceShortcutUpdated = () => {
|
const onSpaceShortcutUpdated = () => {
|
||||||
setSpaceShortcut([...accountData.spaceShortcut]);
|
setSpaceShortcut([...accountData.spaceShortcut]);
|
||||||
};
|
};
|
||||||
accountData.on(
|
accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
|
||||||
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
|
|
||||||
onSpaceShortcutUpdated
|
|
||||||
);
|
|
||||||
return () => {
|
return () => {
|
||||||
accountData.removeListener(
|
accountData.removeListener(
|
||||||
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
|
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
|
||||||
onSpaceShortcutUpdated
|
onSpaceShortcutUpdated,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export function useStore(...args) {
|
export function useStore(...args) {
|
||||||
const itemRef = useRef(null);
|
const itemRef = useRef(null);
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ConfirmDialog.scss";
|
import './ConfirmDialog.scss';
|
||||||
|
|
||||||
import { openReusableDialog } from "../../../client/action/navigation";
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
|
|
||||||
function ConfirmDialog({ desc, actionTitle, actionType, onComplete }) {
|
function ConfirmDialog({
|
||||||
|
desc, actionTitle, actionType, onComplete,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="confirm-dialog">
|
<div className="confirm-dialog">
|
||||||
<Text>{desc}</Text>
|
<Text>{desc}</Text>
|
||||||
<div className="confirm-dialog__btn">
|
<div className="confirm-dialog__btn">
|
||||||
<Button variant={actionType} onClick={() => onComplete(true)}>
|
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
|
||||||
{actionTitle}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => onComplete(false)}>Cancel</Button>
|
<Button onClick={() => onComplete(false)}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,8 +23,7 @@ function ConfirmDialog({ desc, actionTitle, actionType, onComplete }) {
|
||||||
ConfirmDialog.propTypes = {
|
ConfirmDialog.propTypes = {
|
||||||
desc: PropTypes.string.isRequired,
|
desc: PropTypes.string.isRequired,
|
||||||
actionTitle: PropTypes.string.isRequired,
|
actionTitle: PropTypes.string.isRequired,
|
||||||
actionType: PropTypes.oneOf(["primary", "positive", "danger", "caution"])
|
actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired,
|
||||||
.isRequired,
|
|
||||||
onComplete: PropTypes.func.isRequired,
|
onComplete: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,18 +35,10 @@ ConfirmDialog.propTypes = {
|
||||||
* @return {Promise<boolean>} does it get's confirmed or not
|
* @return {Promise<boolean>} does it get's confirmed or not
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const confirmDialog = (
|
export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => {
|
||||||
title,
|
|
||||||
desc,
|
|
||||||
actionTitle,
|
|
||||||
actionType = "primary"
|
|
||||||
) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
let isCompleted = false;
|
let isCompleted = false;
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">
|
<Text variant="s1" weight="medium">{title}</Text>,
|
||||||
{title}
|
|
||||||
</Text>,
|
|
||||||
(requestClose) => (
|
(requestClose) => (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
desc={desc}
|
desc={desc}
|
||||||
|
@ -62,6 +53,6 @@ export const confirmDialog = (
|
||||||
),
|
),
|
||||||
() => {
|
() => {
|
||||||
if (!isCompleted) resolve(false);
|
if (!isCompleted) resolve(false);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,29 +1,22 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Dialog.scss";
|
import './Dialog.scss';
|
||||||
|
|
||||||
import { twemojify } from "../../../util/twemojify";
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Header, { TitleWrapper } from "../../atoms/header/Header";
|
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||||
import ScrollView from "../../atoms/scroll/ScrollView";
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import RawModal from "../../atoms/modal/RawModal";
|
import RawModal from '../../atoms/modal/RawModal';
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
className,
|
className, isOpen, title, onAfterOpen, onAfterClose,
|
||||||
isOpen,
|
contentOptions, onRequestClose, closeFromOutside, children,
|
||||||
title,
|
|
||||||
onAfterOpen,
|
|
||||||
onAfterClose,
|
|
||||||
contentOptions,
|
|
||||||
onRequestClose,
|
|
||||||
closeFromOutside,
|
|
||||||
children,
|
|
||||||
invisibleScroll,
|
invisibleScroll,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RawModal
|
<RawModal
|
||||||
className={`${className === null ? "" : `${className} `}dialog-modal`}
|
className={`${className === null ? '' : `${className} `}dialog-modal`}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onAfterOpen={onAfterOpen}
|
onAfterOpen={onAfterOpen}
|
||||||
onAfterClose={onAfterClose}
|
onAfterClose={onAfterClose}
|
||||||
|
@ -35,19 +28,19 @@ function Dialog({
|
||||||
<div className="dialog__content">
|
<div className="dialog__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
{typeof title === "string" ? (
|
{
|
||||||
<Text variant="h2" weight="medium" primary>
|
typeof title === 'string'
|
||||||
{twemojify(title)}
|
? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
|
||||||
</Text>
|
: title
|
||||||
) : (
|
}
|
||||||
title
|
|
||||||
)}
|
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="dialog__content__wrapper">
|
<div className="dialog__content__wrapper">
|
||||||
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
||||||
<div className="dialog__content-container">{children}</div>
|
<div className="dialog__content-container">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import cons from "../../../client/state/cons";
|
import cons from '../../../client/state/cons';
|
||||||
|
|
||||||
import navigation from "../../../client/state/navigation";
|
import navigation from '../../../client/state/navigation';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Dialog from "./Dialog";
|
import Dialog from './Dialog';
|
||||||
|
|
||||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
function ReusableDialog() {
|
function ReusableDialog() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
@ -19,10 +19,7 @@ function ReusableDialog() {
|
||||||
};
|
};
|
||||||
navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
|
navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(
|
navigation.removeListener(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
|
||||||
cons.events.navigation.REUSABLE_DIALOG_OPENED,
|
|
||||||
handleOpen
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -38,16 +35,10 @@ function ReusableDialog() {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={data?.title || ""}
|
title={data?.title || ''}
|
||||||
onAfterClose={handleAfterClose}
|
onAfterClose={handleAfterClose}
|
||||||
onRequestClose={handleRequestClose}
|
onRequestClose={handleRequestClose}
|
||||||
contentOptions={
|
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />}
|
||||||
<IconButton
|
|
||||||
src={CrossIC}
|
|
||||||
onClick={handleRequestClose}
|
|
||||||
tooltip="Close"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
invisibleScroll
|
invisibleScroll
|
||||||
>
|
>
|
||||||
{data?.render(handleRequestClose) || <div />}
|
{data?.render(handleRequestClose) || <div />}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./FollowingMembers.scss";
|
import './FollowingMembers.scss';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from "../../../client/state/cons";
|
import cons from '../../../client/state/cons';
|
||||||
import { openReadReceipts } from "../../../client/action/navigation";
|
import { openReadReceipts } from '../../../client/action/navigation';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import RawIcon from "../../atoms/system-icons/RawIcon";
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
import TickMarkIC from "../../../../public/res/ic/outlined/tick-mark.svg";
|
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||||
|
|
||||||
import { getUsersActionJsx } from "../../organisms/room/common";
|
import { getUsersActionJsx } from '../../organisms/room/common';
|
||||||
|
|
||||||
function FollowingMembers({ roomTimeline }) {
|
function FollowingMembers({ roomTimeline }) {
|
||||||
const [followingMembers, setFollowingMembers] = useState([]);
|
const [followingMembers, setFollowingMembers] = useState([]);
|
||||||
|
@ -27,38 +27,28 @@ function FollowingMembers({ roomTimeline }) {
|
||||||
setFollowingMembers(roomTimeline.getLiveReaders());
|
setFollowingMembers(roomTimeline.getLiveReaders());
|
||||||
};
|
};
|
||||||
updateFollowingMembers();
|
updateFollowingMembers();
|
||||||
roomTimeline.on(
|
roomTimeline.on(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
||||||
cons.events.roomTimeline.LIVE_RECEIPT,
|
|
||||||
updateFollowingMembers
|
|
||||||
);
|
|
||||||
roomsInput.on(cons.events.roomsInput.MESSAGE_SENT, handleOnMessageSent);
|
roomsInput.on(cons.events.roomsInput.MESSAGE_SENT, handleOnMessageSent);
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.removeListener(
|
roomTimeline.removeListener(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
||||||
cons.events.roomTimeline.LIVE_RECEIPT,
|
roomsInput.removeListener(cons.events.roomsInput.MESSAGE_SENT, handleOnMessageSent);
|
||||||
updateFollowingMembers
|
|
||||||
);
|
|
||||||
roomsInput.removeListener(
|
|
||||||
cons.events.roomsInput.MESSAGE_SENT,
|
|
||||||
handleOnMessageSent
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const filteredM = followingMembers.filter((userId) => userId !== myUserId);
|
const filteredM = followingMembers.filter((userId) => userId !== myUserId);
|
||||||
|
|
||||||
return (
|
return filteredM.length !== 0 && (
|
||||||
filteredM.length !== 0 && (
|
|
||||||
<button
|
<button
|
||||||
className="following-members"
|
className="following-members"
|
||||||
onClick={() => openReadReceipts(roomId, followingMembers)}
|
onClick={() => openReadReceipts(roomId, followingMembers)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<RawIcon size="extra-small" src={TickMarkIC} />
|
<RawIcon
|
||||||
<Text variant="b2">
|
size="extra-small"
|
||||||
{getUsersActionJsx(roomId, filteredM, "following the conversation.")}
|
src={TickMarkIC}
|
||||||
</Text>
|
/>
|
||||||
|
<Text variant="b2">{getUsersActionJsx(roomId, filteredM, 'following the conversation.')}</Text>
|
||||||
</button>
|
</button>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
|
|
||||||
.following-members {
|
.following-members {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,61 +1,59 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords } from "../../../util/common";
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
import SettingTile from "../setting-tile/SettingTile";
|
import SettingTile from '../setting-tile/SettingTile';
|
||||||
|
|
||||||
import NotificationSelector from "./NotificationSelector";
|
import NotificationSelector from './NotificationSelector';
|
||||||
|
|
||||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
|
|
||||||
import { useAccountData } from "../../hooks/useAccountData";
|
import { useAccountData } from '../../hooks/useAccountData';
|
||||||
|
|
||||||
export const notifType = {
|
export const notifType = {
|
||||||
ON: "on",
|
ON: 'on',
|
||||||
OFF: "off",
|
OFF: 'off',
|
||||||
NOISY: "noisy",
|
NOISY: 'noisy',
|
||||||
};
|
};
|
||||||
export const typeToLabel = {
|
export const typeToLabel = {
|
||||||
[notifType.ON]: "On",
|
[notifType.ON]: 'On',
|
||||||
[notifType.OFF]: "Off",
|
[notifType.OFF]: 'Off',
|
||||||
[notifType.NOISY]: "Noisy",
|
[notifType.NOISY]: 'Noisy',
|
||||||
};
|
};
|
||||||
Object.freeze(notifType);
|
Object.freeze(notifType);
|
||||||
|
|
||||||
const DM = ".m.rule.room_one_to_one";
|
const DM = '.m.rule.room_one_to_one';
|
||||||
const ENC_DM = ".m.rule.encrypted_room_one_to_one";
|
const ENC_DM = '.m.rule.encrypted_room_one_to_one';
|
||||||
const ROOM = ".m.rule.message";
|
const ROOM = '.m.rule.message';
|
||||||
const ENC_ROOM = ".m.rule.encrypted";
|
const ENC_ROOM = '.m.rule.encrypted';
|
||||||
|
|
||||||
export function getActionType(rule) {
|
export function getActionType(rule) {
|
||||||
const { actions } = rule;
|
const { actions } = rule;
|
||||||
if (actions.find((action) => action?.set_tweak === "sound"))
|
if (actions.find((action) => action?.set_tweak === 'sound')) return notifType.NOISY;
|
||||||
return notifType.NOISY;
|
if (actions.find((action) => action?.set_tweak === 'highlight')) return notifType.ON;
|
||||||
if (actions.find((action) => action?.set_tweak === "highlight"))
|
if (actions.find((action) => action === 'dont_notify')) return notifType.OFF;
|
||||||
return notifType.ON;
|
|
||||||
if (actions.find((action) => action === "dont_notify")) return notifType.OFF;
|
|
||||||
return notifType.OFF;
|
return notifType.OFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTypeActions(type, highlightValue = false) {
|
export function getTypeActions(type, highlightValue = false) {
|
||||||
if (type === notifType.OFF) return ["dont_notify"];
|
if (type === notifType.OFF) return ['dont_notify'];
|
||||||
|
|
||||||
const highlight = { set_tweak: "highlight" };
|
const highlight = { set_tweak: 'highlight' };
|
||||||
if (typeof highlightValue === "boolean") highlight.value = highlightValue;
|
if (typeof highlightValue === 'boolean') highlight.value = highlightValue;
|
||||||
if (type === notifType.ON) return ["notify", highlight];
|
if (type === notifType.ON) return ['notify', highlight];
|
||||||
|
|
||||||
const sound = { set_tweak: "sound", value: "default" };
|
const sound = { set_tweak: 'sound', value: 'default' };
|
||||||
return ["notify", sound, highlight];
|
return ['notify', sound, highlight];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGlobalNotif() {
|
function useGlobalNotif() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const pushRules = useAccountData("m.push_rules")?.getContent();
|
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||||
const underride = pushRules?.global?.underride ?? [];
|
const underride = pushRules?.global?.underride ?? [];
|
||||||
const rulesToType = {
|
const rulesToType = {
|
||||||
[DM]: notifType.ON,
|
[DM]: notifType.ON,
|
||||||
|
@ -67,14 +65,12 @@ function useGlobalNotif() {
|
||||||
const getRuleCondition = (rule) => {
|
const getRuleCondition = (rule) => {
|
||||||
const condition = [];
|
const condition = [];
|
||||||
if (rule === DM || rule === ENC_DM) {
|
if (rule === DM || rule === ENC_DM) {
|
||||||
condition.push({ kind: "room_member_count", is: "2" });
|
condition.push({ kind: 'room_member_count', is: '2' });
|
||||||
}
|
}
|
||||||
condition.push({
|
condition.push({
|
||||||
kind: "event_match",
|
kind: 'event_match',
|
||||||
key: "type",
|
key: 'type',
|
||||||
pattern: [ENC_DM, ENC_ROOM].includes(rule)
|
pattern: [ENC_DM, ENC_ROOM].includes(rule) ? 'm.room.encrypted' : 'm.room.message',
|
||||||
? "m.room.encrypted"
|
|
||||||
: "m.room.message",
|
|
||||||
});
|
});
|
||||||
return condition;
|
return condition;
|
||||||
};
|
};
|
||||||
|
@ -97,7 +93,7 @@ function useGlobalNotif() {
|
||||||
}
|
}
|
||||||
ruleContent.actions = getTypeActions(type);
|
ruleContent.actions = getTypeActions(type);
|
||||||
|
|
||||||
mx.setAccountData("m.push_rules", content);
|
mx.setAccountData('m.push_rules', content);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dmRule = underride.find((rule) => rule.rule_id === DM);
|
const dmRule = underride.find((rule) => rule.rule_id === DM);
|
||||||
|
@ -118,8 +114,8 @@ function GlobalNotification() {
|
||||||
|
|
||||||
const onSelect = (evt, rule) => {
|
const onSelect = (evt, rule) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu(
|
||||||
"bottom",
|
'bottom',
|
||||||
getEventCords(evt, ".btn-surface"),
|
getEventCords(evt, '.btn-surface'),
|
||||||
(requestClose) => (
|
(requestClose) => (
|
||||||
<NotificationSelector
|
<NotificationSelector
|
||||||
value={rulesToType[rule]}
|
value={rulesToType[rule]}
|
||||||
|
@ -128,7 +124,7 @@ function GlobalNotification() {
|
||||||
requestClose();
|
requestClose();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -137,67 +133,39 @@ function GlobalNotification() {
|
||||||
<MenuHeader>Global Notifications</MenuHeader>
|
<MenuHeader>Global Notifications</MenuHeader>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Direct messages"
|
title="Direct messages"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, DM)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, DM)}
|
{ typeToLabel[rulesToType[DM]] }
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[DM]]}
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all direct message.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all direct message.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Encrypted direct messages"
|
title="Encrypted direct messages"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, ENC_DM)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, ENC_DM)}
|
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[ENC_DM]]}
|
{typeToLabel[rulesToType[ENC_DM]]}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all encrypted direct message.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all encrypted direct message.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Rooms messages"
|
title="Rooms messages"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, ROOM)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, ROOM)}
|
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[ROOM]]}
|
{typeToLabel[rulesToType[ROOM]]}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all room message.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all room message.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Encrypted rooms messages"
|
title="Encrypted rooms messages"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, ENC_ROOM)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, ENC_ROOM)}
|
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[ENC_ROOM]]}
|
{typeToLabel[rulesToType[ENC_ROOM]]}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all encrypted room message.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all encrypted room message.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import "./IgnoreUserList.scss";
|
import './IgnoreUserList.scss';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import * as roomActions from "../../../client/action/room";
|
import * as roomActions from '../../../client/action/room';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Chip from "../../atoms/chip/Chip";
|
import Chip from '../../atoms/chip/Chip';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
import SettingTile from "../setting-tile/SettingTile";
|
import SettingTile from '../setting-tile/SettingTile';
|
||||||
|
|
||||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
import { useAccountData } from "../../hooks/useAccountData";
|
import { useAccountData } from '../../hooks/useAccountData';
|
||||||
|
|
||||||
function IgnoreUserList() {
|
function IgnoreUserList() {
|
||||||
useAccountData("m.ignored_user_list");
|
useAccountData('m.ignored_user_list');
|
||||||
const ignoredUsers = initMatrix.matrixClient.getIgnoredUsers();
|
const ignoredUsers = initMatrix.matrixClient.getIgnoredUsers();
|
||||||
|
|
||||||
const handleSubmit = (evt) => {
|
const handleSubmit = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { ignoreInput } = evt.target.elements;
|
const { ignoreInput } = evt.target.elements;
|
||||||
const value = ignoreInput.value.trim();
|
const value = ignoreInput.value.trim();
|
||||||
const userIds = value.split(" ").filter((v) => v.match(/^@\S+:\S+$/));
|
const userIds = value.split(' ').filter((v) => v.match(/^@\S+:\S+$/));
|
||||||
if (userIds.length === 0) return;
|
if (userIds.length === 0) return;
|
||||||
ignoreInput.value = "";
|
ignoreInput.value = '';
|
||||||
roomActions.ignore(userIds);
|
roomActions.ignore(userIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,17 +34,12 @@ function IgnoreUserList() {
|
||||||
<MenuHeader>Ignored users</MenuHeader>
|
<MenuHeader>Ignored users</MenuHeader>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Ignore user"
|
title="Ignore user"
|
||||||
content={
|
content={(
|
||||||
<div className="ignore-user-list__users">
|
<div className="ignore-user-list__users">
|
||||||
<Text variant="b3">
|
<Text variant="b3">Ignore userId if you do not want to receive their messages or invites.</Text>
|
||||||
Ignore userId if you do not want to receive their messages or
|
|
||||||
invites.
|
|
||||||
</Text>
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Input name="ignoreInput" required />
|
<Input name="ignoreInput" required />
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">Ignore</Button>
|
||||||
Ignore
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
{ignoredUsers.length > 0 && (
|
{ignoredUsers.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -60,7 +55,7 @@ function IgnoreUserList() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,38 +1,35 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import "./KeywordNotification.scss";
|
import './KeywordNotification.scss';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords } from "../../../util/common";
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Chip from "../../atoms/chip/Chip";
|
import Chip from '../../atoms/chip/Chip';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
import SettingTile from "../setting-tile/SettingTile";
|
import SettingTile from '../setting-tile/SettingTile';
|
||||||
|
|
||||||
import NotificationSelector from "./NotificationSelector";
|
import NotificationSelector from './NotificationSelector';
|
||||||
|
|
||||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
import { useAccountData } from "../../hooks/useAccountData";
|
import { useAccountData } from '../../hooks/useAccountData';
|
||||||
import {
|
import {
|
||||||
notifType,
|
notifType, typeToLabel, getActionType, getTypeActions,
|
||||||
typeToLabel,
|
} from './GlobalNotification';
|
||||||
getActionType,
|
|
||||||
getTypeActions,
|
|
||||||
} from "./GlobalNotification";
|
|
||||||
|
|
||||||
const DISPLAY_NAME = ".m.rule.contains_display_name";
|
const DISPLAY_NAME = '.m.rule.contains_display_name';
|
||||||
const ROOM_PING = ".m.rule.roomnotif";
|
const ROOM_PING = '.m.rule.roomnotif';
|
||||||
const USERNAME = ".m.rule.contains_user_name";
|
const USERNAME = '.m.rule.contains_user_name';
|
||||||
const KEYWORD = "keyword";
|
const KEYWORD = 'keyword';
|
||||||
|
|
||||||
function useKeywordNotif() {
|
function useKeywordNotif() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const pushRules = useAccountData("m.push_rules")?.getContent();
|
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||||
const override = pushRules?.global?.override ?? [];
|
const override = pushRules?.global?.override ?? [];
|
||||||
const content = pushRules?.global?.content ?? [];
|
const content = pushRules?.global?.content ?? [];
|
||||||
|
|
||||||
|
@ -63,12 +60,12 @@ function useKeywordNotif() {
|
||||||
or.push(orRule);
|
or.push(orRule);
|
||||||
}
|
}
|
||||||
if (rule === DISPLAY_NAME) {
|
if (rule === DISPLAY_NAME) {
|
||||||
orRule.conditions = [{ kind: "contains_display_name" }];
|
orRule.conditions = [{ kind: 'contains_display_name' }];
|
||||||
orRule.actions = getTypeActions(type, true);
|
orRule.actions = getTypeActions(type, true);
|
||||||
} else {
|
} else {
|
||||||
orRule.conditions = [
|
orRule.conditions = [
|
||||||
{ kind: "event_match", key: "content.body", pattern: "@room" },
|
{ kind: 'event_match', key: 'content.body', pattern: '@room' },
|
||||||
{ kind: "sender_notification_permission", key: "room" },
|
{ kind: 'sender_notification_permission', key: 'room' },
|
||||||
];
|
];
|
||||||
orRule.actions = getTypeActions(type, true);
|
orRule.actions = getTypeActions(type, true);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +92,7 @@ function useKeywordNotif() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mx.setAccountData("m.push_rules", evtContent);
|
mx.setAccountData('m.push_rules', evtContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addKeyword = (keyword) => {
|
const addKeyword = (keyword) => {
|
||||||
|
@ -107,13 +104,11 @@ function useKeywordNotif() {
|
||||||
default: false,
|
default: false,
|
||||||
actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
|
actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
|
||||||
});
|
});
|
||||||
mx.setAccountData("m.push_rules", pushRules);
|
mx.setAccountData('m.push_rules', pushRules);
|
||||||
};
|
};
|
||||||
const removeKeyword = (rule) => {
|
const removeKeyword = (rule) => {
|
||||||
pushRules.global.content = content.filter(
|
pushRules.global.content = content.filter((r) => r.rule_id !== rule.rule_id);
|
||||||
(r) => r.rule_id !== rule.rule_id
|
mx.setAccountData('m.push_rules', pushRules);
|
||||||
);
|
|
||||||
mx.setAccountData("m.push_rules", pushRules);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
|
const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
|
||||||
|
@ -136,16 +131,20 @@ function useKeywordNotif() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function GlobalNotification() {
|
function GlobalNotification() {
|
||||||
const { rulesToType, pushRules, setRule, addKeyword, removeKeyword } =
|
const {
|
||||||
useKeywordNotif();
|
rulesToType,
|
||||||
|
pushRules,
|
||||||
|
setRule,
|
||||||
|
addKeyword,
|
||||||
|
removeKeyword,
|
||||||
|
} = useKeywordNotif();
|
||||||
|
|
||||||
const keywordRules =
|
const keywordRules = pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
|
||||||
pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
|
|
||||||
|
|
||||||
const onSelect = (evt, rule) => {
|
const onSelect = (evt, rule) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu(
|
||||||
"bottom",
|
'bottom',
|
||||||
getEventCords(evt, ".btn-surface"),
|
getEventCords(evt, '.btn-surface'),
|
||||||
(requestClose) => (
|
(requestClose) => (
|
||||||
<NotificationSelector
|
<NotificationSelector
|
||||||
value={rulesToType[rule]}
|
value={rulesToType[rule]}
|
||||||
|
@ -154,7 +153,7 @@ function GlobalNotification() {
|
||||||
requestClose();
|
requestClose();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -162,9 +161,9 @@ function GlobalNotification() {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { keywordInput } = evt.target.elements;
|
const { keywordInput } = evt.target.elements;
|
||||||
const value = keywordInput.value.trim();
|
const value = keywordInput.value.trim();
|
||||||
if (value === "") return;
|
if (value === '') return;
|
||||||
addKeyword(value);
|
addKeyword(value);
|
||||||
keywordInput.value = "";
|
keywordInput.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -172,84 +171,50 @@ function GlobalNotification() {
|
||||||
<MenuHeader>Mentions & keywords</MenuHeader>
|
<MenuHeader>Mentions & keywords</MenuHeader>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Message containing my display name"
|
title="Message containing my display name"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, DISPLAY_NAME)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, DISPLAY_NAME)}
|
{ typeToLabel[rulesToType[DISPLAY_NAME]] }
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[DISPLAY_NAME]]}
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all message containing your display name.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all message containing your
|
|
||||||
display name.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Message containing my username"
|
title="Message containing my username"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, USERNAME)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, USERNAME)}
|
{ typeToLabel[rulesToType[USERNAME]] }
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[USERNAME]]}
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all message containing your username.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all message containing your
|
|
||||||
username.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Message containing @room"
|
title="Message containing @room"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, ROOM_PING)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, ROOM_PING)}
|
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[ROOM_PING]]}
|
{typeToLabel[rulesToType[ROOM_PING]]}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all messages containing @room.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all messages containing @room.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{rulesToType[KEYWORD] && (
|
{ rulesToType[KEYWORD] && (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Message containing keywords"
|
title="Message containing keywords"
|
||||||
options={
|
options={(
|
||||||
<Button
|
<Button onClick={(evt) => onSelect(evt, KEYWORD)} iconSrc={ChevronBottomIC}>
|
||||||
onClick={(evt) => onSelect(evt, KEYWORD)}
|
|
||||||
iconSrc={ChevronBottomIC}
|
|
||||||
>
|
|
||||||
{typeToLabel[rulesToType[KEYWORD]]}
|
{typeToLabel[rulesToType[KEYWORD]]}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
content={
|
content={<Text variant="b3">Default notification settings for all message containing keywords.</Text>}
|
||||||
<Text variant="b3">
|
|
||||||
Default notification settings for all message containing keywords.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Keywords"
|
title="Keywords"
|
||||||
content={
|
content={(
|
||||||
<div className="keyword-notification__keyword">
|
<div className="keyword-notification__keyword">
|
||||||
<Text variant="b3">
|
<Text variant="b3">Get notification when a message contains keyword.</Text>
|
||||||
Get notification when a message contains keyword.
|
|
||||||
</Text>
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Input name="keywordInput" required />
|
<Input name="keywordInput" required />
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">Add</Button>
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
{keywordRules.length > 0 && (
|
{keywordRules.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -265,7 +230,7 @@ function GlobalNotification() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,41 +1,25 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { MenuHeader, MenuItem } from "../../atoms/context-menu/ContextMenu";
|
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
|
||||||
import CheckIC from "../../../../public/res/ic/outlined/check.svg";
|
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
||||||
|
|
||||||
function NotificationSelector({ value, onSelect }) {
|
function NotificationSelector({
|
||||||
|
value, onSelect,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<MenuHeader>Notification</MenuHeader>
|
<MenuHeader>Notification</MenuHeader>
|
||||||
<MenuItem
|
<MenuItem iconSrc={value === 'off' ? CheckIC : null} variant={value === 'off' ? 'positive' : 'surface'} onClick={() => onSelect('off')}>Off</MenuItem>
|
||||||
iconSrc={value === "off" ? CheckIC : null}
|
<MenuItem iconSrc={value === 'on' ? CheckIC : null} variant={value === 'on' ? 'positive' : 'surface'} onClick={() => onSelect('on')}>On</MenuItem>
|
||||||
variant={value === "off" ? "positive" : "surface"}
|
<MenuItem iconSrc={value === 'noisy' ? CheckIC : null} variant={value === 'noisy' ? 'positive' : 'surface'} onClick={() => onSelect('noisy')}>Noisy</MenuItem>
|
||||||
onClick={() => onSelect("off")}
|
|
||||||
>
|
|
||||||
Off
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={value === "on" ? CheckIC : null}
|
|
||||||
variant={value === "on" ? "positive" : "surface"}
|
|
||||||
onClick={() => onSelect("on")}
|
|
||||||
>
|
|
||||||
On
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={value === "noisy" ? CheckIC : null}
|
|
||||||
variant={value === "noisy" ? "positive" : "surface"}
|
|
||||||
onClick={() => onSelect("noisy")}
|
|
||||||
>
|
|
||||||
Noisy
|
|
||||||
</MenuItem>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationSelector.propTypes = {
|
NotificationSelector.propTypes = {
|
||||||
value: PropTypes.oneOf(["off", "on", "noisy"]).isRequired,
|
value: PropTypes.oneOf(['off', 'on', 'noisy']).isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ImageLightbox.scss";
|
import './ImageLightbox.scss';
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from 'file-saver';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import RawModal from "../../atoms/modal/RawModal";
|
import RawModal from '../../atoms/modal/RawModal';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
|
||||||
import DownloadSVG from "../../../../public/res/ic/outlined/download.svg";
|
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
||||||
import ExternalSVG from "../../../../public/res/ic/outlined/external.svg";
|
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
||||||
|
|
||||||
function ImageLightbox({ url, alt, isOpen, onRequestClose }) {
|
function ImageLightbox({
|
||||||
|
url, alt, isOpen, onRequestClose,
|
||||||
|
}) {
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
FileSaver.saveAs(url, alt);
|
FileSaver.saveAs(url, alt);
|
||||||
};
|
};
|
||||||
|
@ -24,14 +26,8 @@ function ImageLightbox({ url, alt, isOpen, onRequestClose }) {
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<div className="image-lightbox__header">
|
<div className="image-lightbox__header">
|
||||||
<Text variant="b2" weight="medium">
|
<Text variant="b2" weight="medium">{alt}</Text>
|
||||||
{alt}
|
<IconButton onClick={() => window.open(url)} size="small" src={ExternalSVG} />
|
||||||
</Text>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => window.open(url)}
|
|
||||||
size="small"
|
|
||||||
src={ExternalSVG}
|
|
||||||
/>
|
|
||||||
<IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
|
<IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
|
||||||
</div>
|
</div>
|
||||||
<div className="image-lightbox__content">
|
<div className="image-lightbox__content">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
|
|
||||||
.image-lightbox__modal {
|
.image-lightbox__modal {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
background-color: var(--bg-overlay-low);
|
background-color: var(--bg-overlay-low);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.image-lightbox__header > *,
|
.image-lightbox__header > *,
|
||||||
.image-lightbox__content > * {
|
.image-lightbox__content > * {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
|
@ -1,38 +1,37 @@
|
||||||
import React, { useState, useMemo, useReducer, useEffect } from "react";
|
import React, {
|
||||||
import PropTypes from "prop-types";
|
useState, useMemo, useReducer, useEffect,
|
||||||
import "./ImagePack.scss";
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './ImagePack.scss';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openReusableDialog } from "../../../client/action/navigation";
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
import { suffixRename } from "../../../util/common";
|
import { suffixRename } from '../../../util/common';
|
||||||
|
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import Checkbox from "../../atoms/button/Checkbox";
|
import Checkbox from '../../atoms/button/Checkbox';
|
||||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
|
||||||
import { ImagePack as ImagePackBuilder } from "../../organisms/emoji-board/custom-emoji";
|
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
|
||||||
import { confirmDialog } from "../confirm-dialog/ConfirmDialog";
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
import ImagePackProfile from "./ImagePackProfile";
|
import ImagePackProfile from './ImagePackProfile';
|
||||||
import ImagePackItem from "./ImagePackItem";
|
import ImagePackItem from './ImagePackItem';
|
||||||
import ImagePackUpload from "./ImagePackUpload";
|
import ImagePackUpload from './ImagePackUpload';
|
||||||
|
|
||||||
const renameImagePackItem = (shortcode) =>
|
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
|
||||||
new Promise((resolve) => {
|
|
||||||
let isCompleted = false;
|
let isCompleted = false;
|
||||||
|
|
||||||
openReusableDialog(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">
|
<Text variant="s1" weight="medium">Rename</Text>,
|
||||||
Rename
|
|
||||||
</Text>,
|
|
||||||
(requestClose) => (
|
(requestClose) => (
|
||||||
<div style={{ padding: "var(--sp-normal)" }}>
|
<div style={{ padding: 'var(--sp-normal)' }}>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const sc = e.target.shortcode.value;
|
const sc = e.target.shortcode.value;
|
||||||
if (sc.trim() === "") return;
|
if (sc.trim() === '') return;
|
||||||
isCompleted = true;
|
isCompleted = true;
|
||||||
resolve(sc.trim());
|
resolve(sc.trim());
|
||||||
requestClose();
|
requestClose();
|
||||||
|
@ -45,36 +44,32 @@ const renameImagePackItem = (shortcode) =>
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div style={{ height: "var(--sp-normal)" }} />
|
<div style={{ height: 'var(--sp-normal)' }} />
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">Rename</Button>
|
||||||
Rename
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
() => {
|
() => {
|
||||||
if (!isCompleted) resolve(null);
|
if (!isCompleted) resolve(null);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getUsage(usage) {
|
function getUsage(usage) {
|
||||||
if (usage.includes("emoticon") && usage.includes("sticker")) return "both";
|
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
||||||
if (usage.includes("emoticon")) return "emoticon";
|
if (usage.includes('emoticon')) return 'emoticon';
|
||||||
if (usage.includes("sticker")) return "sticker";
|
if (usage.includes('sticker')) return 'sticker';
|
||||||
|
|
||||||
return "both";
|
return 'both';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGlobalPack(roomId, stateKey) {
|
function isGlobalPack(roomId, stateKey) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const globalContent = mx
|
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||||
.getAccountData("im.ponies.emote_rooms")
|
if (typeof globalContent !== 'object') return false;
|
||||||
?.getContent();
|
|
||||||
if (typeof globalContent !== "object") return false;
|
|
||||||
|
|
||||||
const { rooms } = globalContent;
|
const { rooms } = globalContent;
|
||||||
if (typeof rooms !== "object") return false;
|
if (typeof rooms !== 'object') return false;
|
||||||
|
|
||||||
return rooms[roomId]?.[stateKey] !== undefined;
|
return rooms[roomId]?.[stateKey] !== undefined;
|
||||||
}
|
}
|
||||||
|
@ -83,17 +78,13 @@ function useRoomImagePack(roomId, stateKey) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
const packEvent = room.currentState.getStateEvents(
|
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||||
"im.ponies.room_emotes",
|
const pack = useMemo(() => (
|
||||||
stateKey
|
ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent())
|
||||||
);
|
), [room, stateKey]);
|
||||||
const pack = useMemo(
|
|
||||||
() => ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent()),
|
|
||||||
[room, stateKey]
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendPackContent = (content) => {
|
const sendPackContent = (content) => {
|
||||||
mx.sendStateEvent(roomId, "im.ponies.room_emotes", content, stateKey);
|
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -104,21 +95,16 @@ function useRoomImagePack(roomId, stateKey) {
|
||||||
|
|
||||||
function useUserImagePack() {
|
function useUserImagePack() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const packEvent = mx.getAccountData("im.ponies.user_emotes");
|
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
||||||
const pack = useMemo(
|
const pack = useMemo(() => (
|
||||||
() =>
|
ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? {
|
||||||
ImagePackBuilder.parsePack(
|
pack: { display_name: 'Personal' },
|
||||||
mx.getUserId(),
|
|
||||||
packEvent?.getContent() ?? {
|
|
||||||
pack: { display_name: "Personal" },
|
|
||||||
images: {},
|
images: {},
|
||||||
}
|
})
|
||||||
),
|
), []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendPackContent = (content) => {
|
const sendPackContent = (content) => {
|
||||||
mx.setAccountData("im.ponies.user_emotes", content);
|
mx.setAccountData('im.ponies.user_emotes', content);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -131,11 +117,12 @@ function useImagePackHandles(pack, sendPackContent) {
|
||||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||||
|
|
||||||
const getNewKey = (key) => {
|
const getNewKey = (key) => {
|
||||||
if (typeof key !== "string") return undefined;
|
if (typeof key !== 'string') return undefined;
|
||||||
let newKey = key?.replace(/\s/g, "_");
|
let newKey = key?.replace(/\s/g, '_');
|
||||||
if (pack.getImages().get(newKey)) {
|
if (pack.getImages().get(newKey)) {
|
||||||
newKey = suffixRename(newKey, (suffixedKey) =>
|
newKey = suffixRename(
|
||||||
pack.getImages().get(suffixedKey)
|
newKey,
|
||||||
|
(suffixedKey) => pack.getImages().get(suffixedKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return newKey;
|
return newKey;
|
||||||
|
@ -154,12 +141,10 @@ function useImagePackHandles(pack, sendPackContent) {
|
||||||
};
|
};
|
||||||
const handleUsageChange = (newUsage) => {
|
const handleUsageChange = (newUsage) => {
|
||||||
const usage = [];
|
const usage = [];
|
||||||
if (newUsage === "emoticon" || newUsage === "both") usage.push("emoticon");
|
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||||
if (newUsage === "sticker" || newUsage === "both") usage.push("sticker");
|
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||||
pack.setUsage(usage);
|
pack.setUsage(usage);
|
||||||
pack
|
pack.getImages().forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
||||||
.getImages()
|
|
||||||
.forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
|
||||||
|
|
||||||
sendPackContent(pack.getContent());
|
sendPackContent(pack.getContent());
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
|
@ -176,10 +161,10 @@ function useImagePackHandles(pack, sendPackContent) {
|
||||||
};
|
};
|
||||||
const handleDeleteItem = async (key) => {
|
const handleDeleteItem = async (key) => {
|
||||||
const isConfirmed = await confirmDialog(
|
const isConfirmed = await confirmDialog(
|
||||||
"Delete",
|
'Delete',
|
||||||
`Are you sure that you want to delete "${key}"?`,
|
`Are you sure that you want to delete "${key}"?`,
|
||||||
"Delete",
|
'Delete',
|
||||||
"danger"
|
'danger',
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
pack.removeImage(key);
|
pack.removeImage(key);
|
||||||
|
@ -189,8 +174,8 @@ function useImagePackHandles(pack, sendPackContent) {
|
||||||
};
|
};
|
||||||
const handleUsageItem = (key, newUsage) => {
|
const handleUsageItem = (key, newUsage) => {
|
||||||
const usage = [];
|
const usage = [];
|
||||||
if (newUsage === "emoticon" || newUsage === "both") usage.push("emoticon");
|
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||||
if (newUsage === "sticker" || newUsage === "both") usage.push("sticker");
|
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||||
pack.setImageUsage(key, usage);
|
pack.setImageUsage(key, usage);
|
||||||
|
|
||||||
sendPackContent(pack.getContent());
|
sendPackContent(pack.getContent());
|
||||||
|
@ -220,23 +205,21 @@ function useImagePackHandles(pack, sendPackContent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGlobalImagePack(mx, roomId, stateKey) {
|
function addGlobalImagePack(mx, roomId, stateKey) {
|
||||||
const content =
|
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||||
mx.getAccountData("im.ponies.emote_rooms")?.getContent() ?? {};
|
|
||||||
if (!content.rooms) content.rooms = {};
|
if (!content.rooms) content.rooms = {};
|
||||||
if (!content.rooms[roomId]) content.rooms[roomId] = {};
|
if (!content.rooms[roomId]) content.rooms[roomId] = {};
|
||||||
content.rooms[roomId][stateKey] = {};
|
content.rooms[roomId][stateKey] = {};
|
||||||
return mx.setAccountData("im.ponies.emote_rooms", content);
|
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||||
}
|
}
|
||||||
function removeGlobalImagePack(mx, roomId, stateKey) {
|
function removeGlobalImagePack(mx, roomId, stateKey) {
|
||||||
const content =
|
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||||
mx.getAccountData("im.ponies.emote_rooms")?.getContent() ?? {};
|
|
||||||
if (!content.rooms) return Promise.resolve();
|
if (!content.rooms) return Promise.resolve();
|
||||||
if (!content.rooms[roomId]) return Promise.resolve();
|
if (!content.rooms[roomId]) return Promise.resolve();
|
||||||
delete content.rooms[roomId][stateKey];
|
delete content.rooms[roomId][stateKey];
|
||||||
if (Object.keys(content.rooms[roomId]).length === 0) {
|
if (Object.keys(content.rooms[roomId]).length === 0) {
|
||||||
delete content.rooms[roomId];
|
delete content.rooms[roomId];
|
||||||
}
|
}
|
||||||
return mx.setAccountData("im.ponies.emote_rooms", content);
|
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||||
|
@ -264,17 +247,14 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||||
const canChange = room.currentState.hasSufficientPowerLevelFor(
|
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||||
"state_default",
|
|
||||||
myPowerlevel
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeletePack = async () => {
|
const handleDeletePack = async () => {
|
||||||
const isConfirmed = await confirmDialog(
|
const isConfirmed = await confirmDialog(
|
||||||
"Delete Pack",
|
'Delete Pack',
|
||||||
`Are you sure that you want to delete "${pack.displayName}"?`,
|
`Are you sure that you want to delete "${pack.displayName}"?`,
|
||||||
"Delete",
|
'Delete',
|
||||||
"danger"
|
'danger',
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
handlePackDelete(stateKey);
|
handlePackDelete(stateKey);
|
||||||
|
@ -285,20 +265,18 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||||
return (
|
return (
|
||||||
<div className="image-pack">
|
<div className="image-pack">
|
||||||
<ImagePackProfile
|
<ImagePackProfile
|
||||||
avatarUrl={
|
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||||
pack.avatarUrl
|
displayName={pack.displayName ?? 'Unknown'}
|
||||||
? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, "crop")
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
displayName={pack.displayName ?? "Unknown"}
|
|
||||||
attribution={pack.attribution}
|
attribution={pack.attribution}
|
||||||
usage={getUsage(pack.usage)}
|
usage={getUsage(pack.usage)}
|
||||||
onUsageChange={canChange ? handleUsageChange : null}
|
onUsageChange={canChange ? handleUsageChange : null}
|
||||||
onAvatarChange={canChange ? handleAvatarChange : null}
|
onAvatarChange={canChange ? handleAvatarChange : null}
|
||||||
onEditProfile={canChange ? handleEditProfile : null}
|
onEditProfile={canChange ? handleEditProfile : null}
|
||||||
/>
|
/>
|
||||||
{canChange && <ImagePackUpload onUpload={handleAddItem} />}
|
{ canChange && (
|
||||||
{images.length === 0 ? null : (
|
<ImagePackUpload onUpload={handleAddItem} />
|
||||||
|
)}
|
||||||
|
{ images.length === 0 ? null : (
|
||||||
<div>
|
<div>
|
||||||
<div className="image-pack__header">
|
<div className="image-pack__header">
|
||||||
<Text variant="b3">Image</Text>
|
<Text variant="b3">Image</Text>
|
||||||
|
@ -322,27 +300,21 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||||
<div className="image-pack__footer">
|
<div className="image-pack__footer">
|
||||||
{pack.images.size > 2 && (
|
{pack.images.size > 2 && (
|
||||||
<Button onClick={() => setViewMore(!viewMore)}>
|
<Button onClick={() => setViewMore(!viewMore)}>
|
||||||
{viewMore ? "View less" : `View ${pack.images.size - 2} more`}
|
{
|
||||||
</Button>
|
viewMore
|
||||||
)}
|
? 'View less'
|
||||||
{handlePackDelete && (
|
: `View ${pack.images.size - 2} more`
|
||||||
<Button variant="danger" onClick={handleDeletePack}>
|
}
|
||||||
Delete Pack
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{ handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="image-pack__global">
|
<div className="image-pack__global">
|
||||||
<Checkbox
|
<Checkbox variant="positive" onToggle={handleGlobalChange} isActive={isGlobal} />
|
||||||
variant="positive"
|
|
||||||
onToggle={handleGlobalChange}
|
|
||||||
isActive={isGlobal}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<Text variant="b2">Use globally</Text>
|
<Text variant="b2">Use globally</Text>
|
||||||
<Text variant="b3">
|
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
||||||
Add this pack to your account to use in all rooms.
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -379,12 +351,8 @@ function ImagePackUser() {
|
||||||
return (
|
return (
|
||||||
<div className="image-pack">
|
<div className="image-pack">
|
||||||
<ImagePackProfile
|
<ImagePackProfile
|
||||||
avatarUrl={
|
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||||
pack.avatarUrl
|
displayName={pack.displayName ?? 'Personal'}
|
||||||
? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, "crop")
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
displayName={pack.displayName ?? "Personal"}
|
|
||||||
attribution={pack.attribution}
|
attribution={pack.attribution}
|
||||||
usage={getUsage(pack.usage)}
|
usage={getUsage(pack.usage)}
|
||||||
onUsageChange={handleUsageChange}
|
onUsageChange={handleUsageChange}
|
||||||
|
@ -392,7 +360,7 @@ function ImagePackUser() {
|
||||||
onEditProfile={handleEditProfile}
|
onEditProfile={handleEditProfile}
|
||||||
/>
|
/>
|
||||||
<ImagePackUpload onUpload={handleAddItem} />
|
<ImagePackUpload onUpload={handleAddItem} />
|
||||||
{images.length === 0 ? null : (
|
{ images.length === 0 ? null : (
|
||||||
<div>
|
<div>
|
||||||
<div className="image-pack__header">
|
<div className="image-pack__header">
|
||||||
<Text variant="b3">Image</Text>
|
<Text variant="b3">Image</Text>
|
||||||
|
@ -412,10 +380,14 @@ function ImagePackUser() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{pack.images.size > 2 && (
|
{(pack.images.size > 2) && (
|
||||||
<div className="image-pack__footer">
|
<div className="image-pack__footer">
|
||||||
<Button onClick={() => setViewMore(!viewMore)}>
|
<Button onClick={() => setViewMore(!viewMore)}>
|
||||||
{viewMore ? "View less" : `View ${pack.images.size - 2} more`}
|
{
|
||||||
|
viewMore
|
||||||
|
? 'View less'
|
||||||
|
: `View ${pack.images.size - 2} more`
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -428,13 +400,11 @@ function useGlobalImagePack() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
const roomIdToStateKeys = new Map();
|
const roomIdToStateKeys = new Map();
|
||||||
const globalContent = mx
|
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
||||||
.getAccountData("im.ponies.emote_rooms")
|
|
||||||
?.getContent() ?? { rooms: {} };
|
|
||||||
const { rooms } = globalContent;
|
const { rooms } = globalContent;
|
||||||
|
|
||||||
Object.keys(rooms).forEach((roomId) => {
|
Object.keys(rooms).forEach((roomId) => {
|
||||||
if (typeof rooms[roomId] !== "object") return;
|
if (typeof rooms[roomId] !== 'object') return;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const stateKeys = Object.keys(rooms[roomId]);
|
const stateKeys = Object.keys(rooms[roomId]);
|
||||||
if (!room || stateKeys.length === 0) return;
|
if (!room || stateKeys.length === 0) return;
|
||||||
|
@ -443,11 +413,11 @@ function useGlobalImagePack() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEvent = (event) => {
|
const handleEvent = (event) => {
|
||||||
if (event.getType() === "im.ponies.emote_rooms") forceUpdate();
|
if (event.getType() === 'im.ponies.emote_rooms') forceUpdate();
|
||||||
};
|
};
|
||||||
mx.addListener("accountData", handleEvent);
|
mx.addListener('accountData', handleEvent);
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener("accountData", handleEvent);
|
mx.removeListener('accountData', handleEvent);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -466,39 +436,29 @@ function ImagePackGlobal() {
|
||||||
<div className="image-pack-global">
|
<div className="image-pack-global">
|
||||||
<MenuHeader>Global packs</MenuHeader>
|
<MenuHeader>Global packs</MenuHeader>
|
||||||
<div>
|
<div>
|
||||||
{roomIdToStateKeys.size > 0 ? (
|
{
|
||||||
[...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
roomIdToStateKeys.size > 0
|
||||||
|
? [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
return stateKeys.map((stateKey) => {
|
return (
|
||||||
const data = room.currentState.getStateEvents(
|
stateKeys.map((stateKey) => {
|
||||||
"im.ponies.room_emotes",
|
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||||
stateKey
|
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
|
||||||
);
|
|
||||||
const pack = ImagePackBuilder.parsePack(
|
|
||||||
data?.getId(),
|
|
||||||
data?.getContent()
|
|
||||||
);
|
|
||||||
if (!pack) return null;
|
if (!pack) return null;
|
||||||
return (
|
return (
|
||||||
<div className="image-pack__global" key={pack.id}>
|
<div className="image-pack__global" key={pack.id}>
|
||||||
<Checkbox
|
<Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive />
|
||||||
variant="positive"
|
|
||||||
onToggle={() => handleChange(roomId, stateKey)}
|
|
||||||
isActive
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<Text variant="b2">{pack.displayName ?? "Unknown"}</Text>
|
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
|
||||||
<Text variant="b3">{room.name}</Text>
|
<Text variant="b3">{room.name}</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
})
|
})
|
||||||
) : (
|
);
|
||||||
<div className="image-pack-global__empty">
|
})
|
||||||
<Text>No global packs</Text>
|
: <div className="image-pack-global__empty"><Text>No global packs</Text></div>
|
||||||
</div>
|
}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
|
|
||||||
.image-pack {
|
.image-pack {
|
||||||
&-item {
|
&-item {
|
||||||
|
|
|
@ -1,33 +1,28 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ImagePackItem.scss";
|
import './ImagePackItem.scss';
|
||||||
|
|
||||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords } from "../../../util/common";
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
import Avatar from "../../atoms/avatar/Avatar";
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import RawIcon from "../../atoms/system-icons/RawIcon";
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import ImagePackUsageSelector from "./ImagePackUsageSelector";
|
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||||
|
|
||||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
|
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||||
import BinIC from "../../../../public/res/ic/outlined/bin.svg";
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||||
|
|
||||||
function ImagePackItem({
|
function ImagePackItem({
|
||||||
url,
|
url, shortcode, usage, onUsageChange, onDelete, onRename,
|
||||||
shortcode,
|
|
||||||
usage,
|
|
||||||
onUsageChange,
|
|
||||||
onDelete,
|
|
||||||
onRename,
|
|
||||||
}) {
|
}) {
|
||||||
const handleUsageSelect = (event) => {
|
const handleUsageSelect = (event) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu(
|
||||||
"bottom",
|
'bottom',
|
||||||
getEventCords(event, ".btn-surface"),
|
getEventCords(event, '.btn-surface'),
|
||||||
(closeMenu) => (
|
(closeMenu) => (
|
||||||
<ImagePackUsageSelector
|
<ImagePackUsageSelector
|
||||||
usage={usage}
|
usage={usage}
|
||||||
|
@ -36,48 +31,27 @@ function ImagePackItem({
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-pack-item">
|
<div className="image-pack-item">
|
||||||
<Avatar
|
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
|
||||||
imageSrc={url}
|
|
||||||
size="extra-small"
|
|
||||||
text={shortcode}
|
|
||||||
bgColor="black"
|
|
||||||
/>
|
|
||||||
<div className="image-pack-item__content">
|
<div className="image-pack-item__content">
|
||||||
<Text>{shortcode}</Text>
|
<Text>{shortcode}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="image-pack-item__usage">
|
<div className="image-pack-item__usage">
|
||||||
<div className="image-pack-item__btn">
|
<div className="image-pack-item__btn">
|
||||||
{onRename && (
|
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
|
||||||
<IconButton
|
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
|
||||||
tooltip="Rename"
|
|
||||||
size="extra-small"
|
|
||||||
src={PencilIC}
|
|
||||||
onClick={() => onRename(shortcode)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{onDelete && (
|
|
||||||
<IconButton
|
|
||||||
tooltip="Delete"
|
|
||||||
size="extra-small"
|
|
||||||
src={BinIC}
|
|
||||||
onClick={() => onDelete(shortcode)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
||||||
{onUsageChange && (
|
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
||||||
<RawIcon src={ChevronBottomIC} size="extra-small" />
|
|
||||||
)}
|
|
||||||
<Text variant="b2">
|
<Text variant="b2">
|
||||||
{usage === "emoticon" && "Emoji"}
|
{usage === 'emoticon' && 'Emoji'}
|
||||||
{usage === "sticker" && "Sticker"}
|
{usage === 'sticker' && 'Sticker'}
|
||||||
{usage === "both" && "Both"}
|
{usage === 'both' && 'Both'}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,7 +67,7 @@ ImagePackItem.defaultProps = {
|
||||||
ImagePackItem.propTypes = {
|
ImagePackItem.propTypes = {
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
shortcode: PropTypes.string.isRequired,
|
shortcode: PropTypes.string.isRequired,
|
||||||
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
|
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||||
onUsageChange: PropTypes.func,
|
onUsageChange: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onRename: PropTypes.func,
|
onRename: PropTypes.func,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.image-pack-item {
|
.image-pack-item {
|
||||||
margin: 0 var(--sp-normal);
|
margin: 0 var(--sp-normal);
|
||||||
|
|
|
@ -1,29 +1,24 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ImagePackProfile.scss";
|
import './ImagePackProfile.scss';
|
||||||
|
|
||||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords } from "../../../util/common";
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Avatar from "../../atoms/avatar/Avatar";
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import ImageUpload from "../image-upload/ImageUpload";
|
import ImageUpload from '../image-upload/ImageUpload';
|
||||||
import ImagePackUsageSelector from "./ImagePackUsageSelector";
|
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||||
|
|
||||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
|
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||||
|
|
||||||
function ImagePackProfile({
|
function ImagePackProfile({
|
||||||
avatarUrl,
|
avatarUrl, displayName, attribution, usage,
|
||||||
displayName,
|
onUsageChange, onAvatarChange, onEditProfile,
|
||||||
attribution,
|
|
||||||
usage,
|
|
||||||
onUsageChange,
|
|
||||||
onAvatarChange,
|
|
||||||
onEditProfile,
|
|
||||||
}) {
|
}) {
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
|
||||||
|
@ -40,8 +35,8 @@ function ImagePackProfile({
|
||||||
|
|
||||||
const handleUsageSelect = (event) => {
|
const handleUsageSelect = (event) => {
|
||||||
openReusableContextMenu(
|
openReusableContextMenu(
|
||||||
"bottom",
|
'bottom',
|
||||||
getEventCords(event, ".btn-surface"),
|
getEventCords(event, '.btn-surface'),
|
||||||
(closeMenu) => (
|
(closeMenu) => (
|
||||||
<ImagePackUsageSelector
|
<ImagePackUsageSelector
|
||||||
usage={usage}
|
usage={usage}
|
||||||
|
@ -50,13 +45,15 @@ function ImagePackProfile({
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-pack-profile">
|
<div className="image-pack-profile">
|
||||||
{onAvatarChange ? (
|
{
|
||||||
|
onAvatarChange
|
||||||
|
? (
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
bgColor="#555"
|
bgColor="#555"
|
||||||
text={displayName}
|
text={displayName}
|
||||||
|
@ -65,28 +62,18 @@ function ImagePackProfile({
|
||||||
onUpload={onAvatarChange}
|
onUpload={onAvatarChange}
|
||||||
onRequestRemove={() => onAvatarChange(undefined)}
|
onRequestRemove={() => onAvatarChange(undefined)}
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
<Avatar
|
: <Avatar bgColor="#555" text={displayName} imageSrc={avatarUrl} size="normal" />
|
||||||
bgColor="#555"
|
}
|
||||||
text={displayName}
|
|
||||||
imageSrc={avatarUrl}
|
|
||||||
size="normal"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="image-pack-profile__content">
|
<div className="image-pack-profile__content">
|
||||||
{isEdit ? (
|
{
|
||||||
|
isEdit
|
||||||
|
? (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Input name="nameInput" label="Name" value={displayName} required />
|
<Input name="nameInput" label="Name" value={displayName} required />
|
||||||
<Input
|
<Input name="attributionInput" label="Attribution" value={attribution} resizable />
|
||||||
name="attributionInput"
|
|
||||||
label="Attribution"
|
|
||||||
value={attribution}
|
|
||||||
resizable
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">Save</Button>
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -94,18 +81,12 @@ function ImagePackProfile({
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Text>{displayName}</Text>
|
<Text>{displayName}</Text>
|
||||||
{onEditProfile && (
|
{onEditProfile && <IconButton size="extra-small" onClick={() => setIsEdit(true)} src={PencilIC} tooltip="Edit" />}
|
||||||
<IconButton
|
|
||||||
size="extra-small"
|
|
||||||
onClick={() => setIsEdit(true)}
|
|
||||||
src={PencilIC}
|
|
||||||
tooltip="Edit"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
{attribution && <Text variant="b3">{attribution}</Text>}
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="image-pack-profile__usage">
|
<div className="image-pack-profile__usage">
|
||||||
<Text variant="b3">Pack usage</Text>
|
<Text variant="b3">Pack usage</Text>
|
||||||
|
@ -114,9 +95,9 @@ function ImagePackProfile({
|
||||||
iconSrc={onUsageChange ? ChevronBottomIC : null}
|
iconSrc={onUsageChange ? ChevronBottomIC : null}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
{usage === "emoticon" && "Emoji"}
|
{usage === 'emoticon' && 'Emoji'}
|
||||||
{usage === "sticker" && "Sticker"}
|
{usage === 'sticker' && 'Sticker'}
|
||||||
{usage === "both" && "Both"}
|
{usage === 'both' && 'Both'}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,7 +116,7 @@ ImagePackProfile.propTypes = {
|
||||||
avatarUrl: PropTypes.string,
|
avatarUrl: PropTypes.string,
|
||||||
displayName: PropTypes.string.isRequired,
|
displayName: PropTypes.string.isRequired,
|
||||||
attribution: PropTypes.string,
|
attribution: PropTypes.string,
|
||||||
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
|
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||||
onUsageChange: PropTypes.func,
|
onUsageChange: PropTypes.func,
|
||||||
onAvatarChange: PropTypes.func,
|
onAvatarChange: PropTypes.func,
|
||||||
onEditProfile: PropTypes.func,
|
onEditProfile: PropTypes.func,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../partials/flex";
|
@use '../../partials/flex';
|
||||||
|
|
||||||
.image-pack-profile {
|
.image-pack-profile {
|
||||||
padding: var(--sp-normal);
|
padding: var(--sp-normal);
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ImagePackUpload.scss";
|
import './ImagePackUpload.scss';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { scaleDownImage } from "../../../util/common";
|
import { scaleDownImage } from '../../../util/common';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import CirclePlusIC from "../../../../public/res/ic/outlined/circle-plus.svg";
|
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||||
|
|
||||||
function ImagePackUpload({ onUpload }) {
|
function ImagePackUpload({ onUpload }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
@ -23,7 +23,7 @@ function ImagePackUpload({ onUpload }) {
|
||||||
if (!imgFile) return;
|
if (!imgFile) return;
|
||||||
const { shortcodeInput } = evt.target;
|
const { shortcodeInput } = evt.target;
|
||||||
const shortcode = shortcodeInput.value.trim();
|
const shortcode = shortcodeInput.value.trim();
|
||||||
if (shortcode === "") return;
|
if (shortcode === '') return;
|
||||||
|
|
||||||
setProgress(true);
|
setProgress(true);
|
||||||
const image = await scaleDownImage(imgFile, 512, 512);
|
const image = await scaleDownImage(imgFile, 512, 512);
|
||||||
|
@ -32,53 +32,37 @@ function ImagePackUpload({ onUpload }) {
|
||||||
onUpload(shortcode, url);
|
onUpload(shortcode, url);
|
||||||
setProgress(false);
|
setProgress(false);
|
||||||
setImgFile(null);
|
setImgFile(null);
|
||||||
shortcodeRef.current.value = "";
|
shortcodeRef.current.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (evt) => {
|
const handleFileChange = (evt) => {
|
||||||
const img = evt.target.files[0];
|
const img = evt.target.files[0];
|
||||||
if (!img) return;
|
if (!img) return;
|
||||||
setImgFile(img);
|
setImgFile(img);
|
||||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf("."));
|
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf('.'));
|
||||||
shortcodeRef.current.focus();
|
shortcodeRef.current.focus();
|
||||||
};
|
};
|
||||||
const handleRemove = () => {
|
const handleRemove = () => {
|
||||||
setImgFile(null);
|
setImgFile(null);
|
||||||
inputRef.current.value = null;
|
inputRef.current.value = null;
|
||||||
shortcodeRef.current.value = "";
|
shortcodeRef.current.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="image-pack-upload">
|
<form onSubmit={handleSubmit} className="image-pack-upload">
|
||||||
<input
|
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" accept=".png, .gif, .webp" required />
|
||||||
ref={inputRef}
|
{
|
||||||
onChange={handleFileChange}
|
imgFile
|
||||||
style={{ display: "none" }}
|
? (
|
||||||
type="file"
|
|
||||||
accept=".png, .gif, .webp"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{imgFile ? (
|
|
||||||
<div className="image-pack-upload__file">
|
<div className="image-pack-upload__file">
|
||||||
<IconButton
|
<IconButton onClick={handleRemove} src={CirclePlusIC} tooltip="Remove file" />
|
||||||
onClick={handleRemove}
|
|
||||||
src={CirclePlusIC}
|
|
||||||
tooltip="Remove file"
|
|
||||||
/>
|
|
||||||
<Text>{imgFile.name}</Text>
|
<Text>{imgFile.name}</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)
|
||||||
<Button onClick={() => inputRef.current.click()}>Import image</Button>
|
: <Button onClick={() => inputRef.current.click()}>Import image</Button>
|
||||||
)}
|
}
|
||||||
<Input
|
<Input forwardRef={shortcodeRef} name="shortcodeInput" placeholder="shortcode" required />
|
||||||
forwardRef={shortcodeRef}
|
<Button disabled={progress} variant="primary" type="submit">{progress ? 'Uploading...' : 'Upload'}</Button>
|
||||||
name="shortcodeInput"
|
|
||||||
placeholder="shortcode"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button disabled={progress} variant="primary" type="submit">
|
|
||||||
{progress ? "Uploading..." : "Upload"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
|
|
||||||
.image-pack-upload {
|
.image-pack-upload {
|
||||||
padding: var(--sp-normal);
|
padding: var(--sp-normal);
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { MenuHeader, MenuItem } from "../../atoms/context-menu/ContextMenu";
|
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
import CheckIC from "../../../../public/res/ic/outlined/check.svg";
|
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
||||||
|
|
||||||
function ImagePackUsageSelector({ usage, onSelect }) {
|
function ImagePackUsageSelector({ usage, onSelect }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<MenuHeader>Usage</MenuHeader>
|
<MenuHeader>Usage</MenuHeader>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={usage === "emoticon" ? CheckIC : undefined}
|
iconSrc={usage === 'emoticon' ? CheckIC : undefined}
|
||||||
variant={usage === "emoticon" ? "positive" : "surface"}
|
variant={usage === 'emoticon' ? 'positive' : 'surface'}
|
||||||
onClick={() => onSelect("emoticon")}
|
onClick={() => onSelect('emoticon')}
|
||||||
>
|
>
|
||||||
Emoji
|
Emoji
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={usage === "sticker" ? CheckIC : undefined}
|
iconSrc={usage === 'sticker' ? CheckIC : undefined}
|
||||||
variant={usage === "sticker" ? "positive" : "surface"}
|
variant={usage === 'sticker' ? 'positive' : 'surface'}
|
||||||
onClick={() => onSelect("sticker")}
|
onClick={() => onSelect('sticker')}
|
||||||
>
|
>
|
||||||
Sticker
|
Sticker
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={usage === "both" ? CheckIC : undefined}
|
iconSrc={usage === 'both' ? CheckIC : undefined}
|
||||||
variant={usage === "both" ? "positive" : "surface"}
|
variant={usage === 'both' ? 'positive' : 'surface'}
|
||||||
onClick={() => onSelect("both")}
|
onClick={() => onSelect('both')}
|
||||||
>
|
>
|
||||||
Both
|
Both
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -34,7 +34,7 @@ function ImagePackUsageSelector({ usage, onSelect }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ImagePackUsageSelector.propTypes = {
|
ImagePackUsageSelector.propTypes = {
|
||||||
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
|
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./ImageUpload.scss";
|
import './ImageUpload.scss';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Avatar from "../../atoms/avatar/Avatar";
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
import Spinner from "../../atoms/spinner/Spinner";
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
import RawIcon from "../../atoms/system-icons/RawIcon";
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
|
|
||||||
import PlusIC from "../../../../public/res/ic/outlined/plus.svg";
|
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||||
|
|
||||||
function ImageUpload({
|
function ImageUpload({
|
||||||
text,
|
text, bgColor, imageSrc, onUpload, onRequestRemove,
|
||||||
bgColor,
|
|
||||||
imageSrc,
|
|
||||||
onUpload,
|
|
||||||
onRequestRemove,
|
|
||||||
size,
|
size,
|
||||||
}) {
|
}) {
|
||||||
const [uploadPromise, setUploadPromise] = useState(null);
|
const [uploadPromise, setUploadPromise] = useState(null);
|
||||||
|
@ -30,7 +26,7 @@ function ImageUpload({
|
||||||
setUploadPromise(uPromise);
|
setUploadPromise(uPromise);
|
||||||
|
|
||||||
const res = await uPromise;
|
const res = await uPromise;
|
||||||
if (typeof res?.content_uri === "string") onUpload(res.content_uri);
|
if (typeof res?.content_uri === 'string') onUpload(res.content_uri);
|
||||||
setUploadPromise(null);
|
setUploadPromise(null);
|
||||||
} catch {
|
} catch {
|
||||||
setUploadPromise(null);
|
setUploadPromise(null);
|
||||||
|
@ -54,48 +50,40 @@ function ImageUpload({
|
||||||
uploadImageRef.current.click();
|
uploadImageRef.current.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar imageSrc={imageSrc} text={text} bgColor={bgColor} size={size} />
|
<Avatar
|
||||||
<div
|
imageSrc={imageSrc}
|
||||||
className={`img-upload__process ${
|
text={text}
|
||||||
uploadPromise === null ? " img-upload__process--stopped" : ""
|
bgColor={bgColor}
|
||||||
}`}
|
size={size}
|
||||||
>
|
/>
|
||||||
{uploadPromise === null &&
|
<div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
|
||||||
(size === "large" ? (
|
{uploadPromise === null && (
|
||||||
<Text variant="b3" weight="bold">
|
size === 'large'
|
||||||
Upload
|
? <Text variant="b3" weight="bold">Upload</Text>
|
||||||
</Text>
|
: <RawIcon src={PlusIC} color="white" />
|
||||||
) : (
|
)}
|
||||||
<RawIcon src={PlusIC} color="white" />
|
|
||||||
))}
|
|
||||||
{uploadPromise !== null && <Spinner size="small" />}
|
{uploadPromise !== null && <Spinner size="small" />}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{(typeof imageSrc === "string" || uploadPromise !== null) && (
|
{ (typeof imageSrc === 'string' || uploadPromise !== null) && (
|
||||||
<button
|
<button
|
||||||
className="img-upload__btn-cancel"
|
className="img-upload__btn-cancel"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={uploadPromise === null ? onRequestRemove : cancelUpload}
|
onClick={uploadPromise === null ? onRequestRemove : cancelUpload}
|
||||||
>
|
>
|
||||||
<Text variant="b3">{uploadPromise ? "Cancel" : "Remove"}</Text>
|
<Text variant="b3">{uploadPromise ? 'Cancel' : 'Remove'}</Text>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<input
|
<input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" accept="image/*" />
|
||||||
onChange={uploadImage}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
ref={uploadImageRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageUpload.defaultProps = {
|
ImageUpload.defaultProps = {
|
||||||
text: null,
|
text: null,
|
||||||
bgColor: "transparent",
|
bgColor: 'transparent',
|
||||||
imageSrc: null,
|
imageSrc: null,
|
||||||
size: "large",
|
size: 'large',
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageUpload.propTypes = {
|
ImageUpload.propTypes = {
|
||||||
|
@ -104,7 +92,7 @@ ImageUpload.propTypes = {
|
||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
onUpload: PropTypes.func.isRequired,
|
onUpload: PropTypes.func.isRequired,
|
||||||
onRequestRemove: PropTypes.func.isRequired,
|
onRequestRemove: PropTypes.func.isRequired,
|
||||||
size: PropTypes.oneOf(["large", "normal"]),
|
size: PropTypes.oneOf(['large', 'normal']),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImageUpload;
|
export default ImageUpload;
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, .6);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
& .donut-spinner {
|
& .donut-spinner {
|
||||||
border-color: rgb(255, 255, 255, 0.3);
|
border-color: rgb(255, 255, 255, .3);
|
||||||
border-left-color: white;
|
border-left-color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,11 +38,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&__btn-cancel {
|
&__btn-cancel {
|
||||||
margin-top: var(--sp-extra-tight);
|
margin-top: var(--sp-extra-tight);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
& .text {
|
& .text {
|
||||||
color: var(--tc-danger-normal);
|
color: var(--tc-danger-normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import "./ExportE2ERoomKeys.scss";
|
import './ExportE2ERoomKeys.scss';
|
||||||
|
|
||||||
import FileSaver from "file-saver";
|
import FileSaver from 'file-saver';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from "../../../client/state/cons";
|
import cons from '../../../client/state/cons';
|
||||||
import { encryptMegolmKeyFile } from "../../../util/cryptE2ERoomKeys";
|
import { encryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import Spinner from "../../atoms/spinner/Spinner";
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
|
|
||||||
import { useStore } from "../../hooks/useStore";
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
function ExportE2ERoomKeys() {
|
function ExportE2ERoomKeys() {
|
||||||
const isMountStore = useStore();
|
const isMountStore = useStore();
|
||||||
|
@ -29,14 +29,14 @@ function ExportE2ERoomKeys() {
|
||||||
if (password !== confirmPasswordRef.current.value) {
|
if (password !== confirmPasswordRef.current.value) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
msg: "Password does not match.",
|
msg: 'Password does not match.',
|
||||||
type: cons.status.ERROR,
|
type: cons.status.ERROR,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: true,
|
isOngoing: true,
|
||||||
msg: "Getting keys...",
|
msg: 'Getting keys...',
|
||||||
type: cons.status.IN_FLIGHT,
|
type: cons.status.IN_FLIGHT,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
@ -44,22 +44,19 @@ function ExportE2ERoomKeys() {
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: true,
|
isOngoing: true,
|
||||||
msg: "Encrypting keys...",
|
msg: 'Encrypting keys...',
|
||||||
type: cons.status.IN_FLIGHT,
|
type: cons.status.IN_FLIGHT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const encKeys = await encryptMegolmKeyFile(
|
const encKeys = await encryptMegolmKeyFile(JSON.stringify(keys), password);
|
||||||
JSON.stringify(keys),
|
|
||||||
password
|
|
||||||
);
|
|
||||||
const blob = new Blob([encKeys], {
|
const blob = new Blob([encKeys], {
|
||||||
type: "text/plain;charset=us-ascii",
|
type: 'text/plain;charset=us-ascii',
|
||||||
});
|
});
|
||||||
FileSaver.saveAs(blob, "cinny-keys.txt");
|
FileSaver.saveAs(blob, 'cinny-keys.txt');
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
msg: "Successfully exported all keys.",
|
msg: 'Successfully exported all keys.',
|
||||||
type: cons.status.SUCCESS,
|
type: cons.status.SUCCESS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -67,7 +64,7 @@ function ExportE2ERoomKeys() {
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
msg: e.friendlyText || "Failed to export keys. Please try again.",
|
msg: e.friendlyText || 'Failed to export keys. Please try again.',
|
||||||
type: cons.status.ERROR,
|
type: cons.status.ERROR,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -83,45 +80,19 @@ function ExportE2ERoomKeys() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="export-e2e-room-keys">
|
<div className="export-e2e-room-keys">
|
||||||
<form
|
<form className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); exportE2ERoomKeys(); }}>
|
||||||
className="export-e2e-room-keys__form"
|
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
|
||||||
onSubmit={(e) => {
|
<Input forwardRef={confirmPasswordRef} type="password" placeholder="Confirm password" required />
|
||||||
e.preventDefault();
|
<Button disabled={status.isOngoing} variant="primary" type="submit">Export</Button>
|
||||||
exportE2ERoomKeys();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
forwardRef={passwordRef}
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
forwardRef={confirmPasswordRef}
|
|
||||||
type="password"
|
|
||||||
placeholder="Confirm password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button disabled={status.isOngoing} variant="primary" type="submit">
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
{status.type === cons.status.IN_FLIGHT && (
|
{ status.type === cons.status.IN_FLIGHT && (
|
||||||
<div className="import-e2e-room-keys__process">
|
<div className="import-e2e-room-keys__process">
|
||||||
<Spinner size="small" />
|
<Spinner size="small" />
|
||||||
<Text variant="b2">{status.msg}</Text>
|
<Text variant="b2">{status.msg}</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{status.type === cons.status.SUCCESS && (
|
{status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
|
||||||
<Text className="import-e2e-room-keys__success" variant="b2">
|
{status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
|
||||||
{status.msg}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{status.type === cons.status.ERROR && (
|
|
||||||
<Text className="import-e2e-room-keys__error" variant="b2">
|
|
||||||
{status.msg}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,4 +24,5 @@
|
||||||
margin-top: var(--sp-tight);
|
margin-top: var(--sp-tight);
|
||||||
color: var(--tc-danger-high);
|
color: var(--tc-danger-high);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,19 +1,19 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import "./ImportE2ERoomKeys.scss";
|
import './ImportE2ERoomKeys.scss';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from "../../../client/state/cons";
|
import cons from '../../../client/state/cons';
|
||||||
import { decryptMegolmKeyFile } from "../../../util/cryptE2ERoomKeys";
|
import { decryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import Spinner from "../../atoms/spinner/Spinner";
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
|
|
||||||
import CirclePlusIC from "../../../../public/res/ic/outlined/circle-plus.svg";
|
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||||
|
|
||||||
import { useStore } from "../../hooks/useStore";
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
function ImportE2ERoomKeys() {
|
function ImportE2ERoomKeys() {
|
||||||
const isMountStore = useStore();
|
const isMountStore = useStore();
|
||||||
|
@ -32,7 +32,7 @@ function ImportE2ERoomKeys() {
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: true,
|
isOngoing: true,
|
||||||
msg: "Decrypting file...",
|
msg: 'Decrypting file...',
|
||||||
type: cons.status.IN_FLIGHT,
|
type: cons.status.IN_FLIGHT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ function ImportE2ERoomKeys() {
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: true,
|
isOngoing: true,
|
||||||
msg: "Decrypting messages...",
|
msg: 'Decrypting messages...',
|
||||||
type: cons.status.IN_FLIGHT,
|
type: cons.status.IN_FLIGHT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ function ImportE2ERoomKeys() {
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
msg: "Successfully imported all keys.",
|
msg: 'Successfully imported all keys.',
|
||||||
type: cons.status.SUCCESS,
|
type: cons.status.SUCCESS,
|
||||||
});
|
});
|
||||||
inputRef.current.value = null;
|
inputRef.current.value = null;
|
||||||
|
@ -59,7 +59,7 @@ function ImportE2ERoomKeys() {
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
msg: e.friendlyText || "Failed to decrypt keys. Please try again.",
|
msg: e.friendlyText || 'Failed to decrypt keys. Please try again.',
|
||||||
type: cons.status.ERROR,
|
type: cons.status.ERROR,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ function ImportE2ERoomKeys() {
|
||||||
|
|
||||||
const importE2ERoomKeys = () => {
|
const importE2ERoomKeys = () => {
|
||||||
const password = passwordRef.current.value;
|
const password = passwordRef.current.value;
|
||||||
if (password === "" || keyFile === null) return;
|
if (password === '' || keyFile === null) return;
|
||||||
if (status.isOngoing) return;
|
if (status.isOngoing) return;
|
||||||
|
|
||||||
tryDecrypt(keyFile, password);
|
tryDecrypt(keyFile, password);
|
||||||
|
@ -76,7 +76,7 @@ function ImportE2ERoomKeys() {
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
const file = e.target.files.item(0);
|
const file = e.target.files.item(0);
|
||||||
passwordRef.current.value = "";
|
passwordRef.current.value = '';
|
||||||
setKeyFile(file);
|
setKeyFile(file);
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
|
@ -105,59 +105,27 @@ function ImportE2ERoomKeys() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="import-e2e-room-keys">
|
<div className="import-e2e-room-keys">
|
||||||
<input
|
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" />
|
||||||
ref={inputRef}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form
|
<form className="import-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); importE2ERoomKeys(); }}>
|
||||||
className="import-e2e-room-keys__form"
|
{ keyFile !== null && (
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
importE2ERoomKeys();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{keyFile !== null && (
|
|
||||||
<div className="import-e2e-room-keys__file">
|
<div className="import-e2e-room-keys__file">
|
||||||
<IconButton
|
<IconButton onClick={removeImportKeysFile} src={CirclePlusIC} tooltip="Remove file" />
|
||||||
onClick={removeImportKeysFile}
|
|
||||||
src={CirclePlusIC}
|
|
||||||
tooltip="Remove file"
|
|
||||||
/>
|
|
||||||
<Text>{keyFile.name}</Text>
|
<Text>{keyFile.name}</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{keyFile === null && (
|
{keyFile === null && <Button onClick={() => inputRef.current.click()}>Import keys</Button>}
|
||||||
<Button onClick={() => inputRef.current.click()}>Import keys</Button>
|
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
|
||||||
)}
|
<Button disabled={status.isOngoing} variant="primary" type="submit">Decrypt</Button>
|
||||||
<Input
|
|
||||||
forwardRef={passwordRef}
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button disabled={status.isOngoing} variant="primary" type="submit">
|
|
||||||
Decrypt
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
{status.type === cons.status.IN_FLIGHT && (
|
{ status.type === cons.status.IN_FLIGHT && (
|
||||||
<div className="import-e2e-room-keys__process">
|
<div className="import-e2e-room-keys__process">
|
||||||
<Spinner size="small" />
|
<Spinner size="small" />
|
||||||
<Text variant="b2">{status.msg}</Text>
|
<Text variant="b2">{status.msg}</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{status.type === cons.status.SUCCESS && (
|
{status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
|
||||||
<Text className="import-e2e-room-keys__success" variant="b2">
|
{status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
|
||||||
{status.msg}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{status.type === cons.status.ERROR && (
|
|
||||||
<Text className="import-e2e-room-keys__error" variant="b2">
|
|
||||||
{status.msg}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.import-e2e-room-keys {
|
.import-e2e-room-keys {
|
||||||
&__file {
|
&__file {
|
||||||
|
@ -34,6 +34,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: var(--sp-extra-tight);
|
margin-top: var(--sp-extra-tight);
|
||||||
|
|
||||||
|
|
||||||
& .input-container {
|
& .input-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0 var(--sp-tight);
|
margin: 0 var(--sp-tight);
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import "./Media.scss";
|
import './Media.scss';
|
||||||
|
|
||||||
import encrypt from "browser-encrypt-attachment";
|
import encrypt from 'browser-encrypt-attachment';
|
||||||
|
|
||||||
import { BlurhashCanvas } from "react-blurhash";
|
import { BlurhashCanvas } from 'react-blurhash';
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Spinner from "../../atoms/spinner/Spinner";
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
import ImageLightbox from "../image-lightbox/ImageLightbox";
|
import ImageLightbox from '../image-lightbox/ImageLightbox';
|
||||||
|
|
||||||
import DownloadSVG from "../../../../public/res/ic/outlined/download.svg";
|
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
||||||
import ExternalSVG from "../../../../public/res/ic/outlined/external.svg";
|
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
||||||
import PlaySVG from "../../../../public/res/ic/outlined/play.svg";
|
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
|
||||||
|
|
||||||
import { getBlobSafeMimeType } from "../../../util/mimetypes";
|
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
||||||
|
|
||||||
async function getDecryptedBlob(response, type, decryptData) {
|
async function getDecryptedBlob(response, type, decryptData) {
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
@ -25,11 +25,9 @@ async function getDecryptedBlob(response, type, decryptData) {
|
||||||
|
|
||||||
async function getUrl(link, type, decryptData) {
|
async function getUrl(link, type, decryptData) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(link, { method: "GET" });
|
const response = await fetch(link, { method: 'GET' });
|
||||||
if (decryptData !== null) {
|
if (decryptData !== null) {
|
||||||
return URL.createObjectURL(
|
return URL.createObjectURL(await getDecryptedBlob(response, type, decryptData));
|
||||||
await getDecryptedBlob(response, type, decryptData)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
|
@ -43,7 +41,10 @@ function getNativeHeight(width, height, maxWidth = 296) {
|
||||||
return scale * height;
|
return scale * height;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileHeader({ name, link, external, file, type }) {
|
function FileHeader({
|
||||||
|
name, link, external,
|
||||||
|
file, type,
|
||||||
|
}) {
|
||||||
const [url, setUrl] = useState(null);
|
const [url, setUrl] = useState(null);
|
||||||
|
|
||||||
async function getFile() {
|
async function getFile() {
|
||||||
|
@ -60,25 +61,20 @@ function FileHeader({ name, link, external, file, type }) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="file-header">
|
<div className="file-header">
|
||||||
<Text className="file-name" variant="b3">
|
<Text className="file-name" variant="b3">{name}</Text>
|
||||||
{name}
|
{ link !== null && (
|
||||||
</Text>
|
|
||||||
{link !== null && (
|
|
||||||
<>
|
<>
|
||||||
{external && (
|
{
|
||||||
|
external && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
tooltip="Open in new tab"
|
tooltip="Open in new tab"
|
||||||
src={ExternalSVG}
|
src={ExternalSVG}
|
||||||
onClick={() => window.open(url || link)}
|
onClick={() => window.open(url || link)}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
<a
|
}
|
||||||
href={url || link}
|
<a href={url || link} download={name} target="_blank" rel="noreferrer">
|
||||||
download={name}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
tooltip="Download"
|
tooltip="Download"
|
||||||
|
@ -104,7 +100,9 @@ FileHeader.propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function File({ name, link, file, type }) {
|
function File({
|
||||||
|
name, link, file, type,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="file-container">
|
<div className="file-container">
|
||||||
<FileHeader name={name} link={link} file={file} type={type} />
|
<FileHeader name={name} link={link} file={file} type={type} />
|
||||||
|
@ -113,7 +111,7 @@ function File({ name, link, file, type }) {
|
||||||
}
|
}
|
||||||
File.defaultProps = {
|
File.defaultProps = {
|
||||||
file: null,
|
file: null,
|
||||||
type: "",
|
type: '',
|
||||||
};
|
};
|
||||||
File.propTypes = {
|
File.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
@ -122,7 +120,9 @@ File.propTypes = {
|
||||||
file: PropTypes.shape({}),
|
file: PropTypes.shape({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
function Image({ name, width, height, link, file, type, blurhash }) {
|
function Image({
|
||||||
|
name, width, height, link, file, type, blurhash,
|
||||||
|
}) {
|
||||||
const [url, setUrl] = useState(null);
|
const [url, setUrl] = useState(null);
|
||||||
const [blur, setBlur] = useState(true);
|
const [blur, setBlur] = useState(true);
|
||||||
const [lightbox, setLightbox] = useState(false);
|
const [lightbox, setLightbox] = useState(false);
|
||||||
|
@ -149,19 +149,17 @@ function Image({ name, width, height, link, file, type, blurhash }) {
|
||||||
<>
|
<>
|
||||||
<div className="file-container">
|
<div className="file-container">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }}
|
||||||
height: width !== null ? getNativeHeight(width, height) : "unset",
|
|
||||||
}}
|
|
||||||
className="image-container"
|
className="image-container"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
onClick={toggleLightbox}
|
onClick={toggleLightbox}
|
||||||
onKeyDown={toggleLightbox}
|
onKeyDown={toggleLightbox}
|
||||||
>
|
>
|
||||||
{blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||||
{url !== null && (
|
{ url !== null && (
|
||||||
<img
|
<img
|
||||||
style={{ display: blur ? "none" : "unset" }}
|
style={{ display: blur ? 'none' : 'unset' }}
|
||||||
onLoad={() => setBlur(false)}
|
onLoad={() => setBlur(false)}
|
||||||
src={url || link}
|
src={url || link}
|
||||||
alt={name}
|
alt={name}
|
||||||
|
@ -184,8 +182,8 @@ Image.defaultProps = {
|
||||||
file: null,
|
file: null,
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
type: "",
|
type: '',
|
||||||
blurhash: "",
|
blurhash: '',
|
||||||
};
|
};
|
||||||
Image.propTypes = {
|
Image.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
@ -197,7 +195,9 @@ Image.propTypes = {
|
||||||
blurhash: PropTypes.string,
|
blurhash: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Sticker({ name, height, width, link, file, type }) {
|
function Sticker({
|
||||||
|
name, height, width, link, file, type,
|
||||||
|
}) {
|
||||||
const [url, setUrl] = useState(null);
|
const [url, setUrl] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -214,19 +214,14 @@ function Sticker({ name, height, width, link, file, type }) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="sticker-container" style={{ height: width !== null ? getNativeHeight(width, height, 128) : 'unset' }}>
|
||||||
className="sticker-container"
|
{ url !== null && <img src={url || link} title={name} alt={name} />}
|
||||||
style={{
|
|
||||||
height: width !== null ? getNativeHeight(width, height, 128) : "unset",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{url !== null && <img src={url || link} title={name} alt={name} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Sticker.defaultProps = {
|
Sticker.defaultProps = {
|
||||||
file: null,
|
file: null,
|
||||||
type: "",
|
type: '',
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
};
|
};
|
||||||
|
@ -239,7 +234,9 @@ Sticker.propTypes = {
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Audio({ name, link, type, file }) {
|
function Audio({
|
||||||
|
name, link, type, file,
|
||||||
|
}) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [url, setUrl] = useState(null);
|
const [url, setUrl] = useState(null);
|
||||||
|
|
||||||
|
@ -255,22 +252,11 @@ function Audio({ name, link, type, file }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-container">
|
<div className="file-container">
|
||||||
<FileHeader
|
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
|
||||||
name={name}
|
|
||||||
link={file !== null ? url : url || link}
|
|
||||||
type={type}
|
|
||||||
external
|
|
||||||
/>
|
|
||||||
<div className="audio-container">
|
<div className="audio-container">
|
||||||
{url === null && isLoading && <Spinner size="small" />}
|
{ url === null && isLoading && <Spinner size="small" /> }
|
||||||
{url === null && !isLoading && (
|
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
|
||||||
<IconButton
|
{ url !== null && (
|
||||||
onClick={handlePlayAudio}
|
|
||||||
tooltip="Play audio"
|
|
||||||
src={PlaySVG}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{url !== null && (
|
|
||||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||||
<audio autoPlay controls>
|
<audio autoPlay controls>
|
||||||
<source src={url} type={getBlobSafeMimeType(type)} />
|
<source src={url} type={getBlobSafeMimeType(type)} />
|
||||||
|
@ -282,7 +268,7 @@ function Audio({ name, link, type, file }) {
|
||||||
}
|
}
|
||||||
Audio.defaultProps = {
|
Audio.defaultProps = {
|
||||||
file: null,
|
file: null,
|
||||||
type: "",
|
type: '',
|
||||||
};
|
};
|
||||||
Audio.propTypes = {
|
Audio.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
@ -292,16 +278,8 @@ Audio.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function Video({
|
function Video({
|
||||||
name,
|
name, link, thumbnail, thumbnailFile, thumbnailType,
|
||||||
link,
|
width, height, file, type, blurhash,
|
||||||
thumbnail,
|
|
||||||
thumbnailFile,
|
|
||||||
thumbnailType,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
file,
|
|
||||||
type,
|
|
||||||
blurhash,
|
|
||||||
}) {
|
}) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [url, setUrl] = useState(null);
|
const [url, setUrl] = useState(null);
|
||||||
|
@ -334,37 +312,21 @@ function Video({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-container">
|
<div className="file-container">
|
||||||
<FileHeader
|
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
|
||||||
name={name}
|
|
||||||
link={file !== null ? url : url || link}
|
|
||||||
type={type}
|
|
||||||
external
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: width !== null ? getNativeHeight(width, height) : "unset",
|
height: width !== null ? getNativeHeight(width, height) : 'unset',
|
||||||
}}
|
}}
|
||||||
className="video-container"
|
className="video-container"
|
||||||
>
|
>
|
||||||
{url === null ? (
|
{ url === null ? (
|
||||||
<>
|
<>
|
||||||
{blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||||
{thumbUrl !== null && (
|
{ thumbUrl !== null && (
|
||||||
<img
|
<img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
|
||||||
style={{ display: blur ? "none" : "unset" }}
|
|
||||||
src={thumbUrl}
|
|
||||||
onLoad={() => setBlur(false)}
|
|
||||||
alt={name}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{isLoading && <Spinner size="small" />}
|
{isLoading && <Spinner size="small" />}
|
||||||
{!isLoading && (
|
{!isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
|
||||||
<IconButton
|
|
||||||
onClick={handlePlayVideo}
|
|
||||||
tooltip="Play video"
|
|
||||||
src={PlaySVG}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||||
|
@ -383,7 +345,7 @@ Video.defaultProps = {
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
thumbnailType: null,
|
thumbnailType: null,
|
||||||
thumbnailFile: null,
|
thumbnailFile: null,
|
||||||
type: "",
|
type: '',
|
||||||
blurhash: null,
|
blurhash: null,
|
||||||
};
|
};
|
||||||
Video.propTypes = {
|
Video.propTypes = {
|
||||||
|
@ -399,4 +361,6 @@ Video.propTypes = {
|
||||||
blurhash: PropTypes.string,
|
blurhash: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { File, Image, Sticker, Audio, Video };
|
export {
|
||||||
|
File, Image, Sticker, Audio, Video,
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
|
|
||||||
.file-header {
|
.file-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,55 +1,46 @@
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
import React, {
|
||||||
import PropTypes from "prop-types";
|
useState, useEffect, useCallback, useRef,
|
||||||
import "./Message.scss";
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './Message.scss';
|
||||||
|
|
||||||
import { twemojify } from "../../../util/twemojify";
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import initMatrix from "../../../client/initMatrix";
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import {
|
import {
|
||||||
getUsername,
|
getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply,
|
||||||
getUsernameOfRoomMember,
|
} from '../../../util/matrixUtil';
|
||||||
parseReply,
|
import colorMXID from '../../../util/colorMXID';
|
||||||
trimHTMLReply,
|
import { getEventCords } from '../../../util/common';
|
||||||
} from "../../../util/matrixUtil";
|
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
||||||
import colorMXID from "../../../util/colorMXID";
|
|
||||||
import { getEventCords } from "../../../util/common";
|
|
||||||
import { redactEvent, sendReaction } from "../../../client/action/roomTimeline";
|
|
||||||
import {
|
import {
|
||||||
openEmojiBoard,
|
openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
|
||||||
openProfileViewer,
|
} from '../../../client/action/navigation';
|
||||||
openReadReceipts,
|
import { sanitizeCustomHtml } from '../../../util/sanitize';
|
||||||
openViewSource,
|
|
||||||
replyTo,
|
|
||||||
} from "../../../client/action/navigation";
|
|
||||||
import { sanitizeCustomHtml } from "../../../util/sanitize";
|
|
||||||
|
|
||||||
import Text from "../../atoms/text/Text";
|
import Text from '../../atoms/text/Text';
|
||||||
import RawIcon from "../../atoms/system-icons/RawIcon";
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
import Button from "../../atoms/button/Button";
|
import Button from '../../atoms/button/Button';
|
||||||
import Tooltip from "../../atoms/tooltip/Tooltip";
|
import Tooltip from '../../atoms/tooltip/Tooltip';
|
||||||
import Input from "../../atoms/input/Input";
|
import Input from '../../atoms/input/Input';
|
||||||
import Avatar from "../../atoms/avatar/Avatar";
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
import IconButton from "../../atoms/button/IconButton";
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Time from "../../atoms/time/Time";
|
import Time from '../../atoms/time/Time';
|
||||||
import ContextMenu, {
|
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
||||||
MenuHeader,
|
import * as Media from '../media/Media';
|
||||||
MenuItem,
|
|
||||||
MenuBorder,
|
|
||||||
} from "../../atoms/context-menu/ContextMenu";
|
|
||||||
import * as Media from "../media/Media";
|
|
||||||
|
|
||||||
import ReplyArrowIC from "../../../../public/res/ic/outlined/reply-arrow.svg";
|
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
||||||
import EmojiAddIC from "../../../../public/res/ic/outlined/emoji-add.svg";
|
import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
|
||||||
import VerticalMenuIC from "../../../../public/res/ic/outlined/vertical-menu.svg";
|
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||||
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
|
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||||
import TickMarkIC from "../../../../public/res/ic/outlined/tick-mark.svg";
|
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||||
import CmdIC from "../../../../public/res/ic/outlined/cmd.svg";
|
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
|
||||||
import BinIC from "../../../../public/res/ic/outlined/bin.svg";
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||||
|
|
||||||
import { confirmDialog } from "../confirm-dialog/ConfirmDialog";
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
import { getBlobSafeMimeType } from "../../../util/mimetypes";
|
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
||||||
import { html, plain } from "../../../util/markdown";
|
import { html, plain } from '../../../util/markdown';
|
||||||
|
|
||||||
function PlaceholderMessage() {
|
function PlaceholderMessage() {
|
||||||
return (
|
return (
|
||||||
|
@ -70,21 +61,19 @@ function PlaceholderMessage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageAvatar = React.memo(({ roomId, avatarSrc, userId, username }) => (
|
const MessageAvatar = React.memo(({
|
||||||
|
roomId, avatarSrc, userId, username,
|
||||||
|
}) => (
|
||||||
<div className="message__avatar-container">
|
<div className="message__avatar-container">
|
||||||
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
||||||
<Avatar
|
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
|
||||||
imageSrc={avatarSrc}
|
|
||||||
text={username}
|
|
||||||
bgColor={colorMXID(userId)}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
const MessageHeader = React.memo(
|
const MessageHeader = React.memo(({
|
||||||
({ userId, username, timestamp, fullTime }) => (
|
userId, username, timestamp, fullTime,
|
||||||
|
}) => (
|
||||||
<div className="message__header">
|
<div className="message__header">
|
||||||
<Text
|
<Text
|
||||||
style={{ color: colorMXID(userId) }}
|
style={{ color: colorMXID(userId) }}
|
||||||
|
@ -102,8 +91,7 @@ const MessageHeader = React.memo(
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
));
|
||||||
);
|
|
||||||
MessageHeader.defaultProps = {
|
MessageHeader.defaultProps = {
|
||||||
fullTime: false,
|
fullTime: false,
|
||||||
};
|
};
|
||||||
|
@ -119,7 +107,9 @@ function MessageReply({ name, color, body }) {
|
||||||
<div className="message__reply">
|
<div className="message__reply">
|
||||||
<Text variant="b2">
|
<Text variant="b2">
|
||||||
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
||||||
<span style={{ color }}>{twemojify(name)}</span> {twemojify(body)}
|
<span style={{ color }}>{twemojify(name)}</span>
|
||||||
|
{' '}
|
||||||
|
{twemojify(body)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -153,11 +143,9 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||||
const username = getUsernameOfRoomMember(mEvent.sender);
|
const username = getUsernameOfRoomMember(mEvent.sender);
|
||||||
|
|
||||||
if (isMountedRef.current === false) return;
|
if (isMountedRef.current === false) return;
|
||||||
const fallbackBody = mEvent.isRedacted()
|
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
|
||||||
? "*** This message has been deleted ***"
|
|
||||||
: "*** Unable to load reply ***";
|
|
||||||
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
|
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
|
||||||
if (editedList && parsedBody.startsWith(" * ")) {
|
if (editedList && parsedBody.startsWith(' * ')) {
|
||||||
parsedBody = parsedBody.slice(3);
|
parsedBody = parsedBody.slice(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,9 +157,9 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
setReply({
|
setReply({
|
||||||
to: "** Unknown user **",
|
to: '** Unknown user **',
|
||||||
color: "var(--tc-danger-normal)",
|
color: 'var(--tc-danger-normal)',
|
||||||
body: "*** Unable to load reply ***",
|
body: '*** Unable to load reply ***',
|
||||||
event: null,
|
event: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -184,7 +172,7 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const focusReply = (ev) => {
|
const focusReply = (ev) => {
|
||||||
if (!ev.key || ev.key === " " || ev.key === "Enter") {
|
if (!ev.key || ev.key === ' ' || ev.key === 'Enter') {
|
||||||
if (ev.key) ev.preventDefault();
|
if (ev.key) ev.preventDefault();
|
||||||
if (reply?.event === null) return;
|
if (reply?.event === null) return;
|
||||||
if (reply?.event.isRedacted()) return;
|
if (reply?.event.isRedacted()) return;
|
||||||
|
@ -200,9 +188,7 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
>
|
>
|
||||||
{reply !== null && (
|
{reply !== null && <MessageReply name={reply.to} color={reply.color} body={reply.body} />}
|
||||||
<MessageReply name={reply.to} color={reply.color} body={reply.body} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -211,11 +197,15 @@ MessageReplyWrapper.propTypes = {
|
||||||
eventId: PropTypes.string.isRequired,
|
eventId: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageBody = React.memo(
|
const MessageBody = React.memo(({
|
||||||
({ senderName, body, isCustomHTML, isEdited, msgType }) => {
|
senderName,
|
||||||
|
body,
|
||||||
|
isCustomHTML,
|
||||||
|
isEdited,
|
||||||
|
msgType,
|
||||||
|
}) => {
|
||||||
// if body is not string it is a React element.
|
// if body is not string it is a React element.
|
||||||
if (typeof body !== "string")
|
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
|
||||||
return <div className="message__body">{body}</div>;
|
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
if (isCustomHTML) {
|
if (isCustomHTML) {
|
||||||
|
@ -225,10 +215,10 @@ const MessageBody = React.memo(
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
console.error("Malformed custom html: ", body);
|
console.error('Malformed custom html: ', body);
|
||||||
content = twemojify(body, undefined);
|
content = twemojify(body, undefined);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -240,24 +230,20 @@ const MessageBody = React.memo(
|
||||||
// - Contains only emoji
|
// - Contains only emoji
|
||||||
// - Contains no more than 10 emoji
|
// - Contains no more than 10 emoji
|
||||||
let emojiOnly = false;
|
let emojiOnly = false;
|
||||||
if (content.type === "img") {
|
if (content.type === 'img') {
|
||||||
// If this messages contains only a single (inline) image
|
// If this messages contains only a single (inline) image
|
||||||
emojiOnly = true;
|
emojiOnly = true;
|
||||||
} else if (content.constructor.name === "Array") {
|
} else if (content.constructor.name === 'Array') {
|
||||||
// Otherwise, it might be an array of images / texb
|
// Otherwise, it might be an array of images / texb
|
||||||
|
|
||||||
// Count the number of emojis
|
// Count the number of emojis
|
||||||
const nEmojis = content.filter((e) => e.type === "img").length;
|
const nEmojis = content.filter((e) => e.type === 'img').length;
|
||||||
|
|
||||||
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
||||||
if (
|
if (nEmojis <= 10 && content.every((element) => (
|
||||||
nEmojis <= 10 &&
|
(typeof element === 'object' && element.type === 'img')
|
||||||
content.every(
|
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
||||||
(element) =>
|
))) {
|
||||||
(typeof element === "object" && element.type === "img") ||
|
|
||||||
(typeof element === "string" && /^[\s\ufe0f]*$/g.test(element))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
emojiOnly = true;
|
emojiOnly = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,29 +251,25 @@ const MessageBody = React.memo(
|
||||||
if (!isCustomHTML) {
|
if (!isCustomHTML) {
|
||||||
// If this is a plaintext message, wrap it in a <p> element (automatically applying
|
// If this is a plaintext message, wrap it in a <p> element (automatically applying
|
||||||
// white-space: pre-wrap) in order to preserve newlines
|
// white-space: pre-wrap) in order to preserve newlines
|
||||||
content = <p className="message__body-plain">{content}</p>;
|
content = (<p className="message__body-plain">{content}</p>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="message__body">
|
<div className="message__body">
|
||||||
<div dir="auto" className={`text ${emojiOnly ? "text-h1" : "text-b1"}`}>
|
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
||||||
{msgType === "m.emote" && (
|
{ msgType === 'm.emote' && (
|
||||||
<>
|
<>
|
||||||
{"* "}
|
{'* '}
|
||||||
{twemojify(senderName)}{" "}
|
{twemojify(senderName)}
|
||||||
|
{' '}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{content}
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
{isEdited && (
|
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
|
||||||
<Text className="message__body-edited" variant="b3">
|
|
||||||
(edited)
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
MessageBody.defaultProps = {
|
MessageBody.defaultProps = {
|
||||||
isCustomHTML: false,
|
isCustomHTML: false,
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
|
@ -306,30 +288,24 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// makes the cursor end up at the end of the line instead of the beginning
|
// makes the cursor end up at the end of the line instead of the beginning
|
||||||
editInputRef.current.value = "";
|
editInputRef.current.value = '';
|
||||||
editInputRef.current.value = body;
|
editInputRef.current.value = body;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === 'Escape') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "Enter" && e.shiftKey === false) {
|
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSave(editInputRef.current.value, body);
|
onSave(editInputRef.current.value, body);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value, body); }}>
|
||||||
className="message__edit"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onSave(editInputRef.current.value, body);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
forwardRef={editInputRef}
|
forwardRef={editInputRef}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
@ -340,9 +316,7 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="message__edit-btns">
|
<div className="message__edit-btns">
|
||||||
<Button type="submit" variant="primary">
|
<Button type="submit" variant="primary">Save</Button>
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onCancel}>Cancel</Button>
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -360,10 +334,7 @@ function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
|
||||||
let rEvent = null;
|
let rEvent = null;
|
||||||
rEvents?.find((rE) => {
|
rEvents?.find((rE) => {
|
||||||
if (rE.getRelation() === null) return false;
|
if (rE.getRelation() === null) return false;
|
||||||
if (
|
if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
|
||||||
rE.getRelation().key === emojiKey &&
|
|
||||||
rE.getSender() === mx.getUserId()
|
|
||||||
) {
|
|
||||||
rEvent = rE;
|
rEvent = rE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -376,7 +347,7 @@ function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
|
||||||
const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
|
const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
|
||||||
if (myAlreadyReactEvent) {
|
if (myAlreadyReactEvent) {
|
||||||
const rId = myAlreadyReactEvent.getId();
|
const rId = myAlreadyReactEvent.getId();
|
||||||
if (rId.startsWith("~")) return;
|
if (rId.startsWith('~')) return;
|
||||||
redactEvent(roomId, rId);
|
redactEvent(roomId, rId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -385,13 +356,7 @@ function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
|
||||||
|
|
||||||
function pickEmoji(e, roomId, eventId, roomTimeline) {
|
function pickEmoji(e, roomId, eventId, roomTimeline) {
|
||||||
openEmojiBoard(getEventCords(e), (emoji) => {
|
openEmojiBoard(getEventCords(e), (emoji) => {
|
||||||
toggleEmoji(
|
toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
|
||||||
roomId,
|
|
||||||
eventId,
|
|
||||||
emoji.mxc ?? emoji.unicode,
|
|
||||||
emoji.shortcodes[0],
|
|
||||||
roomTimeline
|
|
||||||
);
|
|
||||||
e.target.click();
|
e.target.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -403,27 +368,20 @@ function genReactionMsg(userIds, reaction, shortcode) {
|
||||||
<React.Fragment key={userId}>
|
<React.Fragment key={userId}>
|
||||||
{twemojify(getUsername(userId))}
|
{twemojify(getUsername(userId))}
|
||||||
{index < userIds.length - 1 && (
|
{index < userIds.length - 1 && (
|
||||||
<span style={{ opacity: ".6" }}>
|
<span style={{ opacity: '.6' }}>
|
||||||
{index === userIds.length - 2 ? " and " : ", "}
|
{index === userIds.length - 2 ? ' and ' : ', '}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
<span style={{ opacity: ".6" }}>{" reacted with "}</span>
|
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
||||||
{twemojify(shortcode ? `:${shortcode}:` : reaction, {
|
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
|
||||||
className: "react-emoji",
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageReaction({
|
function MessageReaction({
|
||||||
reaction,
|
reaction, shortcode, count, users, isActive, onClick,
|
||||||
shortcode,
|
|
||||||
count,
|
|
||||||
users,
|
|
||||||
isActive,
|
|
||||||
onClick,
|
|
||||||
}) {
|
}) {
|
||||||
let customEmojiUrl = null;
|
let customEmojiUrl = null;
|
||||||
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
||||||
|
@ -432,32 +390,19 @@ function MessageReaction({
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="msg__reaction-tooltip"
|
className="msg__reaction-tooltip"
|
||||||
content={
|
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
|
||||||
<Text variant="b2">
|
|
||||||
{users.length > 0
|
|
||||||
? genReactionMsg(users, reaction, shortcode)
|
|
||||||
: "Unable to load who has reacted"}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
type="button"
|
type="button"
|
||||||
className={`msg__reaction${isActive ? " msg__reaction--active" : ""}`}
|
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
|
||||||
>
|
>
|
||||||
{customEmojiUrl ? (
|
{
|
||||||
<img
|
customEmojiUrl
|
||||||
className="react-emoji"
|
? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
|
||||||
draggable="false"
|
: twemojify(reaction, { className: 'react-emoji' })
|
||||||
alt={shortcode ?? reaction}
|
}
|
||||||
src={customEmojiUrl}
|
<Text variant="b3" className="msg__reaction-count">{count}</Text>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
twemojify(reaction, { className: "react-emoji" })
|
|
||||||
)}
|
|
||||||
<Text variant="b3" className="msg__reaction-count">
|
|
||||||
{count}
|
|
||||||
</Text>
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
@ -478,10 +423,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||||
const { roomId, room, reactionTimeline } = roomTimeline;
|
const { roomId, room, reactionTimeline } = roomTimeline;
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const reactions = {};
|
const reactions = {};
|
||||||
const canSendReaction = room.currentState.maySendEvent(
|
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
||||||
"m.reaction",
|
|
||||||
mx.getUserId()
|
|
||||||
);
|
|
||||||
|
|
||||||
const eventReactions = reactionTimeline.get(mEvent.getId());
|
const eventReactions = reactionTimeline.get(mEvent.getId());
|
||||||
const addReaction = (key, shortcode, count, senderId, isActive) => {
|
const addReaction = (key, shortcode, count, senderId, isActive) => {
|
||||||
|
@ -516,18 +458,18 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Use aggregated reactions
|
// Use aggregated reactions
|
||||||
const aggregatedReaction =
|
const aggregatedReaction = mEvent.getServerAggregatedRelation('m.annotation')?.chunk;
|
||||||
mEvent.getServerAggregatedRelation("m.annotation")?.chunk;
|
|
||||||
if (!aggregatedReaction) return null;
|
if (!aggregatedReaction) return null;
|
||||||
aggregatedReaction.forEach((reaction) => {
|
aggregatedReaction.forEach((reaction) => {
|
||||||
if (reaction.type !== "m.reaction") return;
|
if (reaction.type !== 'm.reaction') return;
|
||||||
addReaction(reaction.key, undefined, reaction.count, undefined, false);
|
addReaction(reaction.key, undefined, reaction.count, undefined, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="message__reactions text text-b3 noselect">
|
<div className="message__reactions text text-b3 noselect">
|
||||||
{Object.keys(reactions).map((key) => (
|
{
|
||||||
|
Object.keys(reactions).map((key) => (
|
||||||
<MessageReaction
|
<MessageReaction
|
||||||
key={key}
|
key={key}
|
||||||
reaction={key}
|
reaction={key}
|
||||||
|
@ -536,16 +478,11 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||||
users={reactions[key].users}
|
users={reactions[key].users}
|
||||||
isActive={reactions[key].isActive}
|
isActive={reactions[key].isActive}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toggleEmoji(
|
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
|
||||||
roomId,
|
|
||||||
mEvent.getId(),
|
|
||||||
key,
|
|
||||||
reactions[key].shortcode,
|
|
||||||
roomTimeline
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
{canSendReaction && (
|
{canSendReaction && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -566,11 +503,11 @@ MessageReactionGroup.propTypes = {
|
||||||
|
|
||||||
function isMedia(mE) {
|
function isMedia(mE) {
|
||||||
return (
|
return (
|
||||||
mE.getContent()?.msgtype === "m.file" ||
|
mE.getContent()?.msgtype === 'm.file'
|
||||||
mE.getContent()?.msgtype === "m.image" ||
|
|| mE.getContent()?.msgtype === 'm.image'
|
||||||
mE.getContent()?.msgtype === "m.audio" ||
|
|| mE.getContent()?.msgtype === 'm.audio'
|
||||||
mE.getContent()?.msgtype === "m.video" ||
|
|| mE.getContent()?.msgtype === 'm.video'
|
||||||
mE.getType() === "m.sticker"
|
|| mE.getType() === 'm.sticker'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,20 +523,16 @@ function handleOpenViewSource(mEvent, roomTimeline) {
|
||||||
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
|
const MessageOptions = React.memo(({
|
||||||
|
roomTimeline, mEvent, edit, reply,
|
||||||
|
}) => {
|
||||||
const { roomId, room } = roomTimeline;
|
const { roomId, room } = roomTimeline;
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const senderId = mEvent.getSender();
|
const senderId = mEvent.getSender();
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
|
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
|
||||||
const canIRedact = room.currentState.hasSufficientPowerLevelFor(
|
const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
|
||||||
"redact",
|
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
||||||
myPowerlevel
|
|
||||||
);
|
|
||||||
const canSendReaction = room.currentState.maySendEvent(
|
|
||||||
"m.reaction",
|
|
||||||
mx.getUserId()
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="message__options">
|
<div className="message__options">
|
||||||
|
@ -617,7 +550,7 @@ const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
tooltip="Reply"
|
tooltip="Reply"
|
||||||
/>
|
/>
|
||||||
{senderId === mx.getUserId() && !isMedia(mEvent) && (
|
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => edit(true)}
|
onClick={() => edit(true)}
|
||||||
src={PencilIC}
|
src={PencilIC}
|
||||||
|
@ -631,9 +564,7 @@ const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
|
||||||
<MenuHeader>Options</MenuHeader>
|
<MenuHeader>Options</MenuHeader>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={TickMarkIC}
|
iconSrc={TickMarkIC}
|
||||||
onClick={() =>
|
onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))}
|
||||||
openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Read receipts
|
Read receipts
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -651,10 +582,10 @@ const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
|
||||||
iconSrc={BinIC}
|
iconSrc={BinIC}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const isConfirmed = await confirmDialog(
|
const isConfirmed = await confirmDialog(
|
||||||
"Delete message",
|
'Delete message',
|
||||||
"Are you sure that you want to delete this message?",
|
'Are you sure that you want to delete this message?',
|
||||||
"Delete",
|
'Delete',
|
||||||
"danger"
|
'danger',
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
redactEvent(roomId, mEvent.getId());
|
redactEvent(roomId, mEvent.getId());
|
||||||
|
@ -688,30 +619,28 @@ MessageOptions.propTypes = {
|
||||||
function genMediaContent(mE) {
|
function genMediaContent(mE) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const mContent = mE.getContent();
|
const mContent = mE.getContent();
|
||||||
if (!mContent || !mContent.body)
|
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||||
return <span style={{ color: "var(--bg-danger)" }}>Malformed event</span>;
|
|
||||||
|
|
||||||
let mediaMXC = mContent?.url;
|
let mediaMXC = mContent?.url;
|
||||||
const isEncryptedFile = typeof mediaMXC === "undefined";
|
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
||||||
if (isEncryptedFile) mediaMXC = mContent?.file?.url;
|
if (isEncryptedFile) mediaMXC = mContent?.file?.url;
|
||||||
|
|
||||||
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
||||||
|
|
||||||
if (typeof mediaMXC === "undefined" || mediaMXC === "")
|
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||||
return <span style={{ color: "var(--bg-danger)" }}>Malformed event</span>;
|
|
||||||
|
|
||||||
let msgType = mE.getContent()?.msgtype;
|
let msgType = mE.getContent()?.msgtype;
|
||||||
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
|
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
|
||||||
if (mE.getType() === "m.sticker") {
|
if (mE.getType() === 'm.sticker') {
|
||||||
msgType = "m.sticker";
|
msgType = 'm.sticker';
|
||||||
} else if (safeMimetype === "application/octet-stream") {
|
} else if (safeMimetype === 'application/octet-stream') {
|
||||||
msgType = "m.file";
|
msgType = 'm.file';
|
||||||
}
|
}
|
||||||
|
|
||||||
const blurhash = mContent?.info?.["xyz.amorgan.blurhash"];
|
const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
|
||||||
|
|
||||||
switch (msgType) {
|
switch (msgType) {
|
||||||
case "m.file":
|
case 'm.file':
|
||||||
return (
|
return (
|
||||||
<Media.File
|
<Media.File
|
||||||
name={mContent.body}
|
name={mContent.body}
|
||||||
|
@ -720,34 +649,30 @@ function genMediaContent(mE) {
|
||||||
file={mContent.file || null}
|
file={mContent.file || null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "m.image":
|
case 'm.image':
|
||||||
return (
|
return (
|
||||||
<Media.Image
|
<Media.Image
|
||||||
name={mContent.body}
|
name={mContent.body}
|
||||||
width={typeof mContent.info?.w === "number" ? mContent.info?.w : null}
|
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||||
height={
|
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||||
typeof mContent.info?.h === "number" ? mContent.info?.h : null
|
|
||||||
}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
file={isEncryptedFile ? mContent.file : null}
|
||||||
type={mContent.info?.mimetype}
|
type={mContent.info?.mimetype}
|
||||||
blurhash={blurhash}
|
blurhash={blurhash}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "m.sticker":
|
case 'm.sticker':
|
||||||
return (
|
return (
|
||||||
<Media.Sticker
|
<Media.Sticker
|
||||||
name={mContent.body}
|
name={mContent.body}
|
||||||
width={typeof mContent.info?.w === "number" ? mContent.info?.w : null}
|
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||||
height={
|
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||||
typeof mContent.info?.h === "number" ? mContent.info?.h : null
|
|
||||||
}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
file={isEncryptedFile ? mContent.file : null}
|
||||||
type={mContent.info?.mimetype}
|
type={mContent.info?.mimetype}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "m.audio":
|
case 'm.audio':
|
||||||
return (
|
return (
|
||||||
<Media.Audio
|
<Media.Audio
|
||||||
name={mContent.body}
|
name={mContent.body}
|
||||||
|
@ -756,38 +681,34 @@ function genMediaContent(mE) {
|
||||||
file={mContent.file || null}
|
file={mContent.file || null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "m.video":
|
case 'm.video':
|
||||||
if (typeof thumbnailMXC === "undefined") {
|
if (typeof thumbnailMXC === 'undefined') {
|
||||||
thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
|
thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Media.Video
|
<Media.Video
|
||||||
name={mContent.body}
|
name={mContent.body}
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||||
thumbnail={
|
thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
|
||||||
thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)
|
|
||||||
}
|
|
||||||
thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
|
thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
|
||||||
thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
|
thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
|
||||||
width={typeof mContent.info?.w === "number" ? mContent.info?.w : null}
|
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||||
height={
|
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||||
typeof mContent.info?.h === "number" ? mContent.info?.h : null
|
|
||||||
}
|
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
file={isEncryptedFile ? mContent.file : null}
|
||||||
type={mContent.info?.mimetype}
|
type={mContent.info?.mimetype}
|
||||||
blurhash={blurhash}
|
blurhash={blurhash}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return <span style={{ color: "var(--bg-danger)" }}>Malformed event</span>;
|
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditedBody(editedMEvent) {
|
function getEditedBody(editedMEvent) {
|
||||||
const newContent = editedMEvent.getContent()["m.new_content"];
|
const newContent = editedMEvent.getContent()['m.new_content'];
|
||||||
if (typeof newContent === "undefined") return [null, false, null];
|
if (typeof newContent === 'undefined') return [null, false, null];
|
||||||
|
|
||||||
const isCustomHTML = newContent.format === "org.matrix.custom.html";
|
const isCustomHTML = newContent.format === 'org.matrix.custom.html';
|
||||||
const parsedContent = parseReply(newContent.body);
|
const parsedContent = parseReply(newContent.body);
|
||||||
if (parsedContent === null) {
|
if (parsedContent === null) {
|
||||||
return [newContent.body, isCustomHTML, newContent.formatted_body ?? null];
|
return [newContent.body, isCustomHTML, newContent.formatted_body ?? null];
|
||||||
|
@ -796,39 +717,22 @@ function getEditedBody(editedMEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Message({
|
function Message({
|
||||||
mEvent,
|
mEvent, isBodyOnly, roomTimeline,
|
||||||
isBodyOnly,
|
focus, fullTime, isEdit, setEdit, cancelEdit,
|
||||||
roomTimeline,
|
|
||||||
focus,
|
|
||||||
fullTime,
|
|
||||||
isEdit,
|
|
||||||
setEdit,
|
|
||||||
cancelEdit,
|
|
||||||
}) {
|
}) {
|
||||||
const roomId = mEvent.getRoomId();
|
const roomId = mEvent.getRoomId();
|
||||||
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
|
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
|
||||||
|
|
||||||
const className = [
|
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
|
||||||
"message",
|
if (focus) className.push('message--focus');
|
||||||
isBodyOnly ? "message--body-only" : "message--full",
|
|
||||||
];
|
|
||||||
if (focus) className.push("message--focus");
|
|
||||||
const content = mEvent.getContent();
|
const content = mEvent.getContent();
|
||||||
const eventId = mEvent.getId();
|
const eventId = mEvent.getId();
|
||||||
const msgType = content?.msgtype;
|
const msgType = content?.msgtype;
|
||||||
const senderId = mEvent.getSender();
|
const senderId = mEvent.getSender();
|
||||||
let { body } = content;
|
let { body } = content;
|
||||||
const username = mEvent.sender
|
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
|
||||||
? getUsernameOfRoomMember(mEvent.sender)
|
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
|
||||||
: getUsername(senderId);
|
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
||||||
const avatarSrc =
|
|
||||||
mEvent.sender?.getAvatarUrl(
|
|
||||||
initMatrix.matrixClient.baseUrl,
|
|
||||||
36,
|
|
||||||
36,
|
|
||||||
"crop"
|
|
||||||
) ?? null;
|
|
||||||
let isCustomHTML = content.format === "org.matrix.custom.html";
|
|
||||||
let customHTML = isCustomHTML ? content.formatted_body : null;
|
let customHTML = isCustomHTML ? content.formatted_body : null;
|
||||||
|
|
||||||
const edit = useCallback(() => {
|
const edit = useCallback(() => {
|
||||||
|
@ -838,12 +742,11 @@ function Message({
|
||||||
replyTo(senderId, mEvent.getId(), body, customHTML);
|
replyTo(senderId, mEvent.getId(), body, customHTML);
|
||||||
}, [body, customHTML]);
|
}, [body, customHTML]);
|
||||||
|
|
||||||
if (msgType === "m.emote") className.push("message--type-emote");
|
if (msgType === 'm.emote') className.push('message--type-emote');
|
||||||
|
|
||||||
const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
|
const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
|
||||||
const haveReactions = roomTimeline
|
const haveReactions = roomTimeline
|
||||||
? reactionTimeline.has(eventId) ||
|
? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
|
||||||
!!mEvent.getServerAggregatedRelation("m.annotation")
|
|
||||||
: false;
|
: false;
|
||||||
const isReply = !!mEvent.replyEventId;
|
const isReply = !!mEvent.replyEventId;
|
||||||
|
|
||||||
|
@ -858,20 +761,22 @@ function Message({
|
||||||
customHTML = trimHTMLReply(customHTML);
|
customHTML = trimHTMLReply(customHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof body !== "string") body = "";
|
if (typeof body !== 'string') body = '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className.join(" ")}>
|
<div className={className.join(' ')}>
|
||||||
{isBodyOnly ? (
|
{
|
||||||
<div className="message__avatar-container" />
|
isBodyOnly
|
||||||
) : (
|
? <div className="message__avatar-container" />
|
||||||
|
: (
|
||||||
<MessageAvatar
|
<MessageAvatar
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
avatarSrc={avatarSrc}
|
avatarSrc={avatarSrc}
|
||||||
userId={senderId}
|
userId={senderId}
|
||||||
username={username}
|
username={username}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
<div className="message__main-container">
|
<div className="message__main-container">
|
||||||
{!isBodyOnly && (
|
{!isBodyOnly && (
|
||||||
<MessageHeader
|
<MessageHeader
|
||||||
|
@ -891,27 +796,19 @@ function Message({
|
||||||
<MessageBody
|
<MessageBody
|
||||||
senderName={username}
|
senderName={username}
|
||||||
isCustomHTML={isCustomHTML}
|
isCustomHTML={isCustomHTML}
|
||||||
body={
|
body={isMedia(mEvent) ? genMediaContent(mEvent) : customHTML ?? body}
|
||||||
isMedia(mEvent) ? genMediaContent(mEvent) : customHTML ?? body
|
|
||||||
}
|
|
||||||
msgType={msgType}
|
msgType={msgType}
|
||||||
isEdited={isEdited}
|
isEdited={isEdited}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isEdit && (
|
{isEdit && (
|
||||||
<MessageEdit
|
<MessageEdit
|
||||||
body={
|
body={(customHTML
|
||||||
customHTML
|
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
|
||||||
? html(customHTML, { kind: "edit", onlyPlain: true }).plain
|
: plain(body, { kind: 'edit', onlyPlain: true }).plain)}
|
||||||
: plain(body, { kind: "edit", onlyPlain: true }).plain
|
|
||||||
}
|
|
||||||
onSave={(newBody, oldBody) => {
|
onSave={(newBody, oldBody) => {
|
||||||
if (newBody !== oldBody) {
|
if (newBody !== oldBody) {
|
||||||
initMatrix.roomsInput.sendEditedMessage(
|
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
||||||
roomId,
|
|
||||||
mEvent,
|
|
||||||
newBody
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
cancelEdit();
|
cancelEdit();
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use "../../atoms/scroll/scrollbar";
|
@use '../../atoms/scroll/scrollbar';
|
||||||
@use "../../partials/text";
|
@use '../../partials/text';
|
||||||
@use "../../partials/dir";
|
@use '../../partials/dir';
|
||||||
@use "../../partials/screen";
|
@use '../../partials/screen';
|
||||||
|
|
||||||
.message,
|
.message,
|
||||||
.ph-msg {
|
.ph-msg {
|
||||||
|
@ -449,11 +449,11 @@
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
[dir="rtl"] & {
|
[dir='rtl'] & {
|
||||||
border-width: 0 1px 1px 0;
|
border-width: 0 1px 1px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[dir="rtl"] &:first-child {
|
[dir='rtl'] &:first-child {
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue