Compare commits

..

1 commit

Author SHA1 Message Date
dependabot[bot]
5738dfc4f6
Bump eslint-config-prettier from 8.5.0 to 8.8.0
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.5.0 to 8.8.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.5.0...v8.8.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 20:05:30 +00:00
293 changed files with 7004 additions and 10262 deletions

View file

@ -9,21 +9,24 @@ module.exports = {
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"airbnb",
"prettier",
'airbnb',
'prettier',
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: "latest",
sourceType: "module",
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ["react", "@typescript-eslint"],
plugins: [
'react',
'@typescript-eslint'
],
rules: {
"linebreak-style": 0,
"no-underscore-dangle": 0,
'linebreak-style': 0,
'no-underscore-dangle': 0,
"import/prefer-default-export": "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": [
"error",
{

View file

@ -1,9 +1,9 @@
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change

View file

@ -1,6 +1,9 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":dependencyDashboardApproval"],
"extends": [
"config:base",
":dependencyDashboardApproval"
],
"labels": [ "Dependencies" ],
"packageRules": [
{

View file

@ -2,7 +2,7 @@ name: Build pull request
on:
pull_request:
types: ["opened", "synchronize"]
types: ['opened', 'synchronize']
jobs:
build-pull-request:
@ -21,8 +21,6 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build app
env:
NODE_OPTIONS: "--max_old_space_size=4096"
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v3.1.1

View file

@ -1,4 +1,4 @@
name: "CLA Assistant"
name: 'CLA Assistant'
on:
issue_comment:
types: [created]
@ -9,7 +9,7 @@ jobs:
CLAssistant:
runs-on: ubuntu-latest
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'
# Beta Release
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
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
with:
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-signatures: 'signatures.json'
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: "main"
branch: 'main'
allowlist: ajbura,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken

View file

@ -1,10 +1,10 @@
name: "Docker check"
name: 'Docker check'
on:
pull_request:
paths:
- "Dockerfile"
- ".github/workflows/docker-pr.yml"
- 'Dockerfile'
- '.github/workflows/docker-pr.yml'
jobs:
docker-build:

View file

@ -3,7 +3,7 @@ name: NPM Lockfile Changes
on:
pull_request:
paths:
- "package-lock.json"
- 'package-lock.json'
jobs:
lockfile_changes:

View file

@ -20,8 +20,6 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build app
env:
NODE_OPTIONS: "--max_old_space_size=4096"
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@5da65c9f74c7961c5501a3ba329b8d0912f39c03
@ -32,7 +30,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
production-deploy: true
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:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_DEV }}

View file

@ -19,8 +19,6 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build app
env:
NODE_OPTIONS: "--max_old_space_size=4096"
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@5da65c9f74c7961c5501a3ba329b8d0912f39c03
@ -31,7 +29,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
production-deploy: true
github-deployment-environment: stable
github-deployment-description: "Stable deployment on each release"
github-deployment-description: 'Stable deployment on each release'
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}

View file

@ -1,8 +1,4 @@
{
"tabWidth": 2,
"semi": true,
"useTabs": false,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 80
"printWidth": 100,
"singleQuote": true
}

View file

@ -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. 🎉
> 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
> - Tweet about it (tag @cinnyapp)
> - 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
> ### 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.
**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.
@ -29,7 +27,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
Example:
|Not ideal|Better|
| ----------------------------------- | --------------------------------------------- |
|---|----|
|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.
@ -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).**
## Helpful links
- [BEM methodology](http://getbem.com/introduction/)
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)

View file

@ -6,7 +6,6 @@ WORKDIR /src
COPY .npmrc package.json package-lock.json /src/
RUN npm ci
COPY . /src/
ENV NODE_OPTIONS=--max_old_space_size=4096
RUN npm run build

View file

@ -1,4 +0,0 @@
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}

View file

@ -18,10 +18,7 @@
<meta property="og:title" content="Cinny" />
<meta property="og:url" content="https://cinny.in" />
<meta
property="og:image"
content="https://cinny.in/assets/favicon-48x48.png"
/>
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
<meta
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."
@ -35,10 +32,7 @@
<meta name="application-name" 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-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link
rel="apple-touch-icon"
@ -102,6 +96,6 @@
<audio id="inviteSound">
<source src="./public/sound/invite.ogg" type="audio/ogg" />
</audio>
<script type="module" src="./src/index.tsx"></script>
<script type="module" src="./src/index.jsx"></script>
</body>
</html>

92
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "cinny",
"version": "2.2.6",
"version": "2.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
"version": "2.2.6",
"version": "2.2.4",
"license": "AGPL-3.0-only",
"dependencies": {
"@fontsource/inter": "4.5.14",
@ -14,7 +14,6 @@
"@khanacademy/simple-markdown": "0.8.6",
"@matrix-org/olm": "3.2.14",
"@tippyjs/react": "4.2.6",
"@types/flux": "3.1.11",
"blurhash": "2.0.4",
"browser-encrypt-attachment": "0.3.0",
"dateformat": "5.0.3",
@ -26,7 +25,7 @@
"katex": "0.16.4",
"linkify-html": "4.0.2",
"linkifyjs": "4.0.2",
"matrix-js-sdk": "24.0.0",
"matrix-js-sdk": "22.0.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-autosize-textarea": "7.1.0",
@ -44,7 +43,6 @@
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1",
"@types/dateformat": "5.0.0",
"@types/node": "18.11.18",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
@ -54,7 +52,7 @@
"buffer": "6.0.3",
"eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.5.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11",
@ -960,14 +958,6 @@
"react-dom": "16.14.0"
}
},
"node_modules/@matrix-org/matrix-sdk-crypto-js": {
"version": "0.1.0-alpha.5",
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz",
"integrity": "sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@matrix-org/olm": {
"version": "3.2.14",
"resolved": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
@ -1117,12 +1107,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
@ -1134,20 +1118,6 @@
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"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": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@ -1200,6 +1170,11 @@
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"node_modules/@types/sdp-transform": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/@types/sdp-transform/-/sdp-transform-2.4.5.tgz",
"integrity": "sha512-GVO0gnmbyO3Oxm2HdPsYUNcyihZE3GyCY8ysMYHuQGfLhGZq89Nm4lSzULWTzZoyHtg+VO/IdrnxZHPnPSGnAg=="
},
"node_modules/@types/semver": {
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
@ -1818,7 +1793,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
@ -2387,9 +2361,9 @@
}
},
"node_modules/eslint-config-prettier": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
@ -3051,8 +3025,7 @@
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/function.prototype.name": {
"version": "1.1.5",
@ -3094,7 +3067,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
"integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@ -3208,7 +3180,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@ -3250,7 +3221,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -3892,12 +3862,12 @@
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
},
"node_modules/matrix-js-sdk": {
"version": "24.0.0",
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-24.0.0.tgz",
"integrity": "sha512-AOhO036ziDf6lwYoauj5DES/RJ6RDTq+vrK2yO/GW/8n+bAXhkjWc9AA/WcTK/9SkNHS46ZanmolkhS1n8WniQ==",
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-22.0.0.tgz",
"integrity": "sha512-mpKqeD3nCobjGiUiATUyEoP44n+AzDW5cSeBTIBY5fPhj0AkzLJhblHt40vzSOJazj8tT0PhsSzhEIR9hGzYGA==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
"@types/sdp-transform": "^2.4.5",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
@ -3905,9 +3875,9 @@
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.0.0",
"p-retry": "4",
"qs": "^6.9.6",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "9"
"unhomoglyph": "^1.0.6"
},
"engines": {
"node": ">=16.0.0"
@ -4049,7 +4019,6 @@
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -4360,6 +4329,20 @@
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -4787,7 +4770,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
@ -5157,14 +5139,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vite": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.1.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "cinny",
"version": "2.2.6",
"version": "2.2.4",
"description": "Yet another matrix client",
"main": "index.js",
"engines": {
@ -9,7 +9,7 @@
"scripts": {
"start": "vite",
"build": "vite build",
"lint": "npm run check:eslint && npm run check:prettier",
"lint": "yarn check:eslint && yarn check:prettier",
"check:eslint": "eslint src/*",
"check:prettier": "prettier --check .",
"fix:prettier": "prettier --write .",
@ -24,7 +24,6 @@
"@khanacademy/simple-markdown": "0.8.6",
"@matrix-org/olm": "3.2.14",
"@tippyjs/react": "4.2.6",
"@types/flux": "3.1.11",
"blurhash": "2.0.4",
"browser-encrypt-attachment": "0.3.0",
"dateformat": "5.0.3",
@ -36,7 +35,7 @@
"katex": "0.16.4",
"linkify-html": "4.0.2",
"linkifyjs": "4.0.2",
"matrix-js-sdk": "24.0.0",
"matrix-js-sdk": "22.0.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-autosize-textarea": "7.1.0",
@ -54,7 +53,6 @@
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1",
"@types/dateformat": "5.0.0",
"@types/node": "18.11.18",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
@ -64,7 +62,7 @@
"buffer": "6.0.3",
"eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.5.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11",

View file

@ -1,69 +1,64 @@
import React from "react";
import PropTypes from "prop-types";
import "./Avatar.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Avatar.scss';
import { twemojify } from "../../../util/twemojify";
import { twemojify } from '../../../util/twemojify';
import Text from "../text/Text";
import RawIcon from "../system-icons/RawIcon";
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
import ImageBrokenSVG from "../../../../public/res/svg/image-broken.svg";
import { avatarInitials } from "../../../util/common";
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
import { avatarInitials } from '../../../util/common';
const Avatar = React.forwardRef(
({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
let textSize = "s1";
if (size === "large") textSize = "h1";
if (size === "small") textSize = "b1";
if (size === "extra-small") textSize = "b3";
const Avatar = React.forwardRef(({
text, bgColor, iconSrc, iconColor, imageSrc, size,
}, ref) => {
let textSize = 's1';
if (size === 'large') textSize = 'h1';
if (size === 'small') textSize = 'b1';
if (size === 'extra-small') textSize = 'b3';
return (
<div
ref={ref}
className={`avatar-container avatar-container__${size} noselect`}
>
{imageSrc !== null ? (
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
{
imageSrc !== null
? (
<img
draggable="false"
src={imageSrc}
onLoad={(e) => {
e.target.style.backgroundColor = "transparent";
}}
onError={(e) => {
e.target.src = ImageBrokenSVG;
}}
onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
onError={(e) => { e.target.src = ImageBrokenSVG; }}
alt=""
/>
) : (
)
: (
<span
style={{
backgroundColor: iconSrc === null ? bgColor : "transparent",
}}
className={`avatar__border${iconSrc !== null ? "--active" : ""}`}
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
>
{iconSrc !== null ? (
<RawIcon size={size} src={iconSrc} color={iconColor} />
) : (
text !== null && (
{
iconSrc !== null
? <RawIcon size={size} src={iconSrc} color={iconColor} />
: text !== null && (
<Text variant={textSize} primary>
{twemojify(avatarInitials(text))}
</Text>
)
)}
}
</span>
)}
)
}
</div>
);
}
);
});
Avatar.defaultProps = {
text: null,
bgColor: "transparent",
bgColor: 'transparent',
iconSrc: null,
iconColor: null,
imageSrc: null,
size: "normal",
size: 'normal',
};
Avatar.propTypes = {
@ -72,7 +67,7 @@ Avatar.propTypes = {
iconSrc: PropTypes.string,
iconColor: PropTypes.string,
imageSrc: PropTypes.string,
size: PropTypes.oneOf(["large", "normal", "small", "extra-small"]),
size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']),
};
export default Avatar;

View file

@ -1,4 +1,4 @@
@use "../../partials/flex";
@use '../../partials/flex';
.avatar-container {
display: inline-flex;

View file

@ -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
export default async function renderAvatar({
text,
bgColor,
imageSrc,
size,
borderRadius,
scale,
text, bgColor, imageSrc, size, borderRadius, scale,
}) {
try {
const canvas = document.createElement("canvas");
const canvas = document.createElement('canvas');
canvas.width = size * scale;
canvas.height = size * scale;
const ctx = canvas.getContext("2d");
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
@ -32,7 +27,7 @@ export default async function renderAvatar({
ctx.clip();
const img = new Image();
img.crossOrigin = "anonymous";
img.crossOrigin = 'anonymous';
const promise = new Promise((resolve, reject) => {
img.onerror = reject;
img.onload = resolve;
@ -47,10 +42,10 @@ export default async function renderAvatar({
ctx.fill();
// centered letter
ctx.fillStyle = "#fff";
ctx.font = `${cssVar("--fs-s1")} ${cssVar("--font-primary")}`;
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = '#fff';
ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(avatarInitials(text), size / 2, size / 2);
}

View file

@ -1,18 +1,14 @@
import React from "react";
import PropTypes from "prop-types";
import "./NotificationBadge.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './NotificationBadge.scss';
import Text from "../text/Text";
import Text from '../text/Text';
function NotificationBadge({ alert, content }) {
const notificationClass = alert ? " notification-badge--alert" : "";
const notificationClass = alert ? ' notification-badge--alert' : '';
return (
<div className={`notification-badge${notificationClass}`}>
{content !== null && (
<Text variant="b3" weight="bold">
{content}
</Text>
)}
{content !== null && <Text variant="b3" weight="bold">{content}</Text>}
</div>
);
}
@ -24,7 +20,10 @@ NotificationBadge.defaultProps = {
NotificationBadge.propTypes = {
alert: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
content: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
export default NotificationBadge;

View file

@ -1,24 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import "./Button.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Button.scss';
import Text from "../text/Text";
import RawIcon from "../system-icons/RawIcon";
import { blurOnBubbling } from "./script";
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
import { blurOnBubbling } from './script';
const Button = React.forwardRef(
(
{ id, className, variant, iconSrc, type, onClick, children, disabled },
ref
) => {
const iconClass = iconSrc === null ? "" : `btn-${variant}--icon`;
const Button = React.forwardRef(({
id, className, variant, iconSrc,
type, onClick, children, disabled,
}, ref) => {
const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`;
return (
<button
ref={ref}
id={id === "" ? undefined : id}
className={`${
className ? `${className} ` : ""
}btn-${variant} ${iconClass} noselect`}
id={id === '' ? undefined : id}
className={`${className ? `${className} ` : ''}btn-${variant} ${iconClass} noselect`}
onMouseUp={(e) => blurOnBubbling(e, `.btn-${variant}`)}
onClick={onClick}
// eslint-disable-next-line react/button-has-type
@ -26,19 +23,18 @@ const Button = React.forwardRef(
disabled={disabled}
>
{iconSrc !== null && <RawIcon size="small" src={iconSrc} />}
{typeof children === "string" && <Text variant="b1">{children}</Text>}
{typeof children !== "string" && children}
{typeof children === 'string' && <Text variant="b1">{ children }</Text>}
{typeof children !== 'string' && children }
</button>
);
}
);
});
Button.defaultProps = {
id: "",
id: '',
className: null,
variant: "surface",
variant: 'surface',
iconSrc: null,
type: "button",
type: 'button',
onClick: null,
disabled: false,
};
@ -46,15 +42,9 @@ Button.defaultProps = {
Button.propTypes = {
id: PropTypes.string,
className: PropTypes.string,
variant: PropTypes.oneOf([
"surface",
"primary",
"positive",
"caution",
"danger",
]),
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
iconSrc: PropTypes.string,
type: PropTypes.oneOf(["button", "submit", "reset"]),
type: PropTypes.oneOf(['button', 'submit', 'reset']),
onClick: PropTypes.func,
children: PropTypes.node.isRequired,
disabled: PropTypes.bool,

View file

@ -1,6 +1,6 @@
@use "state";
@use "../../partials/dir";
@use "../../partials/text";
@use 'state';
@use '../../partials/dir';
@use '../../partials/text';
.btn-surface,
.btn-primary,
@ -25,6 +25,7 @@
&--icon {
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
}
.ic-raw {
@include dir.side(margin, 0, var(--sp-extra-tight));
@ -41,6 +42,7 @@
}
}
.btn-surface {
box-shadow: var(--bs-surface-border);
@include color(var(--tc-surface-high), var(--ic-surface-normal));

View file

@ -1,11 +1,12 @@
import React from "react";
import PropTypes from "prop-types";
import "./Checkbox.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Checkbox.scss';
function Checkbox({ variant, isActive, onToggle, disabled, tabIndex }) {
const className = `checkbox checkbox-${variant}${
isActive ? " checkbox--active" : ""
}`;
function Checkbox({
variant, isActive, onToggle,
disabled, tabIndex,
}) {
const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`;
if (onToggle === null) return <span className={className} />;
return (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
@ -20,7 +21,7 @@ function Checkbox({ variant, isActive, onToggle, disabled, tabIndex }) {
}
Checkbox.defaultProps = {
variant: "primary",
variant: 'primary',
isActive: false,
onToggle: null,
disabled: false,
@ -28,7 +29,7 @@ Checkbox.defaultProps = {
};
Checkbox.propTypes = {
variant: PropTypes.oneOf(["primary", "positive", "caution", "danger"]),
variant: PropTypes.oneOf(['primary', 'positive', 'caution', 'danger']),
isActive: PropTypes.bool,
onToggle: PropTypes.func,
disabled: PropTypes.bool,

View file

@ -1,5 +1,5 @@
@use "../../partials/flex";
@use "./state";
@use '../../partials/flex';
@use './state';
.checkbox {
width: 20px;

View file

@ -1,29 +1,18 @@
import React from "react";
import PropTypes from "prop-types";
import "./IconButton.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './IconButton.scss';
import RawIcon from "../system-icons/RawIcon";
import Tooltip from "../tooltip/Tooltip";
import { blurOnBubbling } from "./script";
import Text from "../text/Text";
import RawIcon from '../system-icons/RawIcon';
import Tooltip from '../tooltip/Tooltip';
import { blurOnBubbling } from './script';
import Text from '../text/Text';
const IconButton = React.forwardRef(
(
{
variant,
size,
type,
tooltip,
tooltipPlacement,
src,
onClick,
tabIndex,
disabled,
isImage,
const IconButton = React.forwardRef(({
variant, size, type,
tooltip, tooltipPlacement, src,
onClick, tabIndex, disabled, isImage,
className,
},
ref
) => {
}, ref) => {
const btn = (
<button
ref={ref}
@ -47,34 +36,27 @@ const IconButton = React.forwardRef(
{btn}
</Tooltip>
);
}
);
});
IconButton.defaultProps = {
variant: "surface",
size: "normal",
type: "button",
variant: 'surface',
size: 'normal',
type: 'button',
tooltip: null,
tooltipPlacement: "top",
tooltipPlacement: 'top',
onClick: null,
tabIndex: 0,
disabled: false,
isImage: false,
className: "",
className: '',
};
IconButton.propTypes = {
variant: PropTypes.oneOf([
"surface",
"primary",
"positive",
"caution",
"danger",
]),
size: PropTypes.oneOf(["normal", "small", "extra-small"]),
type: PropTypes.oneOf(["button", "submit", "reset"]),
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
size: PropTypes.oneOf(['normal', 'small', 'extra-small']),
type: PropTypes.oneOf(['button', 'submit', 'reset']),
tooltip: PropTypes.string,
tooltipPlacement: PropTypes.oneOf(["top", "right", "bottom", "left"]),
tooltipPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
src: PropTypes.string.isRequired,
onClick: PropTypes.func,
tabIndex: PropTypes.number,

View file

@ -1,4 +1,4 @@
@use "state";
@use 'state';
.ic-btn {
padding: var(--sp-extra-tight);

View file

@ -1,17 +1,14 @@
import React from "react";
import PropTypes from "prop-types";
import "./RadioButton.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './RadioButton.scss';
function RadioButton({ isActive, onToggle, disabled }) {
if (onToggle === null)
return (
<span className={`radio-btn${isActive ? " radio-btn--active" : ""}`} />
);
if (onToggle === null) return <span className={`radio-btn${isActive ? ' radio-btn--active' : ''}`} />;
return (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button
onClick={() => onToggle(!isActive)}
className={`radio-btn${isActive ? " radio-btn--active" : ""}`}
className={`radio-btn${isActive ? ' radio-btn--active' : ''}`}
type="button"
disabled={disabled}
/>

View file

@ -1,5 +1,5 @@
@use "../../partials/flex";
@use "./state";
@use '../../partials/flex';
@use './state';
.radio-btn {
@extend .cp-fx__row--c-c;
@ -12,7 +12,7 @@
@include state.disabled;
&::before {
content: "";
content: '';
display: inline-block;
width: 12px;
height: 12px;

View file

@ -1,9 +1,9 @@
import React from "react";
import PropTypes from "prop-types";
import "./Toggle.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Toggle.scss';
function Toggle({ isActive, onToggle, disabled }) {
const className = `toggle${isActive ? " toggle--active" : ""}`;
const className = `toggle${isActive ? ' toggle--active' : ''}`;
if (onToggle === null) return <span className={className} />;
return (
// eslint-disable-next-line jsx-a11y/control-has-associated-label

View file

@ -1,5 +1,5 @@
@use "../../partials/dir";
@use "./state";
@use '../../partials/dir';
@use './state';
.toggle {
width: 44px;
@ -16,13 +16,14 @@
transition: background 200ms ease-in-out;
&::before {
content: "";
content: '';
display: inline-block;
width: 16px;
height: 16px;
background-color: var(--tc-surface-low);
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;
}

View file

@ -1,3 +1,4 @@
@mixin hover($color) {
@media (hover: hover) {
&:hover {

View file

@ -10,10 +10,7 @@ function blurOnBubbling(e, selector) {
for (let elIndex = 0; elIndex < bubblingPath.length; elIndex += 1) {
if (bubblingPath[elIndex] === document) {
console.warn(
blurOnBubbling,
"blurOnBubbling: not found selector in bubbling path"
);
console.warn(blurOnBubbling, 'blurOnBubbling: not found selector in bubbling path');
break;
}
if (bubblingPath[elIndex].matches(selector)) {

View file

@ -1,28 +1,24 @@
import React from "react";
import PropTypes from "prop-types";
import "./InfoCard.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './InfoCard.scss';
import Text from "../text/Text";
import RawIcon from "../system-icons/RawIcon";
import IconButton from "../button/IconButton";
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
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({
className,
style,
variant,
iconSrc,
title,
content,
rounded,
requestClose,
className, style,
variant, iconSrc,
title, content,
rounded, requestClose,
}) {
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);
return (
<div className={classes.join(" ")} style={style}>
<div className={classes.join(' ')} style={style}>
{iconSrc && (
<div className="info-card__icon">
<RawIcon color={`var(--ic-${variant}-high)`} src={iconSrc} />
@ -42,7 +38,7 @@ function InfoCard({
InfoCard.defaultProps = {
className: null,
style: null,
variant: "surface",
variant: 'surface',
iconSrc: null,
content: null,
rounded: false,
@ -52,13 +48,7 @@ InfoCard.defaultProps = {
InfoCard.propTypes = {
className: PropTypes.string,
style: PropTypes.shape({}),
variant: PropTypes.oneOf([
"surface",
"primary",
"positive",
"caution",
"danger",
]),
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
iconSrc: PropTypes.string,
title: PropTypes.string.isRequired,
content: PropTypes.node,

View file

@ -1,5 +1,5 @@
@use ".././../partials/flex";
@use ".././../partials/dir";
@use '.././../partials/flex';
@use '.././../partials/dir';
.info-card {
display: flex;
@ -34,6 +34,7 @@
&--surface {
border-color: var(--bg-surface-border);
background-color: var(--bg-surface-hover);
}
&--primary {
border-color: var(--bg-primary);

View file

@ -1,17 +1,18 @@
import React from "react";
import PropTypes from "prop-types";
import "./Chip.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Chip.scss';
import Text from "../text/Text";
import RawIcon from "../system-icons/RawIcon";
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
function Chip({ iconSrc, iconColor, text, children, onClick }) {
function Chip({
iconSrc, iconColor, text, children,
onClick,
}) {
return (
<button className="chip" type="button" onClick={onClick}>
{iconSrc != null && (
<RawIcon src={iconSrc} color={iconColor} size="extra-small" />
)}
{text != null && text !== "" && <Text variant="b3">{text}</Text>}
{iconSrc != null && <RawIcon src={iconSrc} color={iconColor} size="extra-small" />}
{(text != null && text !== '') && <Text variant="b3">{text}</Text>}
{children}
</button>
);

View file

@ -1,4 +1,4 @@
@use "../../partials/dir";
@use '../../partials/dir';
.chip {
padding: var(--sp-ultra-tight) var(--sp-extra-tight);

View file

@ -1,15 +1,17 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import "./ContextMenu.scss";
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './ContextMenu.scss';
import Tippy from "@tippyjs/react";
import "tippy.js/animations/scale-extreme.css";
import Tippy from '@tippyjs/react';
import 'tippy.js/animations/scale-extreme.css';
import Text from "../text/Text";
import Button from "../button/Button";
import ScrollView from "../scroll/ScrollView";
import Text from '../text/Text';
import Button from '../button/Button';
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 showMenu = () => setVisibility(true);
const hideMenu = () => setVisibility(false);
@ -24,11 +26,7 @@ function ContextMenu({ content, placement, maxWidth, render, afterToggle }) {
className="context-menu"
visible={isVisible}
onClickOutside={hideMenu}
content={
<ScrollView invisible>
{typeof content === "function" ? content(hideMenu) : content}
</ScrollView>
}
content={<ScrollView invisible>{typeof content === 'function' ? content(hideMenu) : content}</ScrollView>}
placement={placement}
interactive
arrow={false}
@ -41,15 +39,21 @@ function ContextMenu({ content, placement, maxWidth, render, afterToggle }) {
}
ContextMenu.defaultProps = {
maxWidth: "unset",
placement: "right",
maxWidth: 'unset',
placement: 'right',
afterToggle: null,
};
ContextMenu.propTypes = {
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
placement: PropTypes.oneOf(["top", "right", "bottom", "left"]),
maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
content: PropTypes.oneOfType([
PropTypes.node,
PropTypes.func,
]).isRequired,
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
maxWidth: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
render: PropTypes.func.isRequired,
afterToggle: PropTypes.func,
};
@ -66,7 +70,10 @@ MenuHeader.propTypes = {
children: PropTypes.node.isRequired,
};
function MenuItem({ variant, iconSrc, type, onClick, children, disabled }) {
function MenuItem({
variant, iconSrc, type,
onClick, children, disabled,
}) {
return (
<div className="context-menu__item">
<Button
@ -83,26 +90,26 @@ function MenuItem({ variant, iconSrc, type, onClick, children, disabled }) {
}
MenuItem.defaultProps = {
variant: "surface",
variant: 'surface',
iconSrc: null,
type: "button",
type: 'button',
disabled: false,
onClick: null,
};
MenuItem.propTypes = {
variant: PropTypes.oneOf(["surface", "positive", "caution", "danger"]),
variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']),
iconSrc: PropTypes.string,
type: PropTypes.oneOf(["button", "submit"]),
type: PropTypes.oneOf(['button', 'submit']),
onClick: PropTypes.func,
children: PropTypes.node.isRequired,
disabled: PropTypes.bool,
};
function MenuBorder() {
return (
<div style={{ borderBottom: "1px solid var(--bg-surface-border)" }}> </div>
);
return <div style={{ borderBottom: '1px solid var(--bg-surface-border)' }}> </div>;
}
export { ContextMenu as default, MenuHeader, MenuItem, MenuBorder };
export {
ContextMenu as default, MenuHeader, MenuItem, MenuBorder,
};

View file

@ -1,6 +1,6 @@
@use "../../partials/flex";
@use "../../partials/text";
@use "../../partials/dir";
@use '../../partials/flex';
@use '../../partials/text';
@use '../../partials/dir';
.context-menu {
background-color: var(--bg-surface);
@ -59,7 +59,11 @@
// if item doesn't have icon
.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 {

View file

@ -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 navigation from "../../../client/state/navigation";
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import ContextMenu from "./ContextMenu";
import ContextMenu from './ContextMenu';
let key = null;
function ReusableContextMenu() {
@ -29,20 +29,14 @@ function ReusableContextMenu() {
return;
}
setData({
placement,
cords,
render,
afterClose,
placement, cords, render, afterClose,
});
};
navigation.on(
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
handleContextMenuOpen
);
navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen);
return () => {
navigation.removeListener(
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
handleContextMenuOpen
handleContextMenuOpen,
);
};
}, [data]);
@ -65,24 +59,24 @@ function ReusableContextMenu() {
return (
<ContextMenu
afterToggle={handleAfterToggle}
placement={data?.placement || "right"}
content={data?.render(closeMenu) ?? ""}
placement={data?.placement || 'right'}
content={data?.render(closeMenu) ?? ''}
render={(toggleMenu) => (
<input
ref={openerRef}
onClick={toggleMenu}
type="button"
style={{
width: "32px",
height: "32px",
backgroundColor: "transparent",
position: "fixed",
width: '32px',
height: '32px',
backgroundColor: 'transparent',
position: 'fixed',
top: 0,
left: 0,
padding: 0,
border: "none",
visibility: "hidden",
appearance: "none",
border: 'none',
visibility: 'hidden',
appearance: 'none',
}}
/>
)}

View file

@ -1,38 +1,28 @@
import React from "react";
import PropTypes from "prop-types";
import "./Divider.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Divider.scss';
import Text from "../text/Text";
import Text from '../text/Text';
function Divider({ text, variant, align }) {
const dividerClass = ` divider--${variant} divider--${align}`;
return (
<div className={`divider${dividerClass}`}>
{text !== null && (
<Text className="divider__text" variant="b3" weight="bold">
{text}
</Text>
)}
{text !== null && <Text className="divider__text" variant="b3" weight="bold">{text}</Text>}
</div>
);
}
Divider.defaultProps = {
text: null,
variant: "surface",
align: "center",
variant: 'surface',
align: 'center',
};
Divider.propTypes = {
text: PropTypes.string,
variant: PropTypes.oneOf([
"surface",
"primary",
"positive",
"caution",
"danger",
]),
align: PropTypes.oneOf(["left", "center", "right"]),
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
align: PropTypes.oneOf(['left', 'center', 'right']),
};
export default Divider;

View file

@ -1,5 +1,5 @@
.divider-line {
content: "";
content: '';
display: inline-block;
flex: 1;
border-bottom: 1px solid var(--local-divider-color);
@ -36,7 +36,7 @@
}
.divider--primary {
--local-divider-color: var(--bg-primary);
--local-divider-opacity: 0.8;
--local-divider-opacity: .8;
.divider__text {
color: var(--tc-primary-high);
background-color: var(--bg-primary);
@ -44,7 +44,7 @@
}
.divider--positive {
--local-divider-color: var(--bg-positive);
--local-divider-opacity: 0.8;
--local-divider-opacity: .8;
.divider__text {
color: var(--bg-surface);
background-color: var(--bg-positive);
@ -52,7 +52,7 @@
}
.divider--danger {
--local-divider-color: var(--bg-danger);
--local-divider-opacity: 0.8;
--local-divider-opacity: .8;
.divider__text {
color: var(--bg-surface);
background-color: var(--bg-danger);
@ -60,7 +60,7 @@
}
.divider--caution {
--local-divider-color: var(--bg-caution);
--local-divider-opacity: 0.8;
--local-divider-opacity: .8;
.divider__text {
color: var(--bg-surface);
background-color: var(--bg-caution);

View file

@ -1,9 +1,13 @@
import React from "react";
import PropTypes from "prop-types";
import "./Header.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Header.scss';
function Header({ children }) {
return <div className="header">{children}</div>;
return (
<div className="header">
{children}
</div>
);
}
Header.propTypes = {
@ -11,7 +15,11 @@ Header.propTypes = {
};
function TitleWrapper({ children }) {
return <div className="header__title-wrapper">{children}</div>;
return (
<div className="header__title-wrapper">
{children}
</div>
);
}
TitleWrapper.propTypes = {

View file

@ -1,5 +1,5 @@
@use "../../partials/text";
@use "../../partials/dir";
@use '../../partials/text';
@use '../../partials/dir';
.header {
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));

View file

@ -1,43 +1,26 @@
import React from "react";
import PropTypes from "prop-types";
import "./Input.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Input.scss';
import TextareaAutosize from "react-autosize-textarea";
import TextareaAutosize from 'react-autosize-textarea';
function Input({
id,
label,
name,
value,
placeholder,
required,
type,
onChange,
forwardRef,
resizable,
minHeight,
onResize,
state,
onKeyDown,
disabled,
autoFocus,
id, label, name, value, placeholder,
required, type, onChange, forwardRef,
resizable, minHeight, onResize, state,
onKeyDown, disabled, autoFocus,
}) {
return (
<div className="input-container">
{label !== "" && (
<label className="input__label text-b2" htmlFor={id}>
{label}
</label>
)}
{resizable ? (
{ label !== '' && <label className="input__label text-b2" htmlFor={id}>{label}</label> }
{ resizable
? (
<TextareaAutosize
dir="auto"
style={{ minHeight: `${minHeight}px` }}
name={name}
id={id}
className={`input input--resizable${
state !== "normal" ? ` input--${state}` : ""
}`}
className={`input input--resizable${state !== 'normal' ? ` input--${state}` : ''}`}
ref={forwardRef}
type={type}
placeholder={placeholder}
@ -56,7 +39,7 @@ function Input({
ref={forwardRef}
id={id}
name={name}
className={`input ${state !== "normal" ? ` input--${state}` : ""}`}
className={`input ${state !== 'normal' ? ` input--${state}` : ''}`}
type={type}
placeholder={placeholder}
required={required}
@ -75,18 +58,18 @@ function Input({
Input.defaultProps = {
id: null,
name: "",
label: "",
value: "",
placeholder: "",
type: "text",
name: '',
label: '',
value: '',
placeholder: '',
type: 'text',
required: false,
onChange: null,
forwardRef: null,
resizable: false,
minHeight: 46,
onResize: null,
state: "normal",
state: 'normal',
onKeyDown: null,
disabled: false,
autoFocus: false,
@ -105,7 +88,7 @@ Input.propTypes = {
resizable: PropTypes.bool,
minHeight: PropTypes.number,
onResize: PropTypes.func,
state: PropTypes.oneOf(["normal", "success", "error"]),
state: PropTypes.oneOf(['normal', 'success', 'error']),
onKeyDown: PropTypes.func,
disabled: PropTypes.bool,
autoFocus: PropTypes.bool,

View file

@ -1,4 +1,4 @@
@use "../../atoms/scroll/scrollbar";
@use '../../atoms/scroll/scrollbar';
.input {
display: block;
@ -47,6 +47,6 @@
box-shadow: var(--bs-primary-border);
}
&::placeholder {
color: var(--tc-surface-low);
color: var(--tc-surface-low)
}
}

View file

@ -1,27 +1,23 @@
import React, { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import "./Math.scss";
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './Math.scss';
import katex from "katex";
import "katex/dist/katex.min.css";
import katex from 'katex';
import 'katex/dist/katex.min.css';
import "katex/dist/contrib/copy-tex";
import 'katex/dist/contrib/copy-tex';
const Math = React.memo(
({ content, throwOnError, errorColor, displayMode }) => {
const Math = React.memo(({
content, throwOnError, errorColor, displayMode,
}) => {
const ref = useRef(null);
useEffect(() => {
katex.render(content, ref.current, {
throwOnError,
errorColor,
displayMode,
});
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
}, [content, throwOnError, errorColor, displayMode]);
return <span ref={ref} />;
}
);
});
Math.defaultProps = {
throwOnError: null,
errorColor: null,

View file

@ -1,43 +1,36 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import "./RawModal.scss";
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
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({
className,
overlayClassName,
isOpen,
size,
onAfterOpen,
onAfterClose,
onRequestClose,
closeFromOutside,
children,
className, overlayClassName,
isOpen, size, onAfterOpen, onAfterClose,
onRequestClose, closeFromOutside, children,
}) {
let modalClass = className !== null ? `${className} ` : "";
let modalClass = (className !== null) ? `${className} ` : '';
switch (size) {
case "large":
modalClass += "raw-modal__large ";
case 'large':
modalClass += 'raw-modal__large ';
break;
case "medium":
modalClass += "raw-modal__medium ";
case 'medium':
modalClass += 'raw-modal__medium ';
break;
case "small":
case 'small':
default:
modalClass += "raw-modal__small ";
modalClass += 'raw-modal__small ';
}
useEffect(() => {
navigation.setIsRawModalVisible(isOpen);
}, [isOpen]);
const modalOverlayClass =
overlayClassName !== null ? `${overlayClassName} ` : "";
const modalOverlayClass = (overlayClassName !== null) ? `${overlayClassName} ` : '';
return (
<Modal
className={`${modalClass}raw-modal`}
@ -58,7 +51,7 @@ function RawModal({
RawModal.defaultProps = {
className: null,
overlayClassName: null,
size: "small",
size: 'small',
onAfterOpen: null,
onAfterClose: null,
onRequestClose: null,
@ -69,7 +62,7 @@ RawModal.propTypes = {
className: PropTypes.string,
overlayClassName: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
size: PropTypes.oneOf(["large", "medium", "small"]),
size: PropTypes.oneOf(['large', 'medium', 'small']),
onAfterOpen: PropTypes.func,
onAfterClose: PropTypes.func,
onRequestClose: PropTypes.func,

View file

@ -49,7 +49,7 @@
@keyframes raw-modal--content {
0% {
transform: translateY(100px);
opacity: 0.5;
opacity: .5;
}
100% {
transform: translateY(0);

View file

@ -1,25 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import "./ScrollView.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './ScrollView.scss';
const ScrollView = React.forwardRef(
({ horizontal, vertical, autoHide, invisible, onScroll, children }, ref) => {
let scrollbarClasses = "";
if (horizontal) scrollbarClasses += " scrollbar__h";
if (vertical) scrollbarClasses += " scrollbar__v";
if (autoHide) scrollbarClasses += " scrollbar--auto-hide";
if (invisible) scrollbarClasses += " scrollbar--invisible";
const ScrollView = React.forwardRef(({
horizontal, vertical, autoHide, invisible, onScroll, children,
}, ref) => {
let scrollbarClasses = '';
if (horizontal) scrollbarClasses += ' scrollbar__h';
if (vertical) scrollbarClasses += ' scrollbar__v';
if (autoHide) scrollbarClasses += ' scrollbar--auto-hide';
if (invisible) scrollbarClasses += ' scrollbar--invisible';
return (
<div
onScroll={onScroll}
ref={ref}
className={`scrollbar${scrollbarClasses}`}
>
<div onScroll={onScroll} ref={ref} className={`scrollbar${scrollbarClasses}`}>
{children}
</div>
);
}
);
});
ScrollView.defaultProps = {
horizontal: false,

View file

@ -1,8 +1,8 @@
@use "../../partials/dir";
@use "_scrollbar";
@use '../../partials/dir';
@use '_scrollbar';
@mixin paddingForSafari($padding) {
@media not all and (min-resolution: 0.001dpcm) {
@media not all and (min-resolution:.001dpcm) {
@include dir.side(padding, 0, $padding);
}
}

View file

@ -1,13 +1,15 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import "./SegmentedControls.scss";
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './SegmentedControls.scss';
import { blurOnBubbling } from "../button/script";
import { blurOnBubbling } from '../button/script';
import Text from "../text/Text";
import RawIcon from "../system-icons/RawIcon";
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
function SegmentedControls({ selected, segments, onSelect }) {
function SegmentedControls({
selected, segments, onSelect,
}) {
const [select, setSelect] = useState(selected);
function selectSegment(segmentIndex) {
@ -21,34 +23,32 @@ function SegmentedControls({ selected, segments, onSelect }) {
return (
<div className="segmented-controls">
{segments.map((segment, index) => (
{
segments.map((segment, index) => (
<button
key={Math.random().toString(20).substr(2, 6)}
className={`segment-btn${
select === index ? " segment-btn--active" : ""
}`}
className={`segment-btn${select === index ? ' segment-btn--active' : ''}`}
type="button"
onClick={() => selectSegment(index)}
onMouseUp={(e) => blurOnBubbling(e, ".segment-btn")}
onMouseUp={(e) => blurOnBubbling(e, '.segment-btn')}
>
<div className="segment-btn__base">
{segment.iconSrc && <RawIcon size="small" src={segment.iconSrc} />}
{segment.text && <Text variant="b2">{segment.text}</Text>}
</div>
</button>
))}
))
}
</div>
);
}
SegmentedControls.propTypes = {
selected: PropTypes.number.isRequired,
segments: PropTypes.arrayOf(
PropTypes.shape({
segments: PropTypes.arrayOf(PropTypes.shape({
iconSrc: PropTypes.string,
text: PropTypes.string,
})
).isRequired,
})).isRequired,
onSelect: PropTypes.func.isRequired,
};

View file

@ -1,5 +1,5 @@
@use "../button/state";
@use "../../partials/dir";
@use '../button/state';
@use '../../partials/dir';
.segmented-controls {
background-color: var(--bg-surface-low);
@ -46,16 +46,12 @@
&:last-child {
border-right: none;
}
[dir="rtl"] & {
[dir=rtl] & {
border-left: 1px solid var(--bg-surface-border);
border-right: 1px solid var(--bg-surface-border);
&:first-child {
border-right: none;
}
&:last-child {
border-left: none;
}
&:first-child { border-right: none;}
&:last-child { border-left: none;}
}
}
}

View file

@ -1,17 +1,19 @@
import React from "react";
import PropTypes from "prop-types";
import "./Spinner.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Spinner.scss';
function Spinner({ size }) {
return <div className={`donut-spinner donut-spinner--${size}`}> </div>;
return (
<div className={`donut-spinner donut-spinner--${size}`}> </div>
);
}
Spinner.defaultProps = {
size: "normal",
size: 'normal',
};
Spinner.propTypes = {
size: PropTypes.oneOf(["normal", "small"]),
size: PropTypes.oneOf(['normal', 'small']),
};
export default Spinner;

View file

@ -1,12 +1,12 @@
import React from "react";
import PropTypes from "prop-types";
import "./RawIcon.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './RawIcon.scss';
function RawIcon({ color, size, src, isImage }) {
const style = {};
if (color !== null) style.backgroundColor = color;
if (isImage) {
style.backgroundColor = "transparent";
style.backgroundColor = 'transparent';
style.backgroundImage = `url("${src}")`;
} else {
style.WebkitMaskImage = `url("${src}")`;
@ -18,13 +18,13 @@ function RawIcon({ color, size, src, isImage }) {
RawIcon.defaultProps = {
color: null,
size: "normal",
size: 'normal',
isImage: false,
};
RawIcon.propTypes = {
color: PropTypes.string,
size: PropTypes.oneOf(["large", "normal", "small", "extra-small"]),
size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']),
src: PropTypes.string.isRequired,
isImage: PropTypes.bool,
};

View file

@ -1,12 +1,15 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import "./Tabs.scss";
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Tabs.scss';
import Button from "../button/Button";
import ScrollView from "../scroll/ScrollView";
import Button from '../button/Button';
import ScrollView from '../scroll/ScrollView';
function TabItem({ selected, iconSrc, onClick, children, disabled }) {
const isSelected = selected ? "tab-item--selected" : "";
function TabItem({
selected, iconSrc,
onClick, children, disabled,
}) {
const isSelected = selected ? 'tab-item--selected' : '';
return (
<Button
@ -75,7 +78,7 @@ Tabs.propTypes = {
iconSrc: PropTypes.string,
text: PropTypes.string,
disabled: PropTypes.bool,
})
}),
).isRequired,
defaultSelected: PropTypes.number,
onSelect: PropTypes.func.isRequired,

View file

@ -1,4 +1,4 @@
@use "../../partials/dir";
@use '../../partials/dir';
.tabs {
height: var(--header-height);

View file

@ -1,51 +1,30 @@
import React from "react";
import PropTypes from "prop-types";
import "./Text.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Text.scss';
function Text({ className, style, variant, weight, primary, span, children }) {
function Text({
className, style, variant, weight,
primary, span, children,
}) {
const classes = [];
if (className) classes.push(className);
classes.push(`text text-${variant} text-${weight}`);
if (primary) classes.push("font-primary");
if (primary) classes.push('font-primary');
const textClass = classes.join(" ");
if (span)
return (
<span className={textClass} style={style}>
{children}
</span>
);
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>
);
const textClass = classes.join(' ');
if (span) return <span className={textClass} style={style}>{ children }</span>;
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 = {
className: null,
style: null,
variant: "b1",
weight: "normal",
variant: 'b1',
weight: 'normal',
primary: false,
span: false,
};
@ -53,8 +32,8 @@ Text.defaultProps = {
Text.propTypes = {
className: PropTypes.string,
style: PropTypes.shape({}),
variant: PropTypes.oneOf(["h1", "h2", "s1", "b1", "b2", "b3"]),
weight: PropTypes.oneOf(["light", "normal", "medium", "bold"]),
variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']),
weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']),
primary: PropTypes.bool,
span: PropTypes.bool,
children: PropTypes.node.isRequired,

View file

@ -5,7 +5,7 @@
& img.emoji,
& 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;
padding: 0 !important;
position: relative;
top: -0.1rem;
top: -.1rem;
vertical-align: middle;
}
}

View 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;

View file

@ -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;

View file

@ -1,9 +1,11 @@
import React from "react";
import PropTypes from "prop-types";
import "./Tooltip.scss";
import Tippy from "@tippyjs/react";
import React from 'react';
import PropTypes from 'prop-types';
import './Tooltip.scss';
import Tippy from '@tippyjs/react';
function Tooltip({ className, placement, content, delay, children }) {
function Tooltip({
className, placement, content, delay, children,
}) {
return (
<Tippy
content={content}
@ -21,8 +23,8 @@ function Tooltip({ className, placement, content, delay, children }) {
}
Tooltip.defaultProps = {
placement: "top",
className: "",
placement: 'top',
className: '',
delay: [200, 0],
};

View file

@ -1,7 +1,7 @@
/* 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) {
const mx = initMatrix.matrixClient;
@ -12,9 +12,9 @@ export function useAccountData(eventType) {
if (mEvent.getType() !== eventType) return;
setEvent(mEvent);
};
mx.on("accountData", handleChange);
mx.on('accountData', handleChange);
return () => {
mx.removeListener("accountData", handleChange);
mx.removeListener('accountData', handleChange);
};
}, [eventType]);

View file

@ -1,27 +1,22 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import initMatrix from "../../client/initMatrix";
import cons from "../../client/state/cons";
import initMatrix from '../../client/initMatrix';
import cons from '../../client/state/cons';
export function useCategorizedSpaces() {
const { accountData } = initMatrix;
const [categorizedSpaces, setCategorizedSpaces] = useState([
...accountData.categorizedSpaces,
]);
const [categorizedSpaces, setCategorizedSpaces] = useState([...accountData.categorizedSpaces]);
useEffect(() => {
const handleCategorizedSpaces = () => {
setCategorizedSpaces([...accountData.categorizedSpaces]);
};
accountData.on(
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
handleCategorizedSpaces
);
accountData.on(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, handleCategorizedSpaces);
return () => {
accountData.removeListener(
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
handleCategorizedSpaces
handleCategorizedSpaces,
);
};
}, []);

View file

@ -1,8 +1,8 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import initMatrix from "../../client/initMatrix";
import { hasCrossSigningAccountData } from "../../util/matrixUtil";
import initMatrix from '../../client/initMatrix';
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
export function useCrossSigningStatus() {
const mx = initMatrix.matrixClient;
@ -11,14 +11,14 @@ export function useCrossSigningStatus() {
useEffect(() => {
if (isCSEnabled) return null;
const handleAccountData = (event) => {
if (event.getType() === "m.cross_signing.master") {
if (event.getType() === 'm.cross_signing.master') {
setIsCSEnabled(true);
}
};
mx.on("accountData", handleAccountData);
mx.on('accountData', handleAccountData);
return () => {
mx.removeListener("accountData", handleAccountData);
mx.removeListener('accountData', handleAccountData);
};
}, [isCSEnabled === false]);
return isCSEnabled;

View file

@ -1,7 +1,7 @@
/* 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() {
const mx = initMatrix.matrixClient;
@ -10,8 +10,7 @@ export function useDeviceList() {
useEffect(() => {
let isMounted = true;
const updateDevices = () =>
mx.getDevices().then((data) => {
const updateDevices = () => mx.getDevices().then((data) => {
if (!isMounted) return;
setDeviceList(data.devices || []);
});
@ -23,9 +22,9 @@ export function useDeviceList() {
}
};
mx.on("crypto.devicesUpdated", handleDevicesUpdate);
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
return () => {
mx.removeListener("crypto.devicesUpdated", handleDevicesUpdate);
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
isMounted = false;
};
}, []);

View file

@ -1,13 +1,10 @@
/* eslint-disable import/prefer-default-export */
import { useState } from "react";
import { useState } from 'react';
export function useForceUpdate() {
const [data, setData] = useState(null);
return [
data,
function forceUpdateHook() {
return [data, function forceUpdateHook() {
setData({});
},
];
}];
}

View file

@ -1,6 +1,6 @@
/* eslint-disable import/prefer-default-export */
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react';
export function usePermission(name, initial) {
const [state, setState] = useState(initial);
@ -15,12 +15,12 @@ export function usePermission(name, initial) {
descriptor = _descriptor;
update();
descriptor.addEventListener("change", update);
descriptor.addEventListener('change', update);
});
}
return () => {
if (descriptor) descriptor.removeEventListener("change", update);
if (descriptor) descriptor.removeEventListener('change', update);
};
}, []);

View file

@ -1,8 +1,8 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import cons from "../../client/state/cons";
import navigation from "../../client/state/navigation";
import cons from '../../client/state/cons';
import navigation from '../../client/state/navigation';
export function useSelectedSpace() {
const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId);
@ -13,10 +13,7 @@ export function useSelectedSpace() {
};
navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
return () => {
navigation.removeListener(
cons.events.navigation.SPACE_SELECTED,
onSpaceSelected
);
navigation.removeListener(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
};
}, []);

View file

@ -1,8 +1,8 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import cons from "../../client/state/cons";
import navigation from "../../client/state/navigation";
import cons from '../../client/state/cons';
import navigation from '../../client/state/navigation';
export function useSelectedTab() {
const [selectedTab, setSelectedTab] = useState(navigation.selectedTab);
@ -13,10 +13,7 @@ export function useSelectedTab() {
};
navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
return () => {
navigation.removeListener(
cons.events.navigation.TAB_SELECTED,
onTabSelected
);
navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected);
};
}, []);

View file

@ -1,27 +1,22 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from "react";
import { useState, useEffect } from 'react';
import initMatrix from "../../client/initMatrix";
import cons from "../../client/state/cons";
import initMatrix from '../../client/initMatrix';
import cons from '../../client/state/cons';
export function useSpaceShortcut() {
const { accountData } = initMatrix;
const [spaceShortcut, setSpaceShortcut] = useState([
...accountData.spaceShortcut,
]);
const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
useEffect(() => {
const onSpaceShortcutUpdated = () => {
setSpaceShortcut([...accountData.spaceShortcut]);
};
accountData.on(
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
onSpaceShortcutUpdated
);
accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
return () => {
accountData.removeListener(
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
onSpaceShortcutUpdated
onSpaceShortcutUpdated,
);
};
}, []);

View file

@ -1,5 +1,5 @@
/* eslint-disable import/prefer-default-export */
import { useEffect, useRef } from "react";
import { useEffect, useRef } from 'react';
export function useStore(...args) {
const itemRef = useRef(null);

View file

@ -1,20 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import "./ConfirmDialog.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './ConfirmDialog.scss';
import { openReusableDialog } from "../../../client/action/navigation";
import { openReusableDialog } from '../../../client/action/navigation';
import Text from "../../atoms/text/Text";
import Button from "../../atoms/button/Button";
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
function ConfirmDialog({ desc, actionTitle, actionType, onComplete }) {
function ConfirmDialog({
desc, actionTitle, actionType, onComplete,
}) {
return (
<div className="confirm-dialog">
<Text>{desc}</Text>
<div className="confirm-dialog__btn">
<Button variant={actionType} onClick={() => onComplete(true)}>
{actionTitle}
</Button>
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
<Button onClick={() => onComplete(false)}>Cancel</Button>
</div>
</div>
@ -23,8 +23,7 @@ function ConfirmDialog({ desc, actionTitle, actionType, onComplete }) {
ConfirmDialog.propTypes = {
desc: PropTypes.string.isRequired,
actionTitle: PropTypes.string.isRequired,
actionType: PropTypes.oneOf(["primary", "positive", "danger", "caution"])
.isRequired,
actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired,
onComplete: PropTypes.func.isRequired,
};
@ -36,18 +35,10 @@ ConfirmDialog.propTypes = {
* @return {Promise<boolean>} does it get's confirmed or not
*/
// eslint-disable-next-line import/prefer-default-export
export const confirmDialog = (
title,
desc,
actionTitle,
actionType = "primary"
) =>
new Promise((resolve) => {
export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => {
let isCompleted = false;
openReusableDialog(
<Text variant="s1" weight="medium">
{title}
</Text>,
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<ConfirmDialog
desc={desc}
@ -62,6 +53,6 @@ export const confirmDialog = (
),
() => {
if (!isCompleted) resolve(false);
}
},
);
});

View file

@ -1,29 +1,22 @@
import React from "react";
import PropTypes from "prop-types";
import "./Dialog.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './Dialog.scss';
import { twemojify } from "../../../util/twemojify";
import { twemojify } from '../../../util/twemojify';
import Text from "../../atoms/text/Text";
import Header, { TitleWrapper } from "../../atoms/header/Header";
import ScrollView from "../../atoms/scroll/ScrollView";
import RawModal from "../../atoms/modal/RawModal";
import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header';
import ScrollView from '../../atoms/scroll/ScrollView';
import RawModal from '../../atoms/modal/RawModal';
function Dialog({
className,
isOpen,
title,
onAfterOpen,
onAfterClose,
contentOptions,
onRequestClose,
closeFromOutside,
children,
className, isOpen, title, onAfterOpen, onAfterClose,
contentOptions, onRequestClose, closeFromOutside, children,
invisibleScroll,
}) {
return (
<RawModal
className={`${className === null ? "" : `${className} `}dialog-modal`}
className={`${className === null ? '' : `${className} `}dialog-modal`}
isOpen={isOpen}
onAfterOpen={onAfterOpen}
onAfterClose={onAfterClose}
@ -35,19 +28,19 @@ function Dialog({
<div className="dialog__content">
<Header>
<TitleWrapper>
{typeof title === "string" ? (
<Text variant="h2" weight="medium" primary>
{twemojify(title)}
</Text>
) : (
title
)}
{
typeof title === 'string'
? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
: title
}
</TitleWrapper>
{contentOptions}
</Header>
<div className="dialog__content__wrapper">
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
<div className="dialog__content-container">{children}</div>
<div className="dialog__content-container">
{children}
</div>
</ScrollView>
</div>
</div>

View file

@ -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 IconButton from "../../atoms/button/IconButton";
import Dialog from "./Dialog";
import navigation from '../../../client/state/navigation';
import IconButton from '../../atoms/button/IconButton';
import Dialog from './Dialog';
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function ReusableDialog() {
const [isOpen, setIsOpen] = useState(false);
@ -19,10 +19,7 @@ function ReusableDialog() {
};
navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
return () => {
navigation.removeListener(
cons.events.navigation.REUSABLE_DIALOG_OPENED,
handleOpen
);
navigation.removeListener(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
};
}, []);
@ -38,16 +35,10 @@ function ReusableDialog() {
return (
<Dialog
isOpen={isOpen}
title={data?.title || ""}
title={data?.title || ''}
onAfterClose={handleAfterClose}
onRequestClose={handleRequestClose}
contentOptions={
<IconButton
src={CrossIC}
onClick={handleRequestClose}
tooltip="Close"
/>
}
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />}
invisibleScroll
>
{data?.render(handleRequestClose) || <div />}

View file

@ -1,17 +1,17 @@
/* eslint-disable react/prop-types */
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import "./FollowingMembers.scss";
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './FollowingMembers.scss';
import initMatrix from "../../../client/initMatrix";
import cons from "../../../client/state/cons";
import { openReadReceipts } from "../../../client/action/navigation";
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { openReadReceipts } from '../../../client/action/navigation';
import Text from "../../atoms/text/Text";
import RawIcon from "../../atoms/system-icons/RawIcon";
import TickMarkIC from "../../../../public/res/ic/outlined/tick-mark.svg";
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsersActionJsx } from "../../organisms/room/common";
import { getUsersActionJsx } from '../../organisms/room/common';
function FollowingMembers({ roomTimeline }) {
const [followingMembers, setFollowingMembers] = useState([]);
@ -27,38 +27,28 @@ function FollowingMembers({ roomTimeline }) {
setFollowingMembers(roomTimeline.getLiveReaders());
};
updateFollowingMembers();
roomTimeline.on(
cons.events.roomTimeline.LIVE_RECEIPT,
updateFollowingMembers
);
roomTimeline.on(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
roomsInput.on(cons.events.roomsInput.MESSAGE_SENT, handleOnMessageSent);
return () => {
roomTimeline.removeListener(
cons.events.roomTimeline.LIVE_RECEIPT,
updateFollowingMembers
);
roomsInput.removeListener(
cons.events.roomsInput.MESSAGE_SENT,
handleOnMessageSent
);
roomTimeline.removeListener(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
roomsInput.removeListener(cons.events.roomsInput.MESSAGE_SENT, handleOnMessageSent);
};
}, [roomTimeline]);
const filteredM = followingMembers.filter((userId) => userId !== myUserId);
return (
filteredM.length !== 0 && (
return filteredM.length !== 0 && (
<button
className="following-members"
onClick={() => openReadReceipts(roomId, followingMembers)}
type="button"
>
<RawIcon size="extra-small" src={TickMarkIC} />
<Text variant="b2">
{getUsersActionJsx(roomId, filteredM, "following the conversation.")}
</Text>
<RawIcon
size="extra-small"
src={TickMarkIC}
/>
<Text variant="b2">{getUsersActionJsx(roomId, filteredM, 'following the conversation.')}</Text>
</button>
)
);
}

View file

@ -1,4 +1,4 @@
@use "../../partials/text";
@use '../../partials/text';
.following-members {
width: 100%;

View file

@ -1,61 +1,59 @@
import React from "react";
import React from 'react';
import initMatrix from "../../../client/initMatrix";
import { openReusableContextMenu } from "../../../client/action/navigation";
import { getEventCords } from "../../../util/common";
import initMatrix from '../../../client/initMatrix';
import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import Text from "../../atoms/text/Text";
import Button from "../../atoms/button/Button";
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
import SettingTile from "../setting-tile/SettingTile";
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
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 = {
ON: "on",
OFF: "off",
NOISY: "noisy",
ON: 'on',
OFF: 'off',
NOISY: 'noisy',
};
export const typeToLabel = {
[notifType.ON]: "On",
[notifType.OFF]: "Off",
[notifType.NOISY]: "Noisy",
[notifType.ON]: 'On',
[notifType.OFF]: 'Off',
[notifType.NOISY]: 'Noisy',
};
Object.freeze(notifType);
const DM = ".m.rule.room_one_to_one";
const ENC_DM = ".m.rule.encrypted_room_one_to_one";
const ROOM = ".m.rule.message";
const ENC_ROOM = ".m.rule.encrypted";
const DM = '.m.rule.room_one_to_one';
const ENC_DM = '.m.rule.encrypted_room_one_to_one';
const ROOM = '.m.rule.message';
const ENC_ROOM = '.m.rule.encrypted';
export function getActionType(rule) {
const { actions } = rule;
if (actions.find((action) => action?.set_tweak === "sound"))
return notifType.NOISY;
if (actions.find((action) => action?.set_tweak === "highlight"))
return notifType.ON;
if (actions.find((action) => action === "dont_notify")) return notifType.OFF;
if (actions.find((action) => action?.set_tweak === 'sound')) return notifType.NOISY;
if (actions.find((action) => action?.set_tweak === 'highlight')) return notifType.ON;
if (actions.find((action) => action === 'dont_notify')) return notifType.OFF;
return notifType.OFF;
}
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" };
if (typeof highlightValue === "boolean") highlight.value = highlightValue;
if (type === notifType.ON) return ["notify", highlight];
const highlight = { set_tweak: 'highlight' };
if (typeof highlightValue === 'boolean') highlight.value = highlightValue;
if (type === notifType.ON) return ['notify', highlight];
const sound = { set_tweak: "sound", value: "default" };
return ["notify", sound, highlight];
const sound = { set_tweak: 'sound', value: 'default' };
return ['notify', sound, highlight];
}
function useGlobalNotif() {
const mx = initMatrix.matrixClient;
const pushRules = useAccountData("m.push_rules")?.getContent();
const pushRules = useAccountData('m.push_rules')?.getContent();
const underride = pushRules?.global?.underride ?? [];
const rulesToType = {
[DM]: notifType.ON,
@ -67,14 +65,12 @@ function useGlobalNotif() {
const getRuleCondition = (rule) => {
const condition = [];
if (rule === DM || rule === ENC_DM) {
condition.push({ kind: "room_member_count", is: "2" });
condition.push({ kind: 'room_member_count', is: '2' });
}
condition.push({
kind: "event_match",
key: "type",
pattern: [ENC_DM, ENC_ROOM].includes(rule)
? "m.room.encrypted"
: "m.room.message",
kind: 'event_match',
key: 'type',
pattern: [ENC_DM, ENC_ROOM].includes(rule) ? 'm.room.encrypted' : 'm.room.message',
});
return condition;
};
@ -97,7 +93,7 @@ function useGlobalNotif() {
}
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);
@ -118,8 +114,8 @@ function GlobalNotification() {
const onSelect = (evt, rule) => {
openReusableContextMenu(
"bottom",
getEventCords(evt, ".btn-surface"),
'bottom',
getEventCords(evt, '.btn-surface'),
(requestClose) => (
<NotificationSelector
value={rulesToType[rule]}
@ -128,7 +124,7 @@ function GlobalNotification() {
requestClose();
}}
/>
)
),
);
};
@ -137,67 +133,39 @@ function GlobalNotification() {
<MenuHeader>Global Notifications</MenuHeader>
<SettingTile
title="Direct messages"
options={
<Button
onClick={(evt) => onSelect(evt, DM)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, DM)} iconSrc={ChevronBottomIC}>
{ typeToLabel[rulesToType[DM]] }
</Button>
}
content={
<Text variant="b3">
Default notification settings for all direct message.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all direct message.</Text>}
/>
<SettingTile
title="Encrypted direct messages"
options={
<Button
onClick={(evt) => onSelect(evt, ENC_DM)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, ENC_DM)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ENC_DM]]}
</Button>
}
content={
<Text variant="b3">
Default notification settings for all encrypted direct message.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all encrypted direct message.</Text>}
/>
<SettingTile
title="Rooms messages"
options={
<Button
onClick={(evt) => onSelect(evt, ROOM)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, ROOM)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ROOM]]}
</Button>
}
content={
<Text variant="b3">
Default notification settings for all room message.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all room message.</Text>}
/>
<SettingTile
title="Encrypted rooms messages"
options={
<Button
onClick={(evt) => onSelect(evt, ENC_ROOM)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, ENC_ROOM)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ENC_ROOM]]}
</Button>
}
content={
<Text variant="b3">
Default notification settings for all encrypted room message.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all encrypted room message.</Text>}
/>
</div>
);

View file

@ -1,31 +1,31 @@
import React from "react";
import "./IgnoreUserList.scss";
import React from 'react';
import './IgnoreUserList.scss';
import initMatrix from "../../../client/initMatrix";
import * as roomActions from "../../../client/action/room";
import initMatrix from '../../../client/initMatrix';
import * as roomActions from '../../../client/action/room';
import Text from "../../atoms/text/Text";
import Chip from "../../atoms/chip/Chip";
import Input from "../../atoms/input/Input";
import Button from "../../atoms/button/Button";
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
import SettingTile from "../setting-tile/SettingTile";
import Text from '../../atoms/text/Text';
import Chip from '../../atoms/chip/Chip';
import Input from '../../atoms/input/Input';
import Button from '../../atoms/button/Button';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
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() {
useAccountData("m.ignored_user_list");
useAccountData('m.ignored_user_list');
const ignoredUsers = initMatrix.matrixClient.getIgnoredUsers();
const handleSubmit = (evt) => {
evt.preventDefault();
const { ignoreInput } = evt.target.elements;
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;
ignoreInput.value = "";
ignoreInput.value = '';
roomActions.ignore(userIds);
};
@ -34,17 +34,12 @@ function IgnoreUserList() {
<MenuHeader>Ignored users</MenuHeader>
<SettingTile
title="Ignore user"
content={
content={(
<div className="ignore-user-list__users">
<Text variant="b3">
Ignore userId if you do not want to receive their messages or
invites.
</Text>
<Text variant="b3">Ignore userId if you do not want to receive their messages or invites.</Text>
<form onSubmit={handleSubmit}>
<Input name="ignoreInput" required />
<Button variant="primary" type="submit">
Ignore
</Button>
<Button variant="primary" type="submit">Ignore</Button>
</form>
{ignoredUsers.length > 0 && (
<div>
@ -60,7 +55,7 @@ function IgnoreUserList() {
</div>
)}
</div>
}
)}
/>
</div>
);

View file

@ -1,38 +1,35 @@
import React from "react";
import "./KeywordNotification.scss";
import React from 'react';
import './KeywordNotification.scss';
import initMatrix from "../../../client/initMatrix";
import { openReusableContextMenu } from "../../../client/action/navigation";
import { getEventCords } from "../../../util/common";
import initMatrix from '../../../client/initMatrix';
import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import Text from "../../atoms/text/Text";
import Chip from "../../atoms/chip/Chip";
import Input from "../../atoms/input/Input";
import Button from "../../atoms/button/Button";
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
import SettingTile from "../setting-tile/SettingTile";
import Text from '../../atoms/text/Text';
import Chip from '../../atoms/chip/Chip';
import Input from '../../atoms/input/Input';
import Button from '../../atoms/button/Button';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
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 CrossIC from "../../../../public/res/ic/outlined/cross.svg";
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useAccountData } from "../../hooks/useAccountData";
import { useAccountData } from '../../hooks/useAccountData';
import {
notifType,
typeToLabel,
getActionType,
getTypeActions,
} from "./GlobalNotification";
notifType, typeToLabel, getActionType, getTypeActions,
} from './GlobalNotification';
const DISPLAY_NAME = ".m.rule.contains_display_name";
const ROOM_PING = ".m.rule.roomnotif";
const USERNAME = ".m.rule.contains_user_name";
const KEYWORD = "keyword";
const DISPLAY_NAME = '.m.rule.contains_display_name';
const ROOM_PING = '.m.rule.roomnotif';
const USERNAME = '.m.rule.contains_user_name';
const KEYWORD = 'keyword';
function useKeywordNotif() {
const mx = initMatrix.matrixClient;
const pushRules = useAccountData("m.push_rules")?.getContent();
const pushRules = useAccountData('m.push_rules')?.getContent();
const override = pushRules?.global?.override ?? [];
const content = pushRules?.global?.content ?? [];
@ -63,12 +60,12 @@ function useKeywordNotif() {
or.push(orRule);
}
if (rule === DISPLAY_NAME) {
orRule.conditions = [{ kind: "contains_display_name" }];
orRule.conditions = [{ kind: 'contains_display_name' }];
orRule.actions = getTypeActions(type, true);
} else {
orRule.conditions = [
{ kind: "event_match", key: "content.body", pattern: "@room" },
{ kind: "sender_notification_permission", key: "room" },
{ kind: 'event_match', key: 'content.body', pattern: '@room' },
{ kind: 'sender_notification_permission', key: 'room' },
];
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) => {
@ -107,13 +104,11 @@ function useKeywordNotif() {
default: false,
actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
});
mx.setAccountData("m.push_rules", pushRules);
mx.setAccountData('m.push_rules', pushRules);
};
const removeKeyword = (rule) => {
pushRules.global.content = content.filter(
(r) => r.rule_id !== rule.rule_id
);
mx.setAccountData("m.push_rules", pushRules);
pushRules.global.content = content.filter((r) => r.rule_id !== rule.rule_id);
mx.setAccountData('m.push_rules', pushRules);
};
const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
@ -136,16 +131,20 @@ function useKeywordNotif() {
}
function GlobalNotification() {
const { rulesToType, pushRules, setRule, addKeyword, removeKeyword } =
useKeywordNotif();
const {
rulesToType,
pushRules,
setRule,
addKeyword,
removeKeyword,
} = useKeywordNotif();
const keywordRules =
pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
const keywordRules = pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
const onSelect = (evt, rule) => {
openReusableContextMenu(
"bottom",
getEventCords(evt, ".btn-surface"),
'bottom',
getEventCords(evt, '.btn-surface'),
(requestClose) => (
<NotificationSelector
value={rulesToType[rule]}
@ -154,7 +153,7 @@ function GlobalNotification() {
requestClose();
}}
/>
)
),
);
};
@ -162,9 +161,9 @@ function GlobalNotification() {
evt.preventDefault();
const { keywordInput } = evt.target.elements;
const value = keywordInput.value.trim();
if (value === "") return;
if (value === '') return;
addKeyword(value);
keywordInput.value = "";
keywordInput.value = '';
};
return (
@ -172,84 +171,50 @@ function GlobalNotification() {
<MenuHeader>Mentions & keywords</MenuHeader>
<SettingTile
title="Message containing my display name"
options={
<Button
onClick={(evt) => onSelect(evt, DISPLAY_NAME)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, DISPLAY_NAME)} iconSrc={ChevronBottomIC}>
{ typeToLabel[rulesToType[DISPLAY_NAME]] }
</Button>
}
content={
<Text variant="b3">
Default notification settings for all message containing your
display name.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all message containing your display name.</Text>}
/>
<SettingTile
title="Message containing my username"
options={
<Button
onClick={(evt) => onSelect(evt, USERNAME)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, USERNAME)} iconSrc={ChevronBottomIC}>
{ typeToLabel[rulesToType[USERNAME]] }
</Button>
}
content={
<Text variant="b3">
Default notification settings for all message containing your
username.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all message containing your username.</Text>}
/>
<SettingTile
title="Message containing @room"
options={
<Button
onClick={(evt) => onSelect(evt, ROOM_PING)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, ROOM_PING)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ROOM_PING]]}
</Button>
}
content={
<Text variant="b3">
Default notification settings for all messages containing @room.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all messages containing @room.</Text>}
/>
{ rulesToType[KEYWORD] && (
<SettingTile
title="Message containing keywords"
options={
<Button
onClick={(evt) => onSelect(evt, KEYWORD)}
iconSrc={ChevronBottomIC}
>
options={(
<Button onClick={(evt) => onSelect(evt, KEYWORD)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[KEYWORD]]}
</Button>
}
content={
<Text variant="b3">
Default notification settings for all message containing keywords.
</Text>
}
)}
content={<Text variant="b3">Default notification settings for all message containing keywords.</Text>}
/>
)}
<SettingTile
title="Keywords"
content={
content={(
<div className="keyword-notification__keyword">
<Text variant="b3">
Get notification when a message contains keyword.
</Text>
<Text variant="b3">Get notification when a message contains keyword.</Text>
<form onSubmit={handleSubmit}>
<Input name="keywordInput" required />
<Button variant="primary" type="submit">
Add
</Button>
<Button variant="primary" type="submit">Add</Button>
</form>
{keywordRules.length > 0 && (
<div>
@ -265,7 +230,7 @@ function GlobalNotification() {
</div>
)}
</div>
}
)}
/>
</div>
);

View file

@ -1,41 +1,25 @@
import React from "react";
import PropTypes from "prop-types";
import React from 'react';
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 (
<div>
<MenuHeader>Notification</MenuHeader>
<MenuItem
iconSrc={value === "off" ? CheckIC : null}
variant={value === "off" ? "positive" : "surface"}
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>
<MenuItem iconSrc={value === 'off' ? CheckIC : null} variant={value === 'off' ? 'positive' : 'surface'} 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>
);
}
NotificationSelector.propTypes = {
value: PropTypes.oneOf(["off", "on", "noisy"]).isRequired,
value: PropTypes.oneOf(['off', 'on', 'noisy']).isRequired,
onSelect: PropTypes.func.isRequired,
};

View file

@ -1,16 +1,18 @@
import React from "react";
import PropTypes from "prop-types";
import "./ImageLightbox.scss";
import FileSaver from "file-saver";
import React from 'react';
import PropTypes from 'prop-types';
import './ImageLightbox.scss';
import FileSaver from 'file-saver';
import Text from "../../atoms/text/Text";
import RawModal from "../../atoms/modal/RawModal";
import IconButton from "../../atoms/button/IconButton";
import Text from '../../atoms/text/Text';
import RawModal from '../../atoms/modal/RawModal';
import IconButton from '../../atoms/button/IconButton';
import DownloadSVG from "../../../../public/res/ic/outlined/download.svg";
import ExternalSVG from "../../../../public/res/ic/outlined/external.svg";
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
function ImageLightbox({ url, alt, isOpen, onRequestClose }) {
function ImageLightbox({
url, alt, isOpen, onRequestClose,
}) {
const handleDownload = () => {
FileSaver.saveAs(url, alt);
};
@ -24,14 +26,8 @@ function ImageLightbox({ url, alt, isOpen, onRequestClose }) {
size="large"
>
<div className="image-lightbox__header">
<Text variant="b2" weight="medium">
{alt}
</Text>
<IconButton
onClick={() => window.open(url)}
size="small"
src={ExternalSVG}
/>
<Text variant="b2" weight="medium">{alt}</Text>
<IconButton onClick={() => window.open(url)} size="small" src={ExternalSVG} />
<IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
</div>
<div className="image-lightbox__content">

View file

@ -1,5 +1,5 @@
@use "../../partials/flex";
@use "../../partials/text";
@use '../../partials/flex';
@use '../../partials/text';
.image-lightbox__modal {
box-shadow: none;
@ -21,6 +21,7 @@
background-color: var(--bg-overlay-low);
}
.image-lightbox__header > *,
.image-lightbox__content > * {
pointer-events: all;

View file

@ -1,38 +1,37 @@
import React, { useState, useMemo, useReducer, useEffect } from "react";
import PropTypes from "prop-types";
import "./ImagePack.scss";
import React, {
useState, useMemo, useReducer, useEffect,
} from 'react';
import PropTypes from 'prop-types';
import './ImagePack.scss';
import initMatrix from "../../../client/initMatrix";
import { openReusableDialog } from "../../../client/action/navigation";
import { suffixRename } from "../../../util/common";
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { suffixRename } from '../../../util/common';
import Button from "../../atoms/button/Button";
import Text from "../../atoms/text/Text";
import Input from "../../atoms/input/Input";
import Checkbox from "../../atoms/button/Checkbox";
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
import Button from '../../atoms/button/Button';
import Text from '../../atoms/text/Text';
import Input from '../../atoms/input/Input';
import Checkbox from '../../atoms/button/Checkbox';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import { ImagePack as ImagePackBuilder } from "../../organisms/emoji-board/custom-emoji";
import { confirmDialog } from "../confirm-dialog/ConfirmDialog";
import ImagePackProfile from "./ImagePackProfile";
import ImagePackItem from "./ImagePackItem";
import ImagePackUpload from "./ImagePackUpload";
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import ImagePackProfile from './ImagePackProfile';
import ImagePackItem from './ImagePackItem';
import ImagePackUpload from './ImagePackUpload';
const renameImagePackItem = (shortcode) =>
new Promise((resolve) => {
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
let isCompleted = false;
openReusableDialog(
<Text variant="s1" weight="medium">
Rename
</Text>,
<Text variant="s1" weight="medium">Rename</Text>,
(requestClose) => (
<div style={{ padding: "var(--sp-normal)" }}>
<div style={{ padding: 'var(--sp-normal)' }}>
<form
onSubmit={(e) => {
e.preventDefault();
const sc = e.target.shortcode.value;
if (sc.trim() === "") return;
if (sc.trim() === '') return;
isCompleted = true;
resolve(sc.trim());
requestClose();
@ -45,36 +44,32 @@ const renameImagePackItem = (shortcode) =>
autoFocus
required
/>
<div style={{ height: "var(--sp-normal)" }} />
<Button variant="primary" type="submit">
Rename
</Button>
<div style={{ height: 'var(--sp-normal)' }} />
<Button variant="primary" type="submit">Rename</Button>
</form>
</div>
),
() => {
if (!isCompleted) resolve(null);
}
},
);
});
function getUsage(usage) {
if (usage.includes("emoticon") && usage.includes("sticker")) return "both";
if (usage.includes("emoticon")) return "emoticon";
if (usage.includes("sticker")) return "sticker";
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
if (usage.includes('emoticon')) return 'emoticon';
if (usage.includes('sticker')) return 'sticker';
return "both";
return 'both';
}
function isGlobalPack(roomId, stateKey) {
const mx = initMatrix.matrixClient;
const globalContent = mx
.getAccountData("im.ponies.emote_rooms")
?.getContent();
if (typeof globalContent !== "object") return false;
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
if (typeof globalContent !== 'object') return false;
const { rooms } = globalContent;
if (typeof rooms !== "object") return false;
if (typeof rooms !== 'object') return false;
return rooms[roomId]?.[stateKey] !== undefined;
}
@ -83,17 +78,13 @@ function useRoomImagePack(roomId, stateKey) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const packEvent = room.currentState.getStateEvents(
"im.ponies.room_emotes",
stateKey
);
const pack = useMemo(
() => ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent()),
[room, stateKey]
);
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
const pack = useMemo(() => (
ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent())
), [room, stateKey]);
const sendPackContent = (content) => {
mx.sendStateEvent(roomId, "im.ponies.room_emotes", content, stateKey);
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
};
return {
@ -104,21 +95,16 @@ function useRoomImagePack(roomId, stateKey) {
function useUserImagePack() {
const mx = initMatrix.matrixClient;
const packEvent = mx.getAccountData("im.ponies.user_emotes");
const pack = useMemo(
() =>
ImagePackBuilder.parsePack(
mx.getUserId(),
packEvent?.getContent() ?? {
pack: { display_name: "Personal" },
const packEvent = mx.getAccountData('im.ponies.user_emotes');
const pack = useMemo(() => (
ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? {
pack: { display_name: 'Personal' },
images: {},
}
),
[]
);
})
), []);
const sendPackContent = (content) => {
mx.setAccountData("im.ponies.user_emotes", content);
mx.setAccountData('im.ponies.user_emotes', content);
};
return {
@ -131,11 +117,12 @@ function useImagePackHandles(pack, sendPackContent) {
const [, forceUpdate] = useReducer((count) => count + 1, 0);
const getNewKey = (key) => {
if (typeof key !== "string") return undefined;
let newKey = key?.replace(/\s/g, "_");
if (typeof key !== 'string') return undefined;
let newKey = key?.replace(/\s/g, '_');
if (pack.getImages().get(newKey)) {
newKey = suffixRename(newKey, (suffixedKey) =>
pack.getImages().get(suffixedKey)
newKey = suffixRename(
newKey,
(suffixedKey) => pack.getImages().get(suffixedKey),
);
}
return newKey;
@ -154,12 +141,10 @@ function useImagePackHandles(pack, sendPackContent) {
};
const handleUsageChange = (newUsage) => {
const usage = [];
if (newUsage === "emoticon" || newUsage === "both") usage.push("emoticon");
if (newUsage === "sticker" || newUsage === "both") usage.push("sticker");
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
pack.setUsage(usage);
pack
.getImages()
.forEach((img) => pack.setImageUsage(img.shortcode, undefined));
pack.getImages().forEach((img) => pack.setImageUsage(img.shortcode, undefined));
sendPackContent(pack.getContent());
forceUpdate();
@ -176,10 +161,10 @@ function useImagePackHandles(pack, sendPackContent) {
};
const handleDeleteItem = async (key) => {
const isConfirmed = await confirmDialog(
"Delete",
'Delete',
`Are you sure that you want to delete "${key}"?`,
"Delete",
"danger"
'Delete',
'danger',
);
if (!isConfirmed) return;
pack.removeImage(key);
@ -189,8 +174,8 @@ function useImagePackHandles(pack, sendPackContent) {
};
const handleUsageItem = (key, newUsage) => {
const usage = [];
if (newUsage === "emoticon" || newUsage === "both") usage.push("emoticon");
if (newUsage === "sticker" || newUsage === "both") usage.push("sticker");
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
pack.setImageUsage(key, usage);
sendPackContent(pack.getContent());
@ -220,23 +205,21 @@ function useImagePackHandles(pack, sendPackContent) {
}
function addGlobalImagePack(mx, roomId, stateKey) {
const content =
mx.getAccountData("im.ponies.emote_rooms")?.getContent() ?? {};
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
if (!content.rooms) content.rooms = {};
if (!content.rooms[roomId]) content.rooms[roomId] = {};
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) {
const content =
mx.getAccountData("im.ponies.emote_rooms")?.getContent() ?? {};
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
if (!content.rooms) return Promise.resolve();
if (!content.rooms[roomId]) return Promise.resolve();
delete content.rooms[roomId][stateKey];
if (Object.keys(content.rooms[roomId]).length === 0) {
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 }) {
@ -264,17 +247,14 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
};
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const canChange = room.currentState.hasSufficientPowerLevelFor(
"state_default",
myPowerlevel
);
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
const handleDeletePack = async () => {
const isConfirmed = await confirmDialog(
"Delete Pack",
'Delete Pack',
`Are you sure that you want to delete "${pack.displayName}"?`,
"Delete",
"danger"
'Delete',
'danger',
);
if (!isConfirmed) return;
handlePackDelete(stateKey);
@ -285,19 +265,17 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
return (
<div className="image-pack">
<ImagePackProfile
avatarUrl={
pack.avatarUrl
? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, "crop")
: null
}
displayName={pack.displayName ?? "Unknown"}
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
displayName={pack.displayName ?? 'Unknown'}
attribution={pack.attribution}
usage={getUsage(pack.usage)}
onUsageChange={canChange ? handleUsageChange : null}
onAvatarChange={canChange ? handleAvatarChange : null}
onEditProfile={canChange ? handleEditProfile : null}
/>
{canChange && <ImagePackUpload onUpload={handleAddItem} />}
{ canChange && (
<ImagePackUpload onUpload={handleAddItem} />
)}
{ images.length === 0 ? null : (
<div>
<div className="image-pack__header">
@ -322,27 +300,21 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
<div className="image-pack__footer">
{pack.images.size > 2 && (
<Button onClick={() => setViewMore(!viewMore)}>
{viewMore ? "View less" : `View ${pack.images.size - 2} more`}
</Button>
)}
{handlePackDelete && (
<Button variant="danger" onClick={handleDeletePack}>
Delete Pack
{
viewMore
? 'View less'
: `View ${pack.images.size - 2} more`
}
</Button>
)}
{ handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
</div>
)}
<div className="image-pack__global">
<Checkbox
variant="positive"
onToggle={handleGlobalChange}
isActive={isGlobal}
/>
<Checkbox variant="positive" onToggle={handleGlobalChange} isActive={isGlobal} />
<div>
<Text variant="b2">Use globally</Text>
<Text variant="b3">
Add this pack to your account to use in all rooms.
</Text>
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
</div>
</div>
</div>
@ -379,12 +351,8 @@ function ImagePackUser() {
return (
<div className="image-pack">
<ImagePackProfile
avatarUrl={
pack.avatarUrl
? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, "crop")
: null
}
displayName={pack.displayName ?? "Personal"}
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
displayName={pack.displayName ?? 'Personal'}
attribution={pack.attribution}
usage={getUsage(pack.usage)}
onUsageChange={handleUsageChange}
@ -412,10 +380,14 @@ function ImagePackUser() {
))}
</div>
)}
{pack.images.size > 2 && (
{(pack.images.size > 2) && (
<div className="image-pack__footer">
<Button onClick={() => setViewMore(!viewMore)}>
{viewMore ? "View less" : `View ${pack.images.size - 2} more`}
{
viewMore
? 'View less'
: `View ${pack.images.size - 2} more`
}
</Button>
</div>
)}
@ -428,13 +400,11 @@ function useGlobalImagePack() {
const mx = initMatrix.matrixClient;
const roomIdToStateKeys = new Map();
const globalContent = mx
.getAccountData("im.ponies.emote_rooms")
?.getContent() ?? { rooms: {} };
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
const { rooms } = globalContent;
Object.keys(rooms).forEach((roomId) => {
if (typeof rooms[roomId] !== "object") return;
if (typeof rooms[roomId] !== 'object') return;
const room = mx.getRoom(roomId);
const stateKeys = Object.keys(rooms[roomId]);
if (!room || stateKeys.length === 0) return;
@ -443,11 +413,11 @@ function useGlobalImagePack() {
useEffect(() => {
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 () => {
mx.removeListener("accountData", handleEvent);
mx.removeListener('accountData', handleEvent);
};
}, []);
@ -466,39 +436,29 @@ function ImagePackGlobal() {
<div className="image-pack-global">
<MenuHeader>Global packs</MenuHeader>
<div>
{roomIdToStateKeys.size > 0 ? (
[...roomIdToStateKeys].map(([roomId, stateKeys]) => {
{
roomIdToStateKeys.size > 0
? [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
const room = mx.getRoom(roomId);
return stateKeys.map((stateKey) => {
const data = room.currentState.getStateEvents(
"im.ponies.room_emotes",
stateKey
);
const pack = ImagePackBuilder.parsePack(
data?.getId(),
data?.getContent()
);
return (
stateKeys.map((stateKey) => {
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
if (!pack) return null;
return (
<div className="image-pack__global" key={pack.id}>
<Checkbox
variant="positive"
onToggle={() => handleChange(roomId, stateKey)}
isActive
/>
<Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive />
<div>
<Text variant="b2">{pack.displayName ?? "Unknown"}</Text>
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
<Text variant="b3">{room.name}</Text>
</div>
</div>
);
});
})
) : (
<div className="image-pack-global__empty">
<Text>No global packs</Text>
</div>
)}
);
})
: <div className="image-pack-global__empty"><Text>No global packs</Text></div>
}
</div>
</div>
);

View file

@ -1,4 +1,4 @@
@use "../../partials/flex";
@use '../../partials/flex';
.image-pack {
&-item {

View file

@ -1,33 +1,28 @@
import React from "react";
import PropTypes from "prop-types";
import "./ImagePackItem.scss";
import React from 'react';
import PropTypes from 'prop-types';
import './ImagePackItem.scss';
import { openReusableContextMenu } from "../../../client/action/navigation";
import { getEventCords } from "../../../util/common";
import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import Avatar from "../../atoms/avatar/Avatar";
import Text from "../../atoms/text/Text";
import Button from "../../atoms/button/Button";
import RawIcon from "../../atoms/system-icons/RawIcon";
import IconButton from "../../atoms/button/IconButton";
import ImagePackUsageSelector from "./ImagePackUsageSelector";
import Avatar from '../../atoms/avatar/Avatar';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import RawIcon from '../../atoms/system-icons/RawIcon';
import IconButton from '../../atoms/button/IconButton';
import ImagePackUsageSelector from './ImagePackUsageSelector';
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
import BinIC from "../../../../public/res/ic/outlined/bin.svg";
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
function ImagePackItem({
url,
shortcode,
usage,
onUsageChange,
onDelete,
onRename,
url, shortcode, usage, onUsageChange, onDelete, onRename,
}) {
const handleUsageSelect = (event) => {
openReusableContextMenu(
"bottom",
getEventCords(event, ".btn-surface"),
'bottom',
getEventCords(event, '.btn-surface'),
(closeMenu) => (
<ImagePackUsageSelector
usage={usage}
@ -36,48 +31,27 @@ function ImagePackItem({
closeMenu();
}}
/>
)
),
);
};
return (
<div className="image-pack-item">
<Avatar
imageSrc={url}
size="extra-small"
text={shortcode}
bgColor="black"
/>
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
<div className="image-pack-item__content">
<Text>{shortcode}</Text>
</div>
<div className="image-pack-item__usage">
<div className="image-pack-item__btn">
{onRename && (
<IconButton
tooltip="Rename"
size="extra-small"
src={PencilIC}
onClick={() => onRename(shortcode)}
/>
)}
{onDelete && (
<IconButton
tooltip="Delete"
size="extra-small"
src={BinIC}
onClick={() => onDelete(shortcode)}
/>
)}
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
</div>
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
{onUsageChange && (
<RawIcon src={ChevronBottomIC} size="extra-small" />
)}
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
<Text variant="b2">
{usage === "emoticon" && "Emoji"}
{usage === "sticker" && "Sticker"}
{usage === "both" && "Both"}
{usage === 'emoticon' && 'Emoji'}
{usage === 'sticker' && 'Sticker'}
{usage === 'both' && 'Both'}
</Text>
</Button>
</div>
@ -93,7 +67,7 @@ ImagePackItem.defaultProps = {
ImagePackItem.propTypes = {
url: PropTypes.string.isRequired,
shortcode: PropTypes.string.isRequired,
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
onUsageChange: PropTypes.func,
onDelete: PropTypes.func,
onRename: PropTypes.func,

View file

@ -1,5 +1,5 @@
@use "../../partials/flex";
@use "../../partials/dir";
@use '../../partials/flex';
@use '../../partials/dir';
.image-pack-item {
margin: 0 var(--sp-normal);

View file

@ -1,29 +1,24 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import "./ImagePackProfile.scss";
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './ImagePackProfile.scss';
import { openReusableContextMenu } from "../../../client/action/navigation";
import { getEventCords } from "../../../util/common";
import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import Text from "../../atoms/text/Text";
import Avatar from "../../atoms/avatar/Avatar";
import Button from "../../atoms/button/Button";
import IconButton from "../../atoms/button/IconButton";
import Input from "../../atoms/input/Input";
import ImageUpload from "../image-upload/ImageUpload";
import ImagePackUsageSelector from "./ImagePackUsageSelector";
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import Input from '../../atoms/input/Input';
import ImageUpload from '../image-upload/ImageUpload';
import ImagePackUsageSelector from './ImagePackUsageSelector';
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
function ImagePackProfile({
avatarUrl,
displayName,
attribution,
usage,
onUsageChange,
onAvatarChange,
onEditProfile,
avatarUrl, displayName, attribution, usage,
onUsageChange, onAvatarChange, onEditProfile,
}) {
const [isEdit, setIsEdit] = useState(false);
@ -40,8 +35,8 @@ function ImagePackProfile({
const handleUsageSelect = (event) => {
openReusableContextMenu(
"bottom",
getEventCords(event, ".btn-surface"),
'bottom',
getEventCords(event, '.btn-surface'),
(closeMenu) => (
<ImagePackUsageSelector
usage={usage}
@ -50,13 +45,15 @@ function ImagePackProfile({
closeMenu();
}}
/>
)
),
);
};
return (
<div className="image-pack-profile">
{onAvatarChange ? (
{
onAvatarChange
? (
<ImageUpload
bgColor="#555"
text={displayName}
@ -65,28 +62,18 @@ function ImagePackProfile({
onUpload={onAvatarChange}
onRequestRemove={() => onAvatarChange(undefined)}
/>
) : (
<Avatar
bgColor="#555"
text={displayName}
imageSrc={avatarUrl}
size="normal"
/>
)}
)
: <Avatar bgColor="#555" text={displayName} imageSrc={avatarUrl} size="normal" />
}
<div className="image-pack-profile__content">
{isEdit ? (
{
isEdit
? (
<form onSubmit={handleSubmit}>
<Input name="nameInput" label="Name" value={displayName} required />
<Input
name="attributionInput"
label="Attribution"
value={attribution}
resizable
/>
<Input name="attributionInput" label="Attribution" value={attribution} resizable />
<div>
<Button variant="primary" type="submit">
Save
</Button>
<Button variant="primary" type="submit">Save</Button>
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
</div>
</form>
@ -94,18 +81,12 @@ function ImagePackProfile({
<>
<div>
<Text>{displayName}</Text>
{onEditProfile && (
<IconButton
size="extra-small"
onClick={() => setIsEdit(true)}
src={PencilIC}
tooltip="Edit"
/>
)}
{onEditProfile && <IconButton size="extra-small" onClick={() => setIsEdit(true)} src={PencilIC} tooltip="Edit" />}
</div>
{attribution && <Text variant="b3">{attribution}</Text>}
</>
)}
)
}
</div>
<div className="image-pack-profile__usage">
<Text variant="b3">Pack usage</Text>
@ -114,9 +95,9 @@ function ImagePackProfile({
iconSrc={onUsageChange ? ChevronBottomIC : null}
>
<Text>
{usage === "emoticon" && "Emoji"}
{usage === "sticker" && "Sticker"}
{usage === "both" && "Both"}
{usage === 'emoticon' && 'Emoji'}
{usage === 'sticker' && 'Sticker'}
{usage === 'both' && 'Both'}
</Text>
</Button>
</div>
@ -135,7 +116,7 @@ ImagePackProfile.propTypes = {
avatarUrl: PropTypes.string,
displayName: PropTypes.string.isRequired,
attribution: PropTypes.string,
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
onUsageChange: PropTypes.func,
onAvatarChange: PropTypes.func,
onEditProfile: PropTypes.func,

View file

@ -1,4 +1,4 @@
@use "../../partials/flex";
@use '../../partials/flex';
.image-pack-profile {
padding: var(--sp-normal);

View file

@ -1,15 +1,15 @@
import React, { useState, useRef } from "react";
import PropTypes from "prop-types";
import "./ImagePackUpload.scss";
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import './ImagePackUpload.scss';
import initMatrix from "../../../client/initMatrix";
import { scaleDownImage } from "../../../util/common";
import initMatrix from '../../../client/initMatrix';
import { scaleDownImage } from '../../../util/common';
import Text from "../../atoms/text/Text";
import Button from "../../atoms/button/Button";
import Input from "../../atoms/input/Input";
import IconButton from "../../atoms/button/IconButton";
import CirclePlusIC from "../../../../public/res/ic/outlined/circle-plus.svg";
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import IconButton from '../../atoms/button/IconButton';
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
function ImagePackUpload({ onUpload }) {
const mx = initMatrix.matrixClient;
@ -23,7 +23,7 @@ function ImagePackUpload({ onUpload }) {
if (!imgFile) return;
const { shortcodeInput } = evt.target;
const shortcode = shortcodeInput.value.trim();
if (shortcode === "") return;
if (shortcode === '') return;
setProgress(true);
const image = await scaleDownImage(imgFile, 512, 512);
@ -32,53 +32,37 @@ function ImagePackUpload({ onUpload }) {
onUpload(shortcode, url);
setProgress(false);
setImgFile(null);
shortcodeRef.current.value = "";
shortcodeRef.current.value = '';
};
const handleFileChange = (evt) => {
const img = evt.target.files[0];
if (!img) return;
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();
};
const handleRemove = () => {
setImgFile(null);
inputRef.current.value = null;
shortcodeRef.current.value = "";
shortcodeRef.current.value = '';
};
return (
<form onSubmit={handleSubmit} className="image-pack-upload">
<input
ref={inputRef}
onChange={handleFileChange}
style={{ display: "none" }}
type="file"
accept=".png, .gif, .webp"
required
/>
{imgFile ? (
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" accept=".png, .gif, .webp" required />
{
imgFile
? (
<div className="image-pack-upload__file">
<IconButton
onClick={handleRemove}
src={CirclePlusIC}
tooltip="Remove file"
/>
<IconButton onClick={handleRemove} src={CirclePlusIC} tooltip="Remove file" />
<Text>{imgFile.name}</Text>
</div>
) : (
<Button onClick={() => inputRef.current.click()}>Import image</Button>
)}
<Input
forwardRef={shortcodeRef}
name="shortcodeInput"
placeholder="shortcode"
required
/>
<Button disabled={progress} variant="primary" type="submit">
{progress ? "Uploading..." : "Upload"}
</Button>
)
: <Button onClick={() => inputRef.current.click()}>Import image</Button>
}
<Input forwardRef={shortcodeRef} name="shortcodeInput" placeholder="shortcode" required />
<Button disabled={progress} variant="primary" type="submit">{progress ? 'Uploading...' : 'Upload'}</Button>
</form>
);
}

View file

@ -1,5 +1,5 @@
@use "../../partials/dir";
@use "../../partials/text";
@use '../../partials/dir';
@use '../../partials/text';
.image-pack-upload {
padding: var(--sp-normal);

View file

@ -1,31 +1,31 @@
import React from "react";
import PropTypes from "prop-types";
import React from 'react';
import PropTypes from 'prop-types';
import { MenuHeader, MenuItem } from "../../atoms/context-menu/ContextMenu";
import CheckIC from "../../../../public/res/ic/outlined/check.svg";
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
function ImagePackUsageSelector({ usage, onSelect }) {
return (
<div>
<MenuHeader>Usage</MenuHeader>
<MenuItem
iconSrc={usage === "emoticon" ? CheckIC : undefined}
variant={usage === "emoticon" ? "positive" : "surface"}
onClick={() => onSelect("emoticon")}
iconSrc={usage === 'emoticon' ? CheckIC : undefined}
variant={usage === 'emoticon' ? 'positive' : 'surface'}
onClick={() => onSelect('emoticon')}
>
Emoji
</MenuItem>
<MenuItem
iconSrc={usage === "sticker" ? CheckIC : undefined}
variant={usage === "sticker" ? "positive" : "surface"}
onClick={() => onSelect("sticker")}
iconSrc={usage === 'sticker' ? CheckIC : undefined}
variant={usage === 'sticker' ? 'positive' : 'surface'}
onClick={() => onSelect('sticker')}
>
Sticker
</MenuItem>
<MenuItem
iconSrc={usage === "both" ? CheckIC : undefined}
variant={usage === "both" ? "positive" : "surface"}
onClick={() => onSelect("both")}
iconSrc={usage === 'both' ? CheckIC : undefined}
variant={usage === 'both' ? 'positive' : 'surface'}
onClick={() => onSelect('both')}
>
Both
</MenuItem>
@ -34,7 +34,7 @@ function ImagePackUsageSelector({ usage, onSelect }) {
}
ImagePackUsageSelector.propTypes = {
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
onSelect: PropTypes.func.isRequired,
};

View file

@ -1,22 +1,18 @@
import React, { useState, useRef } from "react";
import PropTypes from "prop-types";
import "./ImageUpload.scss";
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import './ImageUpload.scss';
import initMatrix from "../../../client/initMatrix";
import initMatrix from '../../../client/initMatrix';
import Text from "../../atoms/text/Text";
import Avatar from "../../atoms/avatar/Avatar";
import Spinner from "../../atoms/spinner/Spinner";
import RawIcon from "../../atoms/system-icons/RawIcon";
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
import Spinner from '../../atoms/spinner/Spinner';
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({
text,
bgColor,
imageSrc,
onUpload,
onRequestRemove,
text, bgColor, imageSrc, onUpload, onRequestRemove,
size,
}) {
const [uploadPromise, setUploadPromise] = useState(null);
@ -30,7 +26,7 @@ function ImageUpload({
setUploadPromise(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);
} catch {
setUploadPromise(null);
@ -54,48 +50,40 @@ function ImageUpload({
uploadImageRef.current.click();
}}
>
<Avatar imageSrc={imageSrc} text={text} bgColor={bgColor} size={size} />
<div
className={`img-upload__process ${
uploadPromise === null ? " img-upload__process--stopped" : ""
}`}
>
{uploadPromise === null &&
(size === "large" ? (
<Text variant="b3" weight="bold">
Upload
</Text>
) : (
<RawIcon src={PlusIC} color="white" />
))}
<Avatar
imageSrc={imageSrc}
text={text}
bgColor={bgColor}
size={size}
/>
<div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
{uploadPromise === null && (
size === 'large'
? <Text variant="b3" weight="bold">Upload</Text>
: <RawIcon src={PlusIC} color="white" />
)}
{uploadPromise !== null && <Spinner size="small" />}
</div>
</button>
{(typeof imageSrc === "string" || uploadPromise !== null) && (
{ (typeof imageSrc === 'string' || uploadPromise !== null) && (
<button
className="img-upload__btn-cancel"
type="button"
onClick={uploadPromise === null ? onRequestRemove : cancelUpload}
>
<Text variant="b3">{uploadPromise ? "Cancel" : "Remove"}</Text>
<Text variant="b3">{uploadPromise ? 'Cancel' : 'Remove'}</Text>
</button>
)}
<input
onChange={uploadImage}
style={{ display: "none" }}
ref={uploadImageRef}
type="file"
accept="image/*"
/>
<input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" accept="image/*" />
</div>
);
}
ImageUpload.defaultProps = {
text: null,
bgColor: "transparent",
bgColor: 'transparent',
imageSrc: null,
size: "large",
size: 'large',
};
ImageUpload.propTypes = {
@ -104,7 +92,7 @@ ImageUpload.propTypes = {
imageSrc: PropTypes.string,
onUpload: PropTypes.func.isRequired,
onRequestRemove: PropTypes.func.isRequired,
size: PropTypes.oneOf(["large", "normal"]),
size: PropTypes.oneOf(['large', 'normal']),
};
export default ImageUpload;

View file

@ -16,7 +16,7 @@
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.6);
background-color: rgba(0, 0, 0, .6);
position: absolute;
left: 0;
@ -30,7 +30,7 @@
display: none;
}
& .donut-spinner {
border-color: rgb(255, 255, 255, 0.3);
border-color: rgb(255, 255, 255, .3);
border-left-color: white;
}
}
@ -38,11 +38,12 @@
display: flex;
}
&__btn-cancel {
margin-top: var(--sp-extra-tight);
cursor: pointer;
& .text {
color: var(--tc-danger-normal);
color: var(--tc-danger-normal)
}
}
}

View file

@ -1,18 +1,18 @@
import React, { useState, useEffect, useRef } from "react";
import "./ExportE2ERoomKeys.scss";
import React, { useState, useEffect, useRef } from 'react';
import './ExportE2ERoomKeys.scss';
import FileSaver from "file-saver";
import FileSaver from 'file-saver';
import initMatrix from "../../../client/initMatrix";
import cons from "../../../client/state/cons";
import { encryptMegolmKeyFile } from "../../../util/cryptE2ERoomKeys";
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { encryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
import Text from "../../atoms/text/Text";
import Button from "../../atoms/button/Button";
import Input from "../../atoms/input/Input";
import Spinner from "../../atoms/spinner/Spinner";
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from "../../hooks/useStore";
import { useStore } from '../../hooks/useStore';
function ExportE2ERoomKeys() {
const isMountStore = useStore();
@ -29,14 +29,14 @@ function ExportE2ERoomKeys() {
if (password !== confirmPasswordRef.current.value) {
setStatus({
isOngoing: false,
msg: "Password does not match.",
msg: 'Password does not match.',
type: cons.status.ERROR,
});
return;
}
setStatus({
isOngoing: true,
msg: "Getting keys...",
msg: 'Getting keys...',
type: cons.status.IN_FLIGHT,
});
try {
@ -44,22 +44,19 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: "Encrypting keys...",
msg: 'Encrypting keys...',
type: cons.status.IN_FLIGHT,
});
}
const encKeys = await encryptMegolmKeyFile(
JSON.stringify(keys),
password
);
const encKeys = await encryptMegolmKeyFile(JSON.stringify(keys), password);
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()) {
setStatus({
isOngoing: false,
msg: "Successfully exported all keys.",
msg: 'Successfully exported all keys.',
type: cons.status.SUCCESS,
});
}
@ -67,7 +64,7 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
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,
});
}
@ -83,28 +80,10 @@ function ExportE2ERoomKeys() {
return (
<div className="export-e2e-room-keys">
<form
className="export-e2e-room-keys__form"
onSubmit={(e) => {
e.preventDefault();
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 className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); 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>
{ status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process">
@ -112,16 +91,8 @@ function ExportE2ERoomKeys() {
<Text variant="b2">{status.msg}</Text>
</div>
)}
{status.type === cons.status.SUCCESS && (
<Text className="import-e2e-room-keys__success" variant="b2">
{status.msg}
</Text>
)}
{status.type === cons.status.ERROR && (
<Text className="import-e2e-room-keys__error" variant="b2">
{status.msg}
</Text>
)}
{status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
{status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
</div>
);
}

View file

@ -24,4 +24,5 @@
margin-top: var(--sp-tight);
color: var(--tc-danger-high);
}
}

View file

@ -1,19 +1,19 @@
import React, { useState, useEffect, useRef } from "react";
import "./ImportE2ERoomKeys.scss";
import React, { useState, useEffect, useRef } from 'react';
import './ImportE2ERoomKeys.scss';
import initMatrix from "../../../client/initMatrix";
import cons from "../../../client/state/cons";
import { decryptMegolmKeyFile } from "../../../util/cryptE2ERoomKeys";
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { decryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
import Text from "../../atoms/text/Text";
import IconButton from "../../atoms/button/IconButton";
import Button from "../../atoms/button/Button";
import Input from "../../atoms/input/Input";
import Spinner from "../../atoms/spinner/Spinner";
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
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() {
const isMountStore = useStore();
@ -32,7 +32,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: "Decrypting file...",
msg: 'Decrypting file...',
type: cons.status.IN_FLIGHT,
});
}
@ -41,7 +41,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: true,
msg: "Decrypting messages...",
msg: 'Decrypting messages...',
type: cons.status.IN_FLIGHT,
});
}
@ -49,7 +49,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
isOngoing: false,
msg: "Successfully imported all keys.",
msg: 'Successfully imported all keys.',
type: cons.status.SUCCESS,
});
inputRef.current.value = null;
@ -59,7 +59,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) {
setStatus({
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,
});
}
@ -68,7 +68,7 @@ function ImportE2ERoomKeys() {
const importE2ERoomKeys = () => {
const password = passwordRef.current.value;
if (password === "" || keyFile === null) return;
if (password === '' || keyFile === null) return;
if (status.isOngoing) return;
tryDecrypt(keyFile, password);
@ -76,7 +76,7 @@ function ImportE2ERoomKeys() {
const handleFileChange = (e) => {
const file = e.target.files.item(0);
passwordRef.current.value = "";
passwordRef.current.value = '';
setKeyFile(file);
setStatus({
isOngoing: false,
@ -105,42 +105,18 @@ function ImportE2ERoomKeys() {
return (
<div className="import-e2e-room-keys">
<input
ref={inputRef}
onChange={handleFileChange}
style={{ display: "none" }}
type="file"
/>
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" />
<form
className="import-e2e-room-keys__form"
onSubmit={(e) => {
e.preventDefault();
importE2ERoomKeys();
}}
>
<form className="import-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); importE2ERoomKeys(); }}>
{ keyFile !== null && (
<div className="import-e2e-room-keys__file">
<IconButton
onClick={removeImportKeysFile}
src={CirclePlusIC}
tooltip="Remove file"
/>
<IconButton onClick={removeImportKeysFile} src={CirclePlusIC} tooltip="Remove file" />
<Text>{keyFile.name}</Text>
</div>
)}
{keyFile === null && (
<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>
{keyFile === null && <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>
</form>
{ status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process">
@ -148,16 +124,8 @@ function ImportE2ERoomKeys() {
<Text variant="b2">{status.msg}</Text>
</div>
)}
{status.type === cons.status.SUCCESS && (
<Text className="import-e2e-room-keys__success" variant="b2">
{status.msg}
</Text>
)}
{status.type === cons.status.ERROR && (
<Text className="import-e2e-room-keys__error" variant="b2">
{status.msg}
</Text>
)}
{status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
{status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
</div>
);
}

View file

@ -1,5 +1,5 @@
@use "../../partials/text";
@use "../../partials/dir";
@use '../../partials/text';
@use '../../partials/dir';
.import-e2e-room-keys {
&__file {
@ -34,6 +34,7 @@
display: flex;
margin-top: var(--sp-extra-tight);
& .input-container {
flex: 1;
margin: 0 var(--sp-tight);

View file

@ -1,20 +1,20 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import "./Media.scss";
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './Media.scss';
import encrypt from "browser-encrypt-attachment";
import encrypt from 'browser-encrypt-attachment';
import { BlurhashCanvas } from "react-blurhash";
import Text from "../../atoms/text/Text";
import IconButton from "../../atoms/button/IconButton";
import Spinner from "../../atoms/spinner/Spinner";
import ImageLightbox from "../image-lightbox/ImageLightbox";
import { BlurhashCanvas } from 'react-blurhash';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Spinner from '../../atoms/spinner/Spinner';
import ImageLightbox from '../image-lightbox/ImageLightbox';
import DownloadSVG from "../../../../public/res/ic/outlined/download.svg";
import ExternalSVG from "../../../../public/res/ic/outlined/external.svg";
import PlaySVG from "../../../../public/res/ic/outlined/play.svg";
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
import { getBlobSafeMimeType } from "../../../util/mimetypes";
import { getBlobSafeMimeType } from '../../../util/mimetypes';
async function getDecryptedBlob(response, type, decryptData) {
const arrayBuffer = await response.arrayBuffer();
@ -25,11 +25,9 @@ async function getDecryptedBlob(response, type, decryptData) {
async function getUrl(link, type, decryptData) {
try {
const response = await fetch(link, { method: "GET" });
const response = await fetch(link, { method: 'GET' });
if (decryptData !== null) {
return URL.createObjectURL(
await getDecryptedBlob(response, type, decryptData)
);
return URL.createObjectURL(await getDecryptedBlob(response, type, decryptData));
}
const blob = await response.blob();
return URL.createObjectURL(blob);
@ -43,7 +41,10 @@ function getNativeHeight(width, height, maxWidth = 296) {
return scale * height;
}
function FileHeader({ name, link, external, file, type }) {
function FileHeader({
name, link, external,
file, type,
}) {
const [url, setUrl] = useState(null);
async function getFile() {
@ -60,25 +61,20 @@ function FileHeader({ name, link, external, file, type }) {
}
return (
<div className="file-header">
<Text className="file-name" variant="b3">
{name}
</Text>
<Text className="file-name" variant="b3">{name}</Text>
{ link !== null && (
<>
{external && (
{
external && (
<IconButton
size="extra-small"
tooltip="Open in new tab"
src={ExternalSVG}
onClick={() => window.open(url || link)}
/>
)}
<a
href={url || link}
download={name}
target="_blank"
rel="noreferrer"
>
)
}
<a href={url || link} download={name} target="_blank" rel="noreferrer">
<IconButton
size="extra-small"
tooltip="Download"
@ -104,7 +100,9 @@ FileHeader.propTypes = {
type: PropTypes.string.isRequired,
};
function File({ name, link, file, type }) {
function File({
name, link, file, type,
}) {
return (
<div className="file-container">
<FileHeader name={name} link={link} file={file} type={type} />
@ -113,7 +111,7 @@ function File({ name, link, file, type }) {
}
File.defaultProps = {
file: null,
type: "",
type: '',
};
File.propTypes = {
name: PropTypes.string.isRequired,
@ -122,7 +120,9 @@ File.propTypes = {
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 [blur, setBlur] = useState(true);
const [lightbox, setLightbox] = useState(false);
@ -149,9 +149,7 @@ function Image({ name, width, height, link, file, type, blurhash }) {
<>
<div className="file-container">
<div
style={{
height: width !== null ? getNativeHeight(width, height) : "unset",
}}
style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }}
className="image-container"
role="button"
tabIndex="0"
@ -161,7 +159,7 @@ function Image({ name, width, height, link, file, type, blurhash }) {
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
{ url !== null && (
<img
style={{ display: blur ? "none" : "unset" }}
style={{ display: blur ? 'none' : 'unset' }}
onLoad={() => setBlur(false)}
src={url || link}
alt={name}
@ -184,8 +182,8 @@ Image.defaultProps = {
file: null,
width: null,
height: null,
type: "",
blurhash: "",
type: '',
blurhash: '',
};
Image.propTypes = {
name: PropTypes.string.isRequired,
@ -197,7 +195,9 @@ Image.propTypes = {
blurhash: PropTypes.string,
};
function Sticker({ name, height, width, link, file, type }) {
function Sticker({
name, height, width, link, file, type,
}) {
const [url, setUrl] = useState(null);
useEffect(() => {
@ -214,19 +214,14 @@ function Sticker({ name, height, width, link, file, type }) {
}, []);
return (
<div
className="sticker-container"
style={{
height: width !== null ? getNativeHeight(width, height, 128) : "unset",
}}
>
<div className="sticker-container" style={{ height: width !== null ? getNativeHeight(width, height, 128) : 'unset' }}>
{ url !== null && <img src={url || link} title={name} alt={name} />}
</div>
);
}
Sticker.defaultProps = {
file: null,
type: "",
type: '',
width: null,
height: null,
};
@ -239,7 +234,9 @@ Sticker.propTypes = {
type: PropTypes.string,
};
function Audio({ name, link, type, file }) {
function Audio({
name, link, type, file,
}) {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
@ -255,21 +252,10 @@ function Audio({ name, link, type, file }) {
return (
<div className="file-container">
<FileHeader
name={name}
link={file !== null ? url : url || link}
type={type}
external
/>
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div className="audio-container">
{ url === null && isLoading && <Spinner size="small" /> }
{url === null && !isLoading && (
<IconButton
onClick={handlePlayAudio}
tooltip="Play audio"
src={PlaySVG}
/>
)}
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
{ url !== null && (
/* eslint-disable-next-line jsx-a11y/media-has-caption */
<audio autoPlay controls>
@ -282,7 +268,7 @@ function Audio({ name, link, type, file }) {
}
Audio.defaultProps = {
file: null,
type: "",
type: '',
};
Audio.propTypes = {
name: PropTypes.string.isRequired,
@ -292,16 +278,8 @@ Audio.propTypes = {
};
function Video({
name,
link,
thumbnail,
thumbnailFile,
thumbnailType,
width,
height,
file,
type,
blurhash,
name, link, thumbnail, thumbnailFile, thumbnailType,
width, height, file, type, blurhash,
}) {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
@ -334,15 +312,10 @@ function Video({
return (
<div className="file-container">
<FileHeader
name={name}
link={file !== null ? url : url || link}
type={type}
external
/>
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div
style={{
height: width !== null ? getNativeHeight(width, height) : "unset",
height: width !== null ? getNativeHeight(width, height) : 'unset',
}}
className="video-container"
>
@ -350,21 +323,10 @@ function Video({
<>
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
{ thumbUrl !== null && (
<img
style={{ display: blur ? "none" : "unset" }}
src={thumbUrl}
onLoad={() => setBlur(false)}
alt={name}
/>
<img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
)}
{isLoading && <Spinner size="small" />}
{!isLoading && (
<IconButton
onClick={handlePlayVideo}
tooltip="Play video"
src={PlaySVG}
/>
)}
{!isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
</>
) : (
/* eslint-disable-next-line jsx-a11y/media-has-caption */
@ -383,7 +345,7 @@ Video.defaultProps = {
thumbnail: null,
thumbnailType: null,
thumbnailFile: null,
type: "",
type: '',
blurhash: null,
};
Video.propTypes = {
@ -399,4 +361,6 @@ Video.propTypes = {
blurhash: PropTypes.string,
};
export { File, Image, Sticker, Audio, Video };
export {
File, Image, Sticker, Audio, Video,
};

View file

@ -1,4 +1,4 @@
@use "../../partials/text";
@use '../../partials/text';
.file-header {
display: flex;

View file

@ -1,55 +1,46 @@
/* eslint-disable react/prop-types */
import React, { useState, useEffect, useCallback, useRef } from "react";
import PropTypes from "prop-types";
import "./Message.scss";
import React, {
useState, useEffect, useCallback, useRef,
} 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 {
getUsername,
getUsernameOfRoomMember,
parseReply,
trimHTMLReply,
} from "../../../util/matrixUtil";
import colorMXID from "../../../util/colorMXID";
import { getEventCords } from "../../../util/common";
import { redactEvent, sendReaction } from "../../../client/action/roomTimeline";
getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply,
} from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID';
import { getEventCords } from '../../../util/common';
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
import {
openEmojiBoard,
openProfileViewer,
openReadReceipts,
openViewSource,
replyTo,
} from "../../../client/action/navigation";
import { sanitizeCustomHtml } from "../../../util/sanitize";
openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
} from '../../../client/action/navigation';
import { sanitizeCustomHtml } from '../../../util/sanitize';
import Text from "../../atoms/text/Text";
import RawIcon from "../../atoms/system-icons/RawIcon";
import Button from "../../atoms/button/Button";
import Tooltip from "../../atoms/tooltip/Tooltip";
import Input from "../../atoms/input/Input";
import Avatar from "../../atoms/avatar/Avatar";
import IconButton from "../../atoms/button/IconButton";
import Time from "../../atoms/time/Time";
import ContextMenu, {
MenuHeader,
MenuItem,
MenuBorder,
} from "../../atoms/context-menu/ContextMenu";
import * as Media from "../media/Media";
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
import Button from '../../atoms/button/Button';
import Tooltip from '../../atoms/tooltip/Tooltip';
import Input from '../../atoms/input/Input';
import Avatar from '../../atoms/avatar/Avatar';
import IconButton from '../../atoms/button/IconButton';
import Time from '../../atoms/time/Time';
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
import * as Media from '../media/Media';
import ReplyArrowIC from "../../../../public/res/ic/outlined/reply-arrow.svg";
import EmojiAddIC from "../../../../public/res/ic/outlined/emoji-add.svg";
import VerticalMenuIC from "../../../../public/res/ic/outlined/vertical-menu.svg";
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
import TickMarkIC from "../../../../public/res/ic/outlined/tick-mark.svg";
import CmdIC from "../../../../public/res/ic/outlined/cmd.svg";
import BinIC from "../../../../public/res/ic/outlined/bin.svg";
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { confirmDialog } from "../confirm-dialog/ConfirmDialog";
import { getBlobSafeMimeType } from "../../../util/mimetypes";
import { html, plain } from "../../../util/markdown";
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import { getBlobSafeMimeType } from '../../../util/mimetypes';
import { html, plain } from '../../../util/markdown';
function PlaceholderMessage() {
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">
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
<Avatar
imageSrc={avatarSrc}
text={username}
bgColor={colorMXID(userId)}
size="small"
/>
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
</button>
</div>
));
const MessageHeader = React.memo(
({ userId, username, timestamp, fullTime }) => (
const MessageHeader = React.memo(({
userId, username, timestamp, fullTime,
}) => (
<div className="message__header">
<Text
style={{ color: colorMXID(userId) }}
@ -102,8 +91,7 @@ const MessageHeader = React.memo(
</Text>
</div>
</div>
)
);
));
MessageHeader.defaultProps = {
fullTime: false,
};
@ -119,7 +107,9 @@ function MessageReply({ name, color, body }) {
<div className="message__reply">
<Text variant="b2">
<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>
</div>
);
@ -153,11 +143,9 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
const username = getUsernameOfRoomMember(mEvent.sender);
if (isMountedRef.current === false) return;
const fallbackBody = mEvent.isRedacted()
? "*** This message has been deleted ***"
: "*** Unable to load reply ***";
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
if (editedList && parsedBody.startsWith(" * ")) {
if (editedList && parsedBody.startsWith(' * ')) {
parsedBody = parsedBody.slice(3);
}
@ -169,9 +157,9 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
});
} catch {
setReply({
to: "** Unknown user **",
color: "var(--tc-danger-normal)",
body: "*** Unable to load reply ***",
to: '** Unknown user **',
color: 'var(--tc-danger-normal)',
body: '*** Unable to load reply ***',
event: null,
});
}
@ -184,7 +172,7 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
}, []);
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 (reply?.event === null) return;
if (reply?.event.isRedacted()) return;
@ -200,9 +188,7 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
role="button"
tabIndex="0"
>
{reply !== null && (
<MessageReply name={reply.to} color={reply.color} body={reply.body} />
)}
{reply !== null && <MessageReply name={reply.to} color={reply.color} body={reply.body} />}
</div>
);
});
@ -211,11 +197,15 @@ MessageReplyWrapper.propTypes = {
eventId: PropTypes.string.isRequired,
};
const MessageBody = React.memo(
({ senderName, body, isCustomHTML, isEdited, msgType }) => {
const MessageBody = React.memo(({
senderName,
body,
isCustomHTML,
isEdited,
msgType,
}) => {
// if body is not string it is a React element.
if (typeof body !== "string")
return <div className="message__body">{body}</div>;
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
let content = null;
if (isCustomHTML) {
@ -225,10 +215,10 @@ const MessageBody = React.memo(
undefined,
true,
false,
true
true,
);
} catch {
console.error("Malformed custom html: ", body);
console.error('Malformed custom html: ', body);
content = twemojify(body, undefined);
}
} else {
@ -240,24 +230,20 @@ const MessageBody = React.memo(
// - Contains only emoji
// - Contains no more than 10 emoji
let emojiOnly = false;
if (content.type === "img") {
if (content.type === 'img') {
// If this messages contains only a single (inline) image
emojiOnly = true;
} else if (content.constructor.name === "Array") {
} else if (content.constructor.name === 'Array') {
// Otherwise, it might be an array of images / texb
// Count the number of emojis
const nEmojis = content.filter((e) => e.type === "img").length;
const nEmojis = content.filter((e) => e.type === 'img').length;
// Make sure there's no text besides whitespace and variation selector U+FE0F
if (
nEmojis <= 10 &&
content.every(
(element) =>
(typeof element === "object" && element.type === "img") ||
(typeof element === "string" && /^[\s\ufe0f]*$/g.test(element))
)
) {
if (nEmojis <= 10 && content.every((element) => (
(typeof element === 'object' && element.type === 'img')
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
))) {
emojiOnly = true;
}
}
@ -265,29 +251,25 @@ const MessageBody = React.memo(
if (!isCustomHTML) {
// If this is a plaintext message, wrap it in a <p> element (automatically applying
// white-space: pre-wrap) in order to preserve newlines
content = <p className="message__body-plain">{content}</p>;
content = (<p className="message__body-plain">{content}</p>);
}
return (
<div className="message__body">
<div dir="auto" className={`text ${emojiOnly ? "text-h1" : "text-b1"}`}>
{msgType === "m.emote" && (
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
{ msgType === 'm.emote' && (
<>
{"* "}
{twemojify(senderName)}{" "}
{'* '}
{twemojify(senderName)}
{' '}
</>
)}
{ content }
</div>
{isEdited && (
<Text className="message__body-edited" variant="b3">
(edited)
</Text>
)}
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
</div>
);
}
);
});
MessageBody.defaultProps = {
isCustomHTML: false,
isEdited: false,
@ -306,30 +288,24 @@ function MessageEdit({ body, onSave, onCancel }) {
useEffect(() => {
// 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;
}, []);
const handleKeyDown = (e) => {
if (e.key === "Escape") {
if (e.key === 'Escape') {
e.preventDefault();
onCancel();
}
if (e.key === "Enter" && e.shiftKey === false) {
if (e.key === 'Enter' && e.shiftKey === false) {
e.preventDefault();
onSave(editInputRef.current.value, body);
}
};
return (
<form
className="message__edit"
onSubmit={(e) => {
e.preventDefault();
onSave(editInputRef.current.value, body);
}}
>
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value, body); }}>
<Input
forwardRef={editInputRef}
onKeyDown={handleKeyDown}
@ -340,9 +316,7 @@ function MessageEdit({ body, onSave, onCancel }) {
autoFocus
/>
<div className="message__edit-btns">
<Button type="submit" variant="primary">
Save
</Button>
<Button type="submit" variant="primary">Save</Button>
<Button onClick={onCancel}>Cancel</Button>
</div>
</form>
@ -360,10 +334,7 @@ function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
let rEvent = null;
rEvents?.find((rE) => {
if (rE.getRelation() === null) return false;
if (
rE.getRelation().key === emojiKey &&
rE.getSender() === mx.getUserId()
) {
if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
rEvent = rE;
return true;
}
@ -376,7 +347,7 @@ function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
if (myAlreadyReactEvent) {
const rId = myAlreadyReactEvent.getId();
if (rId.startsWith("~")) return;
if (rId.startsWith('~')) return;
redactEvent(roomId, rId);
return;
}
@ -385,13 +356,7 @@ function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
function pickEmoji(e, roomId, eventId, roomTimeline) {
openEmojiBoard(getEventCords(e), (emoji) => {
toggleEmoji(
roomId,
eventId,
emoji.mxc ?? emoji.unicode,
emoji.shortcodes[0],
roomTimeline
);
toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
e.target.click();
});
}
@ -403,27 +368,20 @@ function genReactionMsg(userIds, reaction, shortcode) {
<React.Fragment key={userId}>
{twemojify(getUsername(userId))}
{index < userIds.length - 1 && (
<span style={{ opacity: ".6" }}>
{index === userIds.length - 2 ? " and " : ", "}
<span style={{ opacity: '.6' }}>
{index === userIds.length - 2 ? ' and ' : ', '}
</span>
)}
</React.Fragment>
))}
<span style={{ opacity: ".6" }}>{" reacted with "}</span>
{twemojify(shortcode ? `:${shortcode}:` : reaction, {
className: "react-emoji",
})}
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
</>
);
}
function MessageReaction({
reaction,
shortcode,
count,
users,
isActive,
onClick,
reaction, shortcode, count, users, isActive, onClick,
}) {
let customEmojiUrl = null;
if (reaction.match(/^mxc:\/\/\S+$/)) {
@ -432,32 +390,19 @@ function MessageReaction({
return (
<Tooltip
className="msg__reaction-tooltip"
content={
<Text variant="b2">
{users.length > 0
? genReactionMsg(users, reaction, shortcode)
: "Unable to load who has reacted"}
</Text>
}
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
>
<button
onClick={onClick}
type="button"
className={`msg__reaction${isActive ? " msg__reaction--active" : ""}`}
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
>
{customEmojiUrl ? (
<img
className="react-emoji"
draggable="false"
alt={shortcode ?? reaction}
src={customEmojiUrl}
/>
) : (
twemojify(reaction, { className: "react-emoji" })
)}
<Text variant="b3" className="msg__reaction-count">
{count}
</Text>
{
customEmojiUrl
? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
: twemojify(reaction, { className: 'react-emoji' })
}
<Text variant="b3" className="msg__reaction-count">{count}</Text>
</button>
</Tooltip>
);
@ -478,10 +423,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
const { roomId, room, reactionTimeline } = roomTimeline;
const mx = initMatrix.matrixClient;
const reactions = {};
const canSendReaction = room.currentState.maySendEvent(
"m.reaction",
mx.getUserId()
);
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
const eventReactions = reactionTimeline.get(mEvent.getId());
const addReaction = (key, shortcode, count, senderId, isActive) => {
@ -516,18 +458,18 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
});
} else {
// Use aggregated reactions
const aggregatedReaction =
mEvent.getServerAggregatedRelation("m.annotation")?.chunk;
const aggregatedReaction = mEvent.getServerAggregatedRelation('m.annotation')?.chunk;
if (!aggregatedReaction) return null;
aggregatedReaction.forEach((reaction) => {
if (reaction.type !== "m.reaction") return;
if (reaction.type !== 'm.reaction') return;
addReaction(reaction.key, undefined, reaction.count, undefined, false);
});
}
return (
<div className="message__reactions text text-b3 noselect">
{Object.keys(reactions).map((key) => (
{
Object.keys(reactions).map((key) => (
<MessageReaction
key={key}
reaction={key}
@ -536,16 +478,11 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
users={reactions[key].users}
isActive={reactions[key].isActive}
onClick={() => {
toggleEmoji(
roomId,
mEvent.getId(),
key,
reactions[key].shortcode,
roomTimeline
);
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
}}
/>
))}
))
}
{canSendReaction && (
<IconButton
onClick={(e) => {
@ -566,11 +503,11 @@ MessageReactionGroup.propTypes = {
function isMedia(mE) {
return (
mE.getContent()?.msgtype === "m.file" ||
mE.getContent()?.msgtype === "m.image" ||
mE.getContent()?.msgtype === "m.audio" ||
mE.getContent()?.msgtype === "m.video" ||
mE.getType() === "m.sticker"
mE.getContent()?.msgtype === 'm.file'
|| mE.getContent()?.msgtype === 'm.image'
|| mE.getContent()?.msgtype === 'm.audio'
|| mE.getContent()?.msgtype === 'm.video'
|| mE.getType() === 'm.sticker'
);
}
@ -586,20 +523,16 @@ function handleOpenViewSource(mEvent, roomTimeline) {
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 mx = initMatrix.matrixClient;
const senderId = mEvent.getSender();
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
const canIRedact = room.currentState.hasSufficientPowerLevelFor(
"redact",
myPowerlevel
);
const canSendReaction = room.currentState.maySendEvent(
"m.reaction",
mx.getUserId()
);
const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
return (
<div className="message__options">
@ -617,7 +550,7 @@ const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
size="extra-small"
tooltip="Reply"
/>
{senderId === mx.getUserId() && !isMedia(mEvent) && (
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
<IconButton
onClick={() => edit(true)}
src={PencilIC}
@ -631,9 +564,7 @@ const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
<MenuHeader>Options</MenuHeader>
<MenuItem
iconSrc={TickMarkIC}
onClick={() =>
openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))
}
onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))}
>
Read receipts
</MenuItem>
@ -651,10 +582,10 @@ const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
iconSrc={BinIC}
onClick={async () => {
const isConfirmed = await confirmDialog(
"Delete message",
"Are you sure that you want to delete this message?",
"Delete",
"danger"
'Delete message',
'Are you sure that you want to delete this message?',
'Delete',
'danger',
);
if (!isConfirmed) return;
redactEvent(roomId, mEvent.getId());
@ -688,30 +619,28 @@ MessageOptions.propTypes = {
function genMediaContent(mE) {
const mx = initMatrix.matrixClient;
const mContent = mE.getContent();
if (!mContent || !mContent.body)
return <span style={{ color: "var(--bg-danger)" }}>Malformed event</span>;
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
let mediaMXC = mContent?.url;
const isEncryptedFile = typeof mediaMXC === "undefined";
const isEncryptedFile = typeof mediaMXC === 'undefined';
if (isEncryptedFile) mediaMXC = mContent?.file?.url;
let thumbnailMXC = mContent?.info?.thumbnail_url;
if (typeof mediaMXC === "undefined" || mediaMXC === "")
return <span style={{ color: "var(--bg-danger)" }}>Malformed event</span>;
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
let msgType = mE.getContent()?.msgtype;
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
if (mE.getType() === "m.sticker") {
msgType = "m.sticker";
} else if (safeMimetype === "application/octet-stream") {
msgType = "m.file";
if (mE.getType() === 'm.sticker') {
msgType = 'm.sticker';
} else if (safeMimetype === 'application/octet-stream') {
msgType = 'm.file';
}
const blurhash = mContent?.info?.["xyz.amorgan.blurhash"];
const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
switch (msgType) {
case "m.file":
case 'm.file':
return (
<Media.File
name={mContent.body}
@ -720,34 +649,30 @@ function genMediaContent(mE) {
file={mContent.file || null}
/>
);
case "m.image":
case 'm.image':
return (
<Media.Image
name={mContent.body}
width={typeof mContent.info?.w === "number" ? mContent.info?.w : null}
height={
typeof mContent.info?.h === "number" ? mContent.info?.h : null
}
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
link={mx.mxcUrlToHttp(mediaMXC)}
file={isEncryptedFile ? mContent.file : null}
type={mContent.info?.mimetype}
blurhash={blurhash}
/>
);
case "m.sticker":
case 'm.sticker':
return (
<Media.Sticker
name={mContent.body}
width={typeof mContent.info?.w === "number" ? mContent.info?.w : null}
height={
typeof mContent.info?.h === "number" ? mContent.info?.h : null
}
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
link={mx.mxcUrlToHttp(mediaMXC)}
file={isEncryptedFile ? mContent.file : null}
type={mContent.info?.mimetype}
/>
);
case "m.audio":
case 'm.audio':
return (
<Media.Audio
name={mContent.body}
@ -756,38 +681,34 @@ function genMediaContent(mE) {
file={mContent.file || null}
/>
);
case "m.video":
if (typeof thumbnailMXC === "undefined") {
case 'm.video':
if (typeof thumbnailMXC === 'undefined') {
thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
}
return (
<Media.Video
name={mContent.body}
link={mx.mxcUrlToHttp(mediaMXC)}
thumbnail={
thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)
}
thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
width={typeof mContent.info?.w === "number" ? mContent.info?.w : null}
height={
typeof mContent.info?.h === "number" ? mContent.info?.h : null
}
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
file={isEncryptedFile ? mContent.file : null}
type={mContent.info?.mimetype}
blurhash={blurhash}
/>
);
default:
return <span style={{ color: "var(--bg-danger)" }}>Malformed event</span>;
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
}
}
function getEditedBody(editedMEvent) {
const newContent = editedMEvent.getContent()["m.new_content"];
if (typeof newContent === "undefined") return [null, false, null];
const newContent = editedMEvent.getContent()['m.new_content'];
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);
if (parsedContent === null) {
return [newContent.body, isCustomHTML, newContent.formatted_body ?? null];
@ -796,39 +717,22 @@ function getEditedBody(editedMEvent) {
}
function Message({
mEvent,
isBodyOnly,
roomTimeline,
focus,
fullTime,
isEdit,
setEdit,
cancelEdit,
mEvent, isBodyOnly, roomTimeline,
focus, fullTime, isEdit, setEdit, cancelEdit,
}) {
const roomId = mEvent.getRoomId();
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
const className = [
"message",
isBodyOnly ? "message--body-only" : "message--full",
];
if (focus) className.push("message--focus");
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
if (focus) className.push('message--focus');
const content = mEvent.getContent();
const eventId = mEvent.getId();
const msgType = content?.msgtype;
const senderId = mEvent.getSender();
let { body } = content;
const username = mEvent.sender
? getUsernameOfRoomMember(mEvent.sender)
: getUsername(senderId);
const avatarSrc =
mEvent.sender?.getAvatarUrl(
initMatrix.matrixClient.baseUrl,
36,
36,
"crop"
) ?? null;
let isCustomHTML = content.format === "org.matrix.custom.html";
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
let isCustomHTML = content.format === 'org.matrix.custom.html';
let customHTML = isCustomHTML ? content.formatted_body : null;
const edit = useCallback(() => {
@ -838,12 +742,11 @@ function Message({
replyTo(senderId, mEvent.getId(), 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 haveReactions = roomTimeline
? reactionTimeline.has(eventId) ||
!!mEvent.getServerAggregatedRelation("m.annotation")
? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
: false;
const isReply = !!mEvent.replyEventId;
@ -858,20 +761,22 @@ function Message({
customHTML = trimHTMLReply(customHTML);
}
if (typeof body !== "string") body = "";
if (typeof body !== 'string') body = '';
return (
<div className={className.join(" ")}>
{isBodyOnly ? (
<div className="message__avatar-container" />
) : (
<div className={className.join(' ')}>
{
isBodyOnly
? <div className="message__avatar-container" />
: (
<MessageAvatar
roomId={roomId}
avatarSrc={avatarSrc}
userId={senderId}
username={username}
/>
)}
)
}
<div className="message__main-container">
{!isBodyOnly && (
<MessageHeader
@ -891,27 +796,19 @@ function Message({
<MessageBody
senderName={username}
isCustomHTML={isCustomHTML}
body={
isMedia(mEvent) ? genMediaContent(mEvent) : customHTML ?? body
}
body={isMedia(mEvent) ? genMediaContent(mEvent) : customHTML ?? body}
msgType={msgType}
isEdited={isEdited}
/>
)}
{isEdit && (
<MessageEdit
body={
customHTML
? html(customHTML, { kind: "edit", onlyPlain: true }).plain
: plain(body, { kind: "edit", onlyPlain: true }).plain
}
body={(customHTML
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
: plain(body, { kind: 'edit', onlyPlain: true }).plain)}
onSave={(newBody, oldBody) => {
if (newBody !== oldBody) {
initMatrix.roomsInput.sendEditedMessage(
roomId,
mEvent,
newBody
);
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
}
cancelEdit();
}}

Some files were not shown because too many files have changed in this diff Show more