diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 85499c1..2189f7f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -patreon: ajbura +open_collective: cinny liberapay: ajbura \ No newline at end of file diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..bada578 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,34 @@ +name: Publish Docker image + +on: + release: + types: [published] + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: ajbura/cinny + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/netlify-dev.yaml b/.github/workflows/netlify-dev.yaml new file mode 100644 index 0000000..b08a5c8 --- /dev/null +++ b/.github/workflows/netlify-dev.yaml @@ -0,0 +1,21 @@ +name: 'Deploy to Netlify (dev)' + +on: + push: + branches: + - dev + +jobs: + deploy: + name: 'Deploy' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: jsmrcaga/action-netlify-deploy@master + with: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE2_ID }} + BUILD_DIRECTORY: "dist" + NETLIFY_DEPLOY_MESSAGE: "Dev deploy v${{ github.ref }}" + NETLIFY_DEPLOY_TO_PROD: true \ No newline at end of file diff --git a/.github/workflows/netlify-prod.yaml b/.github/workflows/netlify-prod.yaml new file mode 100644 index 0000000..5d7f789 --- /dev/null +++ b/.github/workflows/netlify-prod.yaml @@ -0,0 +1,20 @@ +name: 'Deploy to Netlify (prod)' + +on: + release: + types: [published] + +jobs: + deploy: + name: 'Deploy' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: jsmrcaga/action-netlify-deploy@master + with: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + BUILD_DIRECTORY: "dist" + NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}" + NETLIFY_DEPLOY_TO_PROD: true \ No newline at end of file diff --git a/LICENSE b/LICENSE index ff85e6a..42d4c6c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Ajay Bura (ajbura) +Copyright (c) 2021 Ajay Bura (ajbura) and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 97205b7..e4995d9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, elegant and secure interface. +![preview](https://github.com/ajbura/cinny-site/blob/master/assets/preview-light.png) + ## Building and Running ### Building from source @@ -42,4 +44,10 @@ docker run -p 8080:80 cinny:latest This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`. +## License +Copyright (c) 2021 Ajay Bura (ajbura) and other contributors + +Code licensed under the MIT License: + +Graphics licensed under CC-BY 4.0: \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9db3222..00e5867 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -20,29 +20,272 @@ "dev": true }, "@babel/core": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.13.tgz", - "integrity": "sha512-1xEs9jZAyKIouOoCmpsgk/I26PoKyvzQ2ixdRpRzfbcp1fL+ozw7TUgdDgwonbTovqRaTfRh50IXuw4QrWO0GA==", + "version": "7.15.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.5.tgz", + "integrity": "sha512-pYgXxiwAgQpgM1bNkZsDEq85f0ggXMA5L7c+o3tskGMh2BunCI9QUwB9Z4jpvXUOuMdyGKiGKQiRe11VS6Jzvg==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", - "@babel/helper-compilation-targets": "^7.13.13", - "@babel/helper-module-transforms": "^7.13.12", - "@babel/helpers": "^7.13.10", - "@babel/parser": "^7.13.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.13", + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-module-transforms": "^7.15.4", + "@babel/helpers": "^7.15.4", + "@babel/parser": "^7.15.5", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", - "lodash": "^4.17.19", "semver": "^6.3.0", "source-map": "^0.5.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true + }, + "@babel/generator": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz", + "integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + } + }, + "@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz", + "integrity": "sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-simple-access": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/helper-validator-identifier": "^7.14.9", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.6.tgz", + "integrity": "sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==", + "dev": true + }, + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + }, + "browserslist": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.0.tgz", + "integrity": "sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001254", + "colorette": "^1.3.0", + "electron-to-chromium": "^1.3.830", + "escalade": "^3.1.1", + "node-releases": "^1.1.75" + } + }, + "caniuse-lite": { + "version": "1.0.30001257", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz", + "integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==", + "dev": true + }, + "colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.836", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.836.tgz", + "integrity": "sha512-Ney3pHOJBWkG/AqYjrW0hr2AUCsao+2uvq9HUlRP8OlpSdk/zOHOUJP7eu0icDvePC9DlgffuelP4TnOJmMRUg==", + "dev": true + }, + "node-releases": { + "version": "1.1.75", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", + "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", + "dev": true + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -303,14 +546,141 @@ } }, "@babel/helpers": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", - "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", "dev": true, "requires": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.0" + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/generator": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz", + "integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.6.tgz", + "integrity": "sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==", + "dev": true + }, + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/highlight": { @@ -3492,9 +3862,9 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, "requires": { "safe-buffer": "~5.1.1" diff --git a/package.json b/package.json index 6aaa680..8316b00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "1.2.0", + "version": "1.3.0", "description": "Yet another matrix client", "main": "index.js", "engines": { @@ -41,7 +41,7 @@ "twemoji": "^13.1.0" }, "devDependencies": { - "@babel/core": "^7.13.13", + "@babel/core": "^7.15.5", "@babel/preset-env": "^7.13.12", "@babel/preset-react": "^7.13.13", "assert": "^2.0.0", diff --git a/public/res/ic/filled/pin.svg b/public/res/ic/filled/pin.svg new file mode 100644 index 0000000..6a70147 --- /dev/null +++ b/public/res/ic/filled/pin.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/filled/star.svg b/public/res/ic/filled/star.svg new file mode 100644 index 0000000..378c891 --- /dev/null +++ b/public/res/ic/filled/star.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/public/res/ic/outlined/bell-off.svg b/public/res/ic/outlined/bell-off.svg new file mode 100644 index 0000000..79ce8a3 --- /dev/null +++ b/public/res/ic/outlined/bell-off.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ping.svg b/public/res/ic/outlined/bell-ping.svg new file mode 100644 index 0000000..3431bea --- /dev/null +++ b/public/res/ic/outlined/bell-ping.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ring.svg b/public/res/ic/outlined/bell-ring.svg new file mode 100644 index 0000000..57fc267 --- /dev/null +++ b/public/res/ic/outlined/bell-ring.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell.svg b/public/res/ic/outlined/bell.svg index d3d2f6d..43d470b 100644 --- a/public/res/ic/outlined/bell.svg +++ b/public/res/ic/outlined/bell.svg @@ -4,8 +4,7 @@ - - + + diff --git a/public/res/ic/outlined/eye.svg b/public/res/ic/outlined/eye.svg new file mode 100644 index 0000000..fb31e4f --- /dev/null +++ b/public/res/ic/outlined/eye.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/pin.svg b/public/res/ic/outlined/pin.svg new file mode 100644 index 0000000..211242c --- /dev/null +++ b/public/res/ic/outlined/pin.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/res/ic/outlined/star.svg b/public/res/ic/outlined/star.svg new file mode 100644 index 0000000..290f159 --- /dev/null +++ b/public/res/ic/outlined/star.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/app/atoms/badge/NotificationBadge.jsx b/src/app/atoms/badge/NotificationBadge.jsx index 897a201..c92815b 100644 --- a/src/app/atoms/badge/NotificationBadge.jsx +++ b/src/app/atoms/badge/NotificationBadge.jsx @@ -8,7 +8,7 @@ function NotificationBadge({ alert, content }) { const notificationClass = alert ? ' notification-badge--alert' : ''; return (
- {content && {content}} + {content !== null && {content}}
); } diff --git a/src/app/atoms/badge/NotificationBadge.scss b/src/app/atoms/badge/NotificationBadge.scss index d408500..c672b11 100644 --- a/src/app/atoms/badge/NotificationBadge.scss +++ b/src/app/atoms/badge/NotificationBadge.scss @@ -2,17 +2,18 @@ min-width: 16px; min-height: 8px; padding: 0 var(--sp-ultra-tight); - background-color: var(--tc-surface-low); + background-color: var(--bg-badge); border-radius: var(--bo-radius); .text { - color: white; + color: var(--tc-badge); text-align: center; font-weight: 700; } &--alert { - background-color: var(--bg-danger); + background-color: var(--bg-positive); + & .text { color: white } } &:empty { diff --git a/src/app/atoms/button/Button.jsx b/src/app/atoms/button/Button.jsx index b6e4a0f..bebca86 100644 --- a/src/app/atoms/button/Button.jsx +++ b/src/app/atoms/button/Button.jsx @@ -7,26 +7,29 @@ import RawIcon from '../system-icons/RawIcon'; import { blurOnBubbling } from './script'; function Button({ - id, variant, iconSrc, type, onClick, children, disabled, + id, className, variant, iconSrc, + type, onClick, children, disabled, }) { const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`; return ( ); } Button.defaultProps = { id: '', + className: null, variant: 'surface', iconSrc: null, type: 'button', @@ -36,7 +39,8 @@ Button.defaultProps = { Button.propTypes = { id: PropTypes.string, - variant: PropTypes.oneOf(['surface', 'primary', 'caution', 'danger']), + className: PropTypes.string, + variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), iconSrc: PropTypes.string, type: PropTypes.oneOf(['button', 'submit']), onClick: PropTypes.func, diff --git a/src/app/atoms/button/Button.scss b/src/app/atoms/button/Button.scss index 224c634..1426500 100644 --- a/src/app/atoms/button/Button.scss +++ b/src/app/atoms/button/Button.scss @@ -2,6 +2,7 @@ .btn-surface, .btn-primary, +.btn-positive, .btn-caution, .btn-danger { display: inline-flex; @@ -67,6 +68,13 @@ @include state.focus(var(--bs-primary-outline)); @include state.active(var(--bg-primary-active)); } +.btn-positive { + box-shadow: var(--bs-positive-border); + @include color(var(--tc-positive-high), var(--ic-positive-normal)); + @include state.hover(var(--bg-positive-hover)); + @include state.focus(var(--bs-positive-outline)); + @include state.active(var(--bg-positive-active)); +} .btn-caution { box-shadow: var(--bs-caution-border); @include color(var(--tc-caution-high), var(--ic-caution-normal)); diff --git a/src/app/atoms/button/IconButton.jsx b/src/app/atoms/button/IconButton.jsx index 34e2424..4ed2b93 100644 --- a/src/app/atoms/button/IconButton.jsx +++ b/src/app/atoms/button/IconButton.jsx @@ -7,45 +7,46 @@ import Tooltip from '../tooltip/Tooltip'; import { blurOnBubbling } from './script'; import Text from '../text/Text'; -// TODO: -// 1. [done] an icon only button have "src" -// 2. have multiple variant -// 3. [done] should have a smart accessibility "label" arial-label -// 4. [done] have size as RawIcon - const IconButton = React.forwardRef(({ variant, size, type, tooltip, tooltipPlacement, src, onClick, -}, ref) => ( - {tooltip}} - > +}, ref) => { + const btn = ( - -)); + ); + if (tooltip === null) return btn; + return ( + {tooltip}} + > + {btn} + + ); +}); IconButton.defaultProps = { variant: 'surface', size: 'normal', type: 'button', + tooltip: null, tooltipPlacement: 'top', onClick: null, }; IconButton.propTypes = { - variant: PropTypes.oneOf(['surface']), + variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']), size: PropTypes.oneOf(['normal', 'small', 'extra-small']), type: PropTypes.oneOf(['button', 'submit']), - tooltip: PropTypes.string.isRequired, + tooltip: PropTypes.string, tooltipPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), src: PropTypes.string.isRequired, onClick: PropTypes.func, diff --git a/src/app/atoms/button/IconButton.scss b/src/app/atoms/button/IconButton.scss index a0cabc6..9b83c08 100644 --- a/src/app/atoms/button/IconButton.scss +++ b/src/app/atoms/button/IconButton.scss @@ -1,9 +1,6 @@ @use 'state'; -.ic-btn-surface, -.ic-btn-primary, -.ic-btn-caution, -.ic-btn-danger { +.ic-btn { padding: var(--sp-extra-tight); border: none; border-radius: var(--bo-radius); @@ -31,4 +28,22 @@ @include state.hover(var(--bg-surface-hover)); @include focus(var(--bg-surface-hover)); @include state.active(var(--bg-surface-active)); +} +.ic-btn-positive { + @include color(var(--ic-positive-normal)); + @include state.hover(var(--bg-positive-hover)); + @include focus(var(--bg-positive-hover)); + @include state.active(var(--bg-positive-active)); +} +.ic-btn-caution { + @include color(var(--ic-caution-normal)); + @include state.hover(var(--bg-caution-hover)); + @include focus(var(--bg-caution-hover)); + @include state.active(var(--bg-caution-active)); +} +.ic-btn-danger { + @include color(var(--ic-danger-normal)); + @include state.hover(var(--bg-danger-hover)); + @include focus(var(--bg-danger-hover)); + @include state.active(var(--bg-danger-active)); } \ No newline at end of file diff --git a/src/app/atoms/context-menu/ContextMenu.jsx b/src/app/atoms/context-menu/ContextMenu.jsx index b525e22..023ee38 100644 --- a/src/app/atoms/context-menu/ContextMenu.jsx +++ b/src/app/atoms/context-menu/ContextMenu.jsx @@ -93,7 +93,7 @@ MenuItem.defaultProps = { }; MenuItem.propTypes = { - variant: PropTypes.oneOf(['surface', 'caution', 'danger']), + variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']), iconSrc: PropTypes.string, type: PropTypes.oneOf(['button', 'submit']), onClick: PropTypes.func.isRequired, diff --git a/src/app/atoms/context-menu/ContextMenu.scss b/src/app/atoms/context-menu/ContextMenu.scss index fd6ca07..4a8cc2a 100644 --- a/src/app/atoms/context-menu/ContextMenu.scss +++ b/src/app/atoms/context-menu/ContextMenu.scss @@ -30,6 +30,9 @@ .text { color: var(--tc-surface-low); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &:not(:first-child) { diff --git a/src/app/atoms/input/Input.jsx b/src/app/atoms/input/Input.jsx index c5401a3..7b5f096 100644 --- a/src/app/atoms/input/Input.jsx +++ b/src/app/atoms/input/Input.jsx @@ -8,6 +8,7 @@ function Input({ id, label, value, placeholder, required, type, onChange, forwardRef, resizable, minHeight, onResize, state, + onKeyDown, }) { return (
@@ -26,6 +27,7 @@ function Input({ autoComplete="off" onChange={onChange} onResize={onResize} + onKeyDown={onKeyDown} /> ) : ( )}
@@ -57,6 +60,7 @@ Input.defaultProps = { minHeight: 46, onResize: null, state: 'normal', + onKeyDown: null, }; Input.propTypes = { @@ -72,6 +76,7 @@ Input.propTypes = { minHeight: PropTypes.number, onResize: PropTypes.func, state: PropTypes.oneOf(['normal', 'success', 'error']), + onKeyDown: PropTypes.func, }; export default Input; diff --git a/src/app/molecules/image-upload/ImageUpload.jsx b/src/app/molecules/image-upload/ImageUpload.jsx new file mode 100644 index 0000000..da79489 --- /dev/null +++ b/src/app/molecules/image-upload/ImageUpload.jsx @@ -0,0 +1,88 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import './ImageUpload.scss'; + +import initMatrix from '../../../client/initMatrix'; + +import Text from '../../atoms/text/Text'; +import Avatar from '../../atoms/avatar/Avatar'; +import Spinner from '../../atoms/spinner/Spinner'; + +function ImageUpload({ + text, bgColor, imageSrc, onUpload, onRequestRemove, +}) { + const [uploadPromise, setUploadPromise] = useState(null); + const uploadImageRef = useRef(null); + + async function uploadImage(e) { + const file = e.target.files.item(0); + if (file === null) return; + try { + const uPromise = initMatrix.matrixClient.uploadContent(file, { onlyContentUri: false }); + setUploadPromise(uPromise); + + const res = await uPromise; + if (typeof res?.content_uri === 'string') onUpload(res.content_uri); + setUploadPromise(null); + } catch { + setUploadPromise(null); + } + uploadImageRef.current.value = null; + } + + function cancelUpload() { + initMatrix.matrixClient.cancelUpload(uploadPromise); + setUploadPromise(null); + uploadImageRef.current.value = null; + } + + return ( +
+ + { (typeof imageSrc === 'string' || uploadPromise !== null) && ( + + )} + +
+ ); +} + +ImageUpload.defaultProps = { + text: null, + bgColor: 'transparent', + imageSrc: null, +}; + +ImageUpload.propTypes = { + text: PropTypes.string, + bgColor: PropTypes.string, + imageSrc: PropTypes.string, + onUpload: PropTypes.func.isRequired, + onRequestRemove: PropTypes.func.isRequired, +}; + +export default ImageUpload; diff --git a/src/app/molecules/image-upload/ImageUpload.scss b/src/app/molecules/image-upload/ImageUpload.scss new file mode 100644 index 0000000..9e0f312 --- /dev/null +++ b/src/app/molecules/image-upload/ImageUpload.scss @@ -0,0 +1,50 @@ +.img-upload__wrapper { + display: flex; + flex-direction: column; + align-items: center; +} + +.img-upload { + display: flex; + cursor: pointer; + position: relative; + + &__process { + width: 100%; + height: 100%; + border-radius: var(--bo-radius); + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, .6); + + position: absolute; + left: 0; + right: 0; + z-index: 1; + & .text { + text-transform: uppercase; + font-weight: 600; + color: white; + } + &--stopped { + display: none; + } + & .donut-spinner { + border-color: rgb(255, 255, 255, .3); + border-left-color: white; + } + } + &:hover .img-upload__process--stopped { + display: flex; + } + + + &__btn-cancel { + margin-top: var(--sp-extra-tight); + cursor: pointer; + & .text { + color: var(--tc-danger-normal) + } + } +} diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index c1da0f0..440197b 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -128,10 +128,19 @@ MessageContent.propTypes = { function MessageEdit({ content, onSave, onCancel }) { const editInputRef = useRef(null); + + function handleKeyDown(e) { + if (e.keyCode === 13 && e.shiftKey === false) { + e.preventDefault(); + onSave(editInputRef.current.value); + } + } + return (
{ e.preventDefault(); onSave(editInputRef.current.value); }}> {content}; } -function ChannelIntro({ +function RoomIntro({ roomId, avatarSrc, name, heading, desc, time, }) { return ( -
+
-
- {heading} - {linkifyContent(desc)} - { time !== null && {time}} +
+ {heading} + {linkifyContent(desc)} + { time !== null && {time}}
); } -ChannelIntro.defaultProps = { +RoomIntro.defaultProps = { avatarSrc: false, time: null, }; -ChannelIntro.propTypes = { +RoomIntro.propTypes = { roomId: PropTypes.string.isRequired, avatarSrc: PropTypes.oneOfType([ PropTypes.string, @@ -44,4 +44,4 @@ ChannelIntro.propTypes = { time: PropTypes.string, }; -export default ChannelIntro; +export default RoomIntro; diff --git a/src/app/molecules/channel-intro/ChannelIntro.scss b/src/app/molecules/room-intro/RoomIntro.scss similarity index 93% rename from src/app/molecules/channel-intro/ChannelIntro.scss rename to src/app/molecules/room-intro/RoomIntro.scss index 35186af..8e923f3 100644 --- a/src/app/molecules/channel-intro/ChannelIntro.scss +++ b/src/app/molecules/room-intro/RoomIntro.scss @@ -1,4 +1,4 @@ -.channel-intro { +.room-intro { margin-top: calc(2 * var(--sp-extra-loose)); margin-bottom: var(--sp-extra-loose); padding-left: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight)); @@ -11,7 +11,7 @@ } } - .channel-intro__content { + .room-intro__content { margin-top: var(--sp-extra-loose); max-width: 640px; } diff --git a/src/app/molecules/channel-selector/ChannelSelector.jsx b/src/app/molecules/room-selector/RoomSelector.jsx similarity index 65% rename from src/app/molecules/channel-selector/ChannelSelector.jsx rename to src/app/molecules/room-selector/RoomSelector.jsx index 076b5fe..47201a6 100644 --- a/src/app/molecules/channel-selector/ChannelSelector.jsx +++ b/src/app/molecules/room-selector/RoomSelector.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import './ChannelSelector.scss'; +import './RoomSelector.scss'; import colorMXID from '../../../util/colorMXID'; @@ -9,41 +9,45 @@ import Avatar from '../../atoms/avatar/Avatar'; import NotificationBadge from '../../atoms/badge/NotificationBadge'; import { blurOnBubbling } from '../../atoms/button/script'; -function ChannelSelectorWrapper({ - isSelected, onClick, content, options, +function RoomSelectorWrapper({ + isSelected, isUnread, onClick, content, options, }) { + let myClass = isUnread ? ' room-selector--unread' : ''; + myClass += isSelected ? ' room-selector--selected' : ''; return ( -
+
-
{options}
+
{options}
); } -ChannelSelectorWrapper.defaultProps = { +RoomSelectorWrapper.defaultProps = { options: null, }; -ChannelSelectorWrapper.propTypes = { +RoomSelectorWrapper.propTypes = { isSelected: PropTypes.bool.isRequired, + isUnread: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, content: PropTypes.node.isRequired, options: PropTypes.node, }; -function ChannelSelector({ +function RoomSelector({ name, roomId, imageSrc, iconSrc, isSelected, isUnread, notificationCount, isAlert, options, onClick, }) { return ( - ); } -ChannelSelector.defaultProps = { +RoomSelector.defaultProps = { + isSelected: false, imageSrc: null, iconSrc: null, options: null, }; -ChannelSelector.propTypes = { +RoomSelector.propTypes = { name: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired, imageSrc: PropTypes.string, iconSrc: PropTypes.string, - isSelected: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, isUnread: PropTypes.bool.isRequired, - notificationCount: PropTypes.number.isRequired, + notificationCount: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, isAlert: PropTypes.bool.isRequired, options: PropTypes.node, onClick: PropTypes.func.isRequired, }; -export default ChannelSelector; +export default RoomSelector; diff --git a/src/app/molecules/channel-selector/ChannelSelector.scss b/src/app/molecules/room-selector/RoomSelector.scss similarity index 69% rename from src/app/molecules/channel-selector/ChannelSelector.scss rename to src/app/molecules/room-selector/RoomSelector.scss index 31385f3..e4dce0b 100644 --- a/src/app/molecules/channel-selector/ChannelSelector.scss +++ b/src/app/molecules/room-selector/RoomSelector.scss @@ -1,25 +1,32 @@ -.channel-selector-flex { +.room-selector-flex { display: flex; align-items: center; } -.channel-selector-flexItem { +.room-selector-flexItem { flex: 1; min-width: 0; min-height: 0; } -.channel-selector { - @extend .channel-selector-flex; +.room-selector { + @extend .room-selector-flex; border: 1px solid transparent; border-radius: var(--bo-radius); cursor: pointer; + + &--unread { + .room-selector__content > .text { + font-weight: 500; + color: var(--tc-surface-high); + } + } &--selected { background-color: var(--bg-surface); border-color: var(--bg-surface-border); - & .channel-selector__options { + & .room-selector__options { display: flex; } } @@ -27,14 +34,16 @@ @media (hover: hover) { &:hover { background-color: var(--bg-surface-hover); - & .channel-selector__options { + & .room-selector__options { display: flex; } } } - &:focus { - outline: none; + &:focus-within { background-color: var(--bg-surface-hover); + & button { + outline: none; + } } &:active { background-color: var(--bg-surface-active); @@ -46,9 +55,9 @@ } } -.channel-selector__content { - @extend .channel-selector-flexItem; - @extend .channel-selector-flex; +.room-selector__content { + @extend .room-selector-flexItem; + @extend .room-selector-flex; padding: 0 var(--sp-extra-tight); min-height: 40px; cursor: inherit; @@ -58,7 +67,7 @@ } & > .text { - @extend .channel-selector-flexItem; + @extend .room-selector-flexItem; margin: 0 var(--sp-extra-tight); color: var(--tc-surface-normal); @@ -67,8 +76,8 @@ text-overflow: ellipsis; } } -.channel-selector__options { - @extend .channel-selector-flex; +.room-selector__options { + @extend .room-selector-flex; display: none; margin-right: var(--sp-ultra-tight); @@ -81,7 +90,7 @@ margin: 0 !important; } - & .ic-btn-surface { + & .ic-btn { padding: 6px; border-radius: calc(var(--bo-radius) / 2); } diff --git a/src/app/molecules/channel-tile/ChannelTile.jsx b/src/app/molecules/room-tile/RoomTile.jsx similarity index 78% rename from src/app/molecules/channel-tile/ChannelTile.jsx rename to src/app/molecules/room-tile/RoomTile.jsx index dfb384d..a9a680d 100644 --- a/src/app/molecules/channel-tile/ChannelTile.jsx +++ b/src/app/molecules/room-tile/RoomTile.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import './ChannelTile.scss'; +import './RoomTile.scss'; import Linkify from 'linkifyjs/react'; import colorMXID from '../../../util/colorMXID'; @@ -12,20 +12,20 @@ function linkifyContent(content) { return {content}; } -function ChannelTile({ +function RoomTile({ avatarSrc, name, id, inviterName, memberCount, desc, options, }) { return ( -
-
+
+
-
+
{name} { @@ -36,12 +36,12 @@ function ChannelTile({ { desc !== null && (typeof desc === 'string') - ? {linkifyContent(desc)} + ? {linkifyContent(desc)} : desc }
{ options !== null && ( -
+
{options}
)} @@ -49,14 +49,14 @@ function ChannelTile({ ); } -ChannelTile.defaultProps = { +RoomTile.defaultProps = { avatarSrc: null, inviterName: null, options: null, desc: null, memberCount: null, }; -ChannelTile.propTypes = { +RoomTile.propTypes = { avatarSrc: PropTypes.string, name: PropTypes.string.isRequired, id: PropTypes.string.isRequired, @@ -69,4 +69,4 @@ ChannelTile.propTypes = { options: PropTypes.node, }; -export default ChannelTile; +export default RoomTile; diff --git a/src/app/molecules/channel-tile/ChannelTile.scss b/src/app/molecules/room-tile/RoomTile.scss similarity index 94% rename from src/app/molecules/channel-tile/ChannelTile.scss rename to src/app/molecules/room-tile/RoomTile.scss index ce20195..bbed710 100644 --- a/src/app/molecules/channel-tile/ChannelTile.scss +++ b/src/app/molecules/room-tile/RoomTile.scss @@ -1,4 +1,4 @@ -.channel-tile { +.room-tile { display: flex; &__content { diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx b/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx index 8d57e9b..882c00c 100644 --- a/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx +++ b/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx @@ -2,29 +2,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import './SidebarAvatar.scss'; -import Tippy from '@tippyjs/react'; import Avatar from '../../atoms/avatar/Avatar'; import Text from '../../atoms/text/Text'; +import Tooltip from '../../atoms/tooltip/Tooltip'; import NotificationBadge from '../../atoms/badge/NotificationBadge'; import { blurOnBubbling } from '../../atoms/button/script'; const SidebarAvatar = React.forwardRef(({ tooltip, text, bgColor, imageSrc, - iconSrc, active, onClick, notifyCount, + iconSrc, active, onClick, isUnread, notificationCount, isAlert, }, ref) => { let activeClass = ''; if (active) activeClass = ' sidebar-avatar--active'; return ( - {tooltip}} - className="sidebar-avatar-tippy" - touch="hold" - arrow={false} placement="right" - maxWidth={200} - delay={[0, 0]} - duration={[100, 0]} - offset={[0, 0]} > - + ); }); SidebarAvatar.defaultProps = { @@ -52,7 +50,9 @@ SidebarAvatar.defaultProps = { imageSrc: null, active: false, onClick: null, - notifyCount: null, + isUnread: false, + notificationCount: 0, + isAlert: false, }; SidebarAvatar.propTypes = { @@ -63,10 +63,12 @@ SidebarAvatar.propTypes = { iconSrc: PropTypes.string, active: PropTypes.bool, onClick: PropTypes.func, - notifyCount: PropTypes.oneOfType([ + isUnread: PropTypes.bool, + notificationCount: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, ]), + isAlert: PropTypes.bool, }; export default SidebarAvatar; diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss index 6191735..3f445df 100644 --- a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss +++ b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss @@ -1,28 +1,18 @@ - -.sidebar-avatar-tippy { - padding: var(--sp-extra-tight) var(--sp-normal); - background-color: var(--bg-tooltip); - border-radius: var(--bo-radius); - box-shadow: var(--bs-popup); - - .text { - color: var(--tc-tooltip); - } -} - .sidebar-avatar { position: relative; display: flex; justify-content: center; align-items: center; - width: 100%; cursor: pointer; & .notification-badge { position: absolute; - right: var(--sp-extra-tight); - top: calc(-1 * var(--sp-ultra-tight)); + right: 0; + top: 0; box-shadow: 0 0 0 2px var(--bg-surface-low); + transform: translate(20%, -20%); + + margin: 0 !important; } &:focus { outline: none; @@ -37,7 +27,7 @@ content: ""; display: block; position: absolute; - left: 0; + left: -11px; top: 50%; transform: translateY(-50%); @@ -48,7 +38,8 @@ transition: height 200ms linear; [dir=rtl] & { - right: 0; + left: unset; + right: -11px; border-radius: 4px 0 0 4px; } } diff --git a/src/app/organisms/create-channel/CreateChannel.jsx b/src/app/organisms/create-room/CreateRoom.jsx similarity index 81% rename from src/app/organisms/create-channel/CreateChannel.jsx rename to src/app/organisms/create-room/CreateRoom.jsx index c44b536..d94c4b1 100644 --- a/src/app/organisms/create-channel/CreateChannel.jsx +++ b/src/app/organisms/create-room/CreateRoom.jsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from 'react'; import PropTypes from 'prop-types'; -import './CreateChannel.scss'; +import './CreateRoom.scss'; import initMatrix from '../../../client/initMatrix'; import { isRoomAliasAvailable } from '../../../util/matrixUtil'; @@ -18,7 +18,7 @@ import SettingTile from '../../molecules/setting-tile/SettingTile'; import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; -function CreateChannel({ isOpen, onRequestClose }) { +function CreateRoom({ isOpen, onRequestClose }) { const [isPublic, togglePublic] = useState(false); const [isEncrypted, toggleEncrypted] = useState(true); const [isValidAddress, updateIsValidAddress] = useState(null); @@ -69,10 +69,10 @@ function CreateChannel({ isOpen, onRequestClose }) { onRequestClose(); } catch (e) { if (e.message === 'M_UNKNOWN: Invalid characters in room alias') { - updateCreatingError('ERROR: Invalid characters in channel address'); + updateCreatingError('ERROR: Invalid characters in room address'); updateIsValidAddress(false); } else if (e.message === 'M_ROOM_IN_USE: Room alias already taken') { - updateCreatingError('ERROR: Channel address is already in use'); + updateCreatingError('ERROR: Room address is already in use'); updateIsValidAddress(false); } else updateCreatingError(e.message); } @@ -110,26 +110,26 @@ function CreateChannel({ isOpen, onRequestClose }) { return ( } onRequestClose={onRequestClose} > -
- { e.preventDefault(); createRoom(); }}> +
+ { e.preventDefault(); createRoom(); }}> } - content={Public channel can be joined by anyone.} + content={Public room can be joined by anyone.} /> {isPublic && (
- Channel address -
+ Room address +
# {hsString}
- {isValidAddress === false && {`#${addressValue}${hsString} is already in use`}} + {isValidAddress === false && {`#${addressValue}${hsString} is already in use`}}
)} {!isPublic && ( @@ -140,26 +140,26 @@ function CreateChannel({ isOpen, onRequestClose }) { /> )} -
- +
+
{isCreatingRoom && ( -
+
- Creating channel... + Creating room...
)} - {typeof creatingError === 'string' && {creatingError}} + {typeof creatingError === 'string' && {creatingError}}
); } -CreateChannel.propTypes = { +CreateRoom.propTypes = { isOpen: PropTypes.bool.isRequired, onRequestClose: PropTypes.func.isRequired, }; -export default CreateChannel; +export default CreateRoom; diff --git a/src/app/organisms/create-channel/CreateChannel.scss b/src/app/organisms/create-room/CreateRoom.scss similarity index 99% rename from src/app/organisms/create-channel/CreateChannel.scss rename to src/app/organisms/create-room/CreateRoom.scss index 6d59f65..c587fa2 100644 --- a/src/app/organisms/create-channel/CreateChannel.scss +++ b/src/app/organisms/create-room/CreateRoom.scss @@ -1,4 +1,4 @@ -.create-channel { +.create-room { margin: 0 var(--sp-normal); margin-right: var(--sp-extra-tight); diff --git a/src/app/organisms/invite-list/InviteList.jsx b/src/app/organisms/invite-list/InviteList.jsx index 297478e..2fee050 100644 --- a/src/app/organisms/invite-list/InviteList.jsx +++ b/src/app/organisms/invite-list/InviteList.jsx @@ -11,7 +11,7 @@ import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; import Spinner from '../../atoms/spinner/Spinner'; import PopupWindow from '../../molecules/popup-window/PopupWindow'; -import ChannelTile from '../../molecules/channel-tile/ChannelTile'; +import RoomTile from '../../molecules/room-tile/RoomTile'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; @@ -47,13 +47,13 @@ function InviteList({ isOpen, onRequestClose }) { }; }, [procInvite]); - function renderChannelTile(roomId) { + function renderRoomTile(roomId) { const myRoom = initMatrix.matrixClient.getRoom(roomId); const roomName = myRoom.name; let roomAlias = myRoom.getCanonicalAlias(); if (roomAlias === null) roomAlias = myRoom.roomId; return ( - Spaces
)} - { Array.from(initMatrix.roomList.inviteSpaces).map(renderChannelTile) } + { Array.from(initMatrix.roomList.inviteSpaces).map(renderRoomTile) } { initMatrix.roomList.inviteRooms.size !== 0 && (
- Channels + Rooms
)} - { Array.from(initMatrix.roomList.inviteRooms).map(renderChannelTile) } + { Array.from(initMatrix.roomList.inviteRooms).map(renderRoomTile) }
); diff --git a/src/app/organisms/invite-list/InviteList.scss b/src/app/organisms/invite-list/InviteList.scss index bdb78c4..70e82c7 100644 --- a/src/app/organisms/invite-list/InviteList.scss +++ b/src/app/organisms/invite-list/InviteList.scss @@ -14,7 +14,7 @@ } } - & .channel-tile { + & .room-tile { margin-top: var(--sp-normal); &__options { align-self: flex-end; diff --git a/src/app/organisms/invite-user/InviteUser.jsx b/src/app/organisms/invite-user/InviteUser.jsx index cac9060..a6ff242 100644 --- a/src/app/organisms/invite-user/InviteUser.jsx +++ b/src/app/organisms/invite-user/InviteUser.jsx @@ -13,7 +13,7 @@ import IconButton from '../../atoms/button/IconButton'; import Spinner from '../../atoms/spinner/Spinner'; import Input from '../../atoms/input/Input'; import PopupWindow from '../../molecules/popup-window/PopupWindow'; -import ChannelTile from '../../molecules/channel-tile/ChannelTile'; +import RoomTile from '../../molecules/room-tile/RoomTile'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import UserIC from '../../../../public/res/ic/outlined/user.svg'; @@ -188,7 +188,7 @@ function InviteUser({ const userId = user.user_id; const name = typeof user.display_name === 'string' ? user.display_name : userId; return ( - { roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); return () => { roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); }; }, []); @@ -62,6 +61,7 @@ function Directs() { key={id} roomId={id} drawerPostie={drawerPostie} + onClick={() => selectRoom(id)} /> )); } diff --git a/src/app/organisms/navigation/Drawer.jsx b/src/app/organisms/navigation/Drawer.jsx index 92c192a..5e9d2fb 100644 --- a/src/app/organisms/navigation/Drawer.jsx +++ b/src/app/organisms/navigation/Drawer.jsx @@ -1,57 +1,83 @@ import React, { useState, useEffect } from 'react'; import './Drawer.scss'; +import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; +import { selectTab, selectSpace } from '../../../client/action/navigation'; +import Text from '../../atoms/text/Text'; import ScrollView from '../../atoms/scroll/ScrollView'; import DrawerHeader from './DrawerHeader'; +import DrawerBreadcrumb from './DrawerBreadcrumb'; import Home from './Home'; import Directs from './Directs'; -function DrawerBradcrumb() { - return ( -
- -
- {/* TODO: bradcrumb space paths when spaces become a thing */} -
-
-
- ); -} - function Drawer() { - const [activeTab, setActiveTab] = useState('home'); + const [systemState, setSystemState] = useState(null); + const [selectedTab, setSelectedTab] = useState(navigation.selectedTab); + const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId); - function onTabChanged(tabId) { - setActiveTab(tabId); + function onTabSelected(tabId) { + setSelectedTab(tabId); + } + function onSpaceSelected(roomId) { + setSpaceId(roomId); + } + function onRoomLeaved(roomId) { + const lRoomIndex = navigation.selectedSpacePath.indexOf(roomId); + if (lRoomIndex === -1) return; + if (lRoomIndex === 0) selectTab(cons.tabs.HOME); + else selectSpace(navigation.selectedSpacePath[lRoomIndex - 1]); + } + + function handleSystemState(state) { + if (state === 'ERROR' || state === 'RECONNECTING' || state === 'STOPPED') { + setSystemState({ status: 'Connection lost!' }); + } + if (systemState !== null) setSystemState(null); } useEffect(() => { - navigation.on(cons.events.navigation.TAB_CHANGED, onTabChanged); + navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected); + navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected); + initMatrix.roomList.on(cons.events.roomList.ROOM_LEAVED, onRoomLeaved); return () => { - navigation.removeListener(cons.events.navigation.TAB_CHANGED, onTabChanged); + navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected); + navigation.removeListener(cons.events.navigation.SPACE_SELECTED, onSpaceSelected); + initMatrix.roomList.removeListener(cons.events.roomList.ROOM_LEAVED, onRoomLeaved); }; }, []); + useEffect(() => { + initMatrix.matrixClient.on('sync', handleSystemState); + return () => { + initMatrix.matrixClient.removeListener('sync', handleSystemState); + }; + }, [systemState]); + return (
- +
- -
+ {selectedTab !== cons.tabs.DIRECTS && } +
-
+
{ - activeTab === 'home' - ? + selectedTab !== cons.tabs.DIRECTS + ? : }
+ { systemState !== null && ( +
+ {systemState.status} +
+ )}
); } diff --git a/src/app/organisms/navigation/Drawer.scss b/src/app/organisms/navigation/Drawer.scss index 4b12bce..c16b974 100644 --- a/src/app/organisms/navigation/Drawer.scss +++ b/src/app/organisms/navigation/Drawer.scss @@ -18,24 +18,49 @@ border-left: 1px solid var(--bg-surface-border); } + & .header__title-wrapper .text { + font-weight: 500; + } + &__content-wrapper { @extend .drawer-flexItem; @extend .drawer-flexBox; } -} -.breadcrumb__wrapper { - display: none; - height: var(--header-height); + &__state { + padding: var(--sp-extra-tight); + border-top: 1px solid var(--bg-surface-border); + display: flex; + justify-content: center; + + & .text { + color: var(--tc-danger-high); + } + } } -.channels__wrapper { +.rooms__wrapper { @extend .drawer-flexItem; + position: relative; } -.channels-container { +.rooms-container { padding-bottom: var(--sp-extra-loose); - & > .channel-selector { + &::before { + position: absolute; + top: 0; + + content: ''; + display: inline-block; + width: 100%; + height: 8px; + background-image: linear-gradient( + to bottom, + var(--bg-surface-low), + var(--bg-surface-low-transparent)); + } + + & > .room-selector { width: calc(100% - var(--sp-extra-tight)); margin-left: auto; @@ -46,7 +71,7 @@ } - & > .channel-selector:first-child { + & > .room-selector:first-child { margin-top: var(--sp-extra-tight); } diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.jsx b/src/app/organisms/navigation/DrawerBreadcrumb.jsx new file mode 100644 index 0000000..7eaae4e --- /dev/null +++ b/src/app/organisms/navigation/DrawerBreadcrumb.jsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import './DrawerBreadcrumb.scss'; + +import initMatrix from '../../../client/initMatrix'; +import cons from '../../../client/state/cons'; +import { selectSpace } from '../../../client/action/navigation'; +import navigation from '../../../client/state/navigation'; +import { abbreviateNumber } from '../../../util/common'; + +import Text from '../../atoms/text/Text'; +import RawIcon from '../../atoms/system-icons/RawIcon'; +import Button from '../../atoms/button/Button'; +import ScrollView from '../../atoms/scroll/ScrollView'; +import NotificationBadge from '../../atoms/badge/NotificationBadge'; + +import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; + +function DrawerBreadcrumb({ spaceId }) { + const [, forceUpdate] = useState({}); + const scrollRef = useRef(null); + const { roomList, notifications } = initMatrix; + const mx = initMatrix.matrixClient; + const spacePath = navigation.selectedSpacePath; + + function onNotiChanged(roomId, total, prevTotal) { + if (total === prevTotal) return; + if (navigation.selectedSpacePath.includes(roomId)) { + forceUpdate({}); + } + if (navigation.selectedSpacePath[0] === cons.tabs.HOME) { + if (!roomList.isOrphan(roomId)) return; + if (roomList.directs.has(roomId)) return; + forceUpdate({}); + } + } + + useEffect(() => { + requestAnimationFrame(() => { + if (scrollRef?.current === null) return; + scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; + }); + notifications.on(cons.events.notifications.NOTI_CHANGED, onNotiChanged); + return () => { + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotiChanged); + }; + }, [spaceId]); + + if (spacePath.length === 1) return null; + + function getHomeNotiExcept(childId) { + const orphans = roomList.getOrphans(); + const childIndex = orphans.indexOf(childId); + if (childId !== -1) orphans.splice(childIndex, 1); + + let noti = null; + + orphans.forEach((roomId) => { + if (!notifications.hasNoti(roomId)) return; + if (noti === null) noti = { total: 0, highlight: 0 }; + const childNoti = notifications.getNoti(roomId); + noti.total += childNoti.total; + noti.highlight += childNoti.highlight; + }); + + return noti; + } + + function getNotiExcept(roomId, childId) { + if (!notifications.hasNoti(roomId)) return null; + + const noti = notifications.getNoti(roomId); + if (!notifications.hasNoti(childId)) return noti; + if (noti.from === null) return noti; + if (noti.from.has(childId) && noti.from.size === 1) return null; + + const childNoti = notifications.getNoti(childId); + + return { + total: noti.total - childNoti.total, + highlight: noti.highlight - childNoti.highlight, + }; + } + + return ( +
+ +
+ { + spacePath.map((id, index) => { + const noti = (id !== cons.tabs.HOME && index < spacePath.length) + ? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1]) + : getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]); + + return ( + + { index !== 0 && } + + + ); + }) + } +
+
+ +
+ ); +} + +DrawerBreadcrumb.defaultProps = { + spaceId: null, +}; + +DrawerBreadcrumb.propTypes = { + spaceId: PropTypes.string, +}; + +export default DrawerBreadcrumb; diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.scss b/src/app/organisms/navigation/DrawerBreadcrumb.scss new file mode 100644 index 0000000..60cd47f --- /dev/null +++ b/src/app/organisms/navigation/DrawerBreadcrumb.scss @@ -0,0 +1,68 @@ +.breadcrumb__wrapper { + height: var(--header-height); + position: relative; +} + +.breadcrumb { + display: flex; + align-items: center; + height: 100%; + margin: 0 var(--sp-extra-tight); + + &::before, + &::after { + flex-shrink: 0; + position: absolute; + right: 0; + z-index: 99; + + content: ''; + display: inline-block; + min-width: 8px; + width: 8px; + height: 100%; + background-image: linear-gradient( + to right, + var(--bg-surface-low-transparent), + var(--bg-surface-low) + ); + } + &::before { + left: 0; + right: unset; + background-image: linear-gradient( + to left, + var(--bg-surface-low-transparent), + var(--bg-surface-low) + ); + } + + & > * { + flex-shrink: 0; + } + + & > .btn-surface { + min-width: 0; + padding: var(--sp-extra-tight) 10px; + white-space: nowrap; + box-shadow: none; + & p { + max-width: 86px; + overflow: hidden; + text-overflow: ellipsis; + } + + & .notification-badge { + margin-left: var(--sp-extra-tight); + [dir=rtl] & { + margin-left: 0; + margin-right: var(--sp-extra-tight); + } + } + } + + &__btn--selected { + box-shadow: var(--bs-surface-border) !important; + background-color: var(--bg-surface); + } +} \ No newline at end of file diff --git a/src/app/organisms/navigation/DrawerHeader.jsx b/src/app/organisms/navigation/DrawerHeader.jsx index 2a45301..5d70525 100644 --- a/src/app/organisms/navigation/DrawerHeader.jsx +++ b/src/app/organisms/navigation/DrawerHeader.jsx @@ -1,9 +1,12 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; +import initMatrix from '../../../client/initMatrix'; +import cons from '../../../client/state/cons'; import { - openPublicChannels, openCreateChannel, openInviteUser, + openPublicRooms, openCreateRoom, openInviteUser, } from '../../../client/action/navigation'; +import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room'; import Text from '../../atoms/text/Text'; import Header, { TitleWrapper } from '../../atoms/header/Header'; @@ -13,43 +16,71 @@ import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/Cont import PlusIC from '../../../../public/res/ic/outlined/plus.svg'; import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg'; import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg'; +import PinIC from '../../../../public/res/ic/outlined/pin.svg'; +import PinFilledIC from '../../../../public/res/ic/filled/pin.svg'; + +function DrawerHeader({ selectedTab, spaceId }) { + const [, forceUpdate] = useState({}); + const mx = initMatrix.matrixClient; + const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages'; + + const room = mx.getRoom(spaceId); + const spaceName = selectedTab === cons.tabs.DIRECTS ? null : (room?.name || null); -function DrawerHeader({ activeTab }) { return (
- {(activeTab === 'home' ? 'Home' : 'Direct messages')} + {spaceName || tabName} - {(activeTab === 'dms') - ? openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" /> - : ( + {spaceName && ( + { + if (initMatrix.roomList.spaceShortcut.has(spaceId)) deleteSpaceShortcut(spaceId); + else createSpaceShortcut(spaceId); + forceUpdate({}); + }} + /> + )} + { selectedTab === cons.tabs.DIRECTS && openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" /> } + { selectedTab !== cons.tabs.DIRECTS && !spaceName && ( + <> ( <> - Add channel + Add room { hideMenu(); openCreateChannel(); }} + onClick={() => { hideMenu(); openCreateRoom(); }} > - Create new channel + Create new room { hideMenu(); openPublicChannels(); }} + onClick={() => { hideMenu(); openPublicRooms(); }} > - Add Public channel + Add public room )} - render={(toggleMenu) => ()} + render={(toggleMenu) => ()} /> - )} + + )} {/* ''} tooltip="Menu" src={VerticalMenuIC} size="normal" /> */}
); } + +DrawerHeader.defaultProps = { + spaceId: null, +}; DrawerHeader.propTypes = { - activeTab: PropTypes.string.isRequired, + selectedTab: PropTypes.string.isRequired, + spaceId: PropTypes.string, }; export default DrawerHeader; diff --git a/src/app/organisms/navigation/Home.jsx b/src/app/organisms/navigation/Home.jsx index 80cd3c0..2c505f7 100644 --- a/src/app/organisms/navigation/Home.jsx +++ b/src/app/organisms/navigation/Home.jsx @@ -1,9 +1,10 @@ import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; -import { selectRoom } from '../../../client/action/navigation'; +import { selectSpace, selectRoom } from '../../../client/action/navigation'; import Postie from '../../../util/Postie'; import Text from '../../atoms/text/Text'; @@ -12,33 +13,46 @@ import Selector from './Selector'; import { AtoZ } from './common'; const drawerPostie = new Postie(); -function Home() { - const { roomList } = initMatrix; - const spaceIds = [...roomList.spaces].sort(AtoZ); - const roomIds = [...roomList.rooms].sort(AtoZ); - +function Home({ spaceId }) { const [, forceUpdate] = useState({}); + const { roomList, notifications } = initMatrix; + let spaceIds = []; + let roomIds = []; + let directIds = []; - function selectorChanged(activeRoomID, prevActiveRoomId) { + const spaceChildIds = roomList.getSpaceChildren(spaceId); + if (spaceChildIds) { + spaceIds = spaceChildIds.filter((roomId) => roomList.spaces.has(roomId)).sort(AtoZ); + roomIds = spaceChildIds.filter((roomId) => roomList.rooms.has(roomId)).sort(AtoZ); + directIds = spaceChildIds.filter((roomId) => roomList.directs.has(roomId)).sort(AtoZ); + } else { + spaceIds = [...roomList.spaces] + .filter((roomId) => !roomList.roomIdToParents.has(roomId)).sort(AtoZ); + roomIds = [...roomList.rooms] + .filter((roomId) => !roomList.roomIdToParents.has(roomId)).sort(AtoZ); + } + + function selectorChanged(selectedRoomId, prevSelectedRoomId) { if (!drawerPostie.hasTopic('selector-change')) return; const addresses = []; - if (drawerPostie.hasSubscriber('selector-change', activeRoomID)) addresses.push(activeRoomID); - if (drawerPostie.hasSubscriber('selector-change', prevActiveRoomId)) addresses.push(prevActiveRoomId); + if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId); + if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId); if (addresses.length === 0) return; - drawerPostie.post('selector-change', addresses, activeRoomID); + drawerPostie.post('selector-change', addresses, selectedRoomId); } - function unreadChanged(roomId) { - if (!drawerPostie.hasTopic('unread-change')) return; - if (!drawerPostie.hasSubscriber('unread-change', roomId)) return; - drawerPostie.post('unread-change', roomId); + function notiChanged(roomId, total, prevTotal) { + if (total === prevTotal) return; + if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) { + drawerPostie.post('unread-change', roomId); + } } function roomListUpdated() { const { spaces, rooms, directs } = initMatrix.roomList; if (!( - spaces.has(navigation.getActiveRoomId()) - || rooms.has(navigation.getActiveRoomId()) - || directs.has(navigation.getActiveRoomId())) + spaces.has(navigation.selectedRoomId) + || rooms.has(navigation.selectedRoomId) + || directs.has(navigation.selectedRoomId)) ) { selectRoom(null); } @@ -48,13 +62,11 @@ function Home() { useEffect(() => { roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); return () => { roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); }; }, []); @@ -67,20 +79,38 @@ function Home() { roomId={id} isDM={false} drawerPostie={drawerPostie} + onClick={() => selectSpace(id)} /> ))} - { roomIds.length !== 0 && Channels } + { roomIds.length !== 0 && Rooms } { roomIds.map((id) => ( selectRoom(id)} /> )) } + + { directIds.length !== 0 && People } + { directIds.map((id) => ( + selectRoom(id)} + /> + ))} ); } +Home.defaultProps = { + spaceId: null, +}; +Home.propTypes = { + spaceId: PropTypes.string, +}; export default Home; diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx index c90fc85..a60422d 100644 --- a/src/app/organisms/navigation/Selector.jsx +++ b/src/app/organisms/navigation/Selector.jsx @@ -3,27 +3,36 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import initMatrix from '../../../client/initMatrix'; -import { doesRoomHaveUnread } from '../../../util/matrixUtil'; -import { selectRoom } from '../../../client/action/navigation'; import navigation from '../../../client/state/navigation'; +import { openRoomOptions } from '../../../client/action/navigation'; +import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room'; +import { getEventCords, abbreviateNumber } from '../../../util/common'; -import ChannelSelector from '../../molecules/channel-selector/ChannelSelector'; +import IconButton from '../../atoms/button/IconButton'; +import RoomSelector from '../../molecules/room-selector/RoomSelector'; import HashIC from '../../../../public/res/ic/outlined/hash.svg'; import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg'; import SpaceIC from '../../../../public/res/ic/outlined/space.svg'; import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg'; +import PinIC from '../../../../public/res/ic/outlined/pin.svg'; +import PinFilledIC from '../../../../public/res/ic/filled/pin.svg'; +import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; -function Selector({ roomId, isDM, drawerPostie }) { +function Selector({ + roomId, isDM, drawerPostie, onClick, +}) { const mx = initMatrix.matrixClient; + const noti = initMatrix.notifications; const room = mx.getRoom(roomId); - const imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; + let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; + if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; - const [isSelected, setIsSelected] = useState(navigation.getActiveRoomId() === roomId); + const [isSelected, setIsSelected] = useState(navigation.selectedRoomId === roomId); const [, forceUpdate] = useState({}); - function selectorChanged(activeRoomId) { - setIsSelected(activeRoomId === roomId); + function selectorChanged(selectedRoomId) { + setIsSelected(selectedRoomId === roomId); } function changeNotificationBadge() { forceUpdate({}); @@ -38,27 +47,57 @@ function Selector({ roomId, isDM, drawerPostie }) { }; }, []); + if (room.isSpaceRoom()) { + return ( + { + if (initMatrix.roomList.spaceShortcut.has(roomId)) deleteSpaceShortcut(roomId); + else createSpaceShortcut(roomId); + forceUpdate({}); + }} + /> + )} + /> + ); + } + return ( - { - if (room.isSpaceRoom()) { - return (room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC); - } - return (room.getJoinRule() === 'invite' ? HashLockIC : HashIC); - })() - } + // eslint-disable-next-line no-nested-ternary + iconSrc={isDM ? null : room.getJoinRule() === 'invite' ? HashLockIC : HashIC} isSelected={isSelected} - isUnread={doesRoomHaveUnread(room)} - notificationCount={room.getUnreadNotificationCount('total') || 0} - isAlert={room.getUnreadNotificationCount('highlight') !== 0} - onClick={() => selectRoom(roomId)} + isUnread={noti.hasNoti(roomId)} + notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))} + isAlert={noti.getHighlightNoti(roomId) !== 0} + onClick={onClick} + options={( + openRoomOptions(getEventCords(e), roomId)} + /> + )} /> ); } @@ -71,6 +110,7 @@ Selector.propTypes = { roomId: PropTypes.string.isRequired, isDM: PropTypes.bool, drawerPostie: PropTypes.shape({}).isRequired, + onClick: PropTypes.func.isRequired, }; export default Selector; diff --git a/src/app/organisms/navigation/SideBar.jsx b/src/app/organisms/navigation/SideBar.jsx index adb8f51..cd6de37 100644 --- a/src/app/organisms/navigation/SideBar.jsx +++ b/src/app/organisms/navigation/SideBar.jsx @@ -6,9 +6,10 @@ import cons from '../../../client/state/cons'; import colorMXID from '../../../util/colorMXID'; import logout from '../../../client/action/logout'; import { - changeTab, openInviteList, openPublicChannels, openSettings, + selectTab, openInviteList, openPublicRooms, openSettings, } from '../../../client/action/navigation'; import navigation from '../../../client/state/navigation'; +import { abbreviateNumber } from '../../../util/common'; import ScrollView from '../../atoms/scroll/ScrollView'; import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar'; @@ -55,48 +56,127 @@ function ProfileAvatarMenu() { } function SideBar() { - const totalInviteCount = () => initMatrix.roomList.inviteRooms.size - + initMatrix.roomList.inviteSpaces.size - + initMatrix.roomList.inviteDirects.size; + const { roomList, notifications } = initMatrix; + const mx = initMatrix.matrixClient; + const totalInviteCount = () => roomList.inviteRooms.size + + roomList.inviteSpaces.size + + roomList.inviteDirects.size; const [totalInvites, updateTotalInvites] = useState(totalInviteCount()); - const [activeTab, setActiveTab] = useState('home'); + const [selectedTab, setSelectedTab] = useState(navigation.selectedTab); + const [, forceUpdate] = useState({}); - function onTabChanged(tabId) { - setActiveTab(tabId); + function onTabSelected(tabId) { + setSelectedTab(tabId); } function onInviteListChange() { updateTotalInvites(totalInviteCount()); } + function onSpaceShortcutUpdated() { + forceUpdate({}); + } + function onNotificationChanged(roomId, total, prevTotal) { + if (total === prevTotal) return; + forceUpdate({}); + } useEffect(() => { - navigation.on(cons.events.navigation.TAB_CHANGED, onTabChanged); - initMatrix.roomList.on( - cons.events.roomList.INVITELIST_UPDATED, - onInviteListChange, - ); + navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected); + roomList.on(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated); + roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange); + notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged); return () => { - navigation.removeListener(cons.events.navigation.TAB_CHANGED, onTabChanged); - initMatrix.roomList.removeListener( - cons.events.roomList.INVITELIST_UPDATED, - onInviteListChange, - ); + navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected); + roomList.removeListener(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated); + roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange); + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged); }; }, []); + function getHomeNoti() { + const orphans = roomList.getOrphans(); + let noti = null; + + orphans.forEach((roomId) => { + if (!notifications.hasNoti(roomId)) return; + if (noti === null) noti = { total: 0, highlight: 0 }; + const childNoti = notifications.getNoti(roomId); + noti.total += childNoti.total; + noti.highlight += childNoti.highlight; + }); + + return noti; + } + function getDMsNoti() { + if (roomList.directs.size === 0) return null; + let noti = null; + + [...roomList.directs].forEach((roomId) => { + if (!notifications.hasNoti(roomId)) return; + if (noti === null) noti = { total: 0, highlight: 0 }; + const childNoti = notifications.getNoti(roomId); + noti.total += childNoti.total; + noti.highlight += childNoti.highlight; + }); + + return noti; + } + + // TODO: bellow operations are heavy. + // refactor this component into more smaller components. + const dmsNoti = getDMsNoti(); + const homeNoti = getHomeNoti(); + return (
- changeTab('home')} tooltip="Home" iconSrc={HomeIC} /> - changeTab('dms')} tooltip="People" iconSrc={UserIC} /> - openPublicChannels()} tooltip="Public channels" iconSrc={HashSearchIC} /> + selectTab(cons.tabs.HOME)} + tooltip="Home" + iconSrc={HomeIC} + isUnread={homeNoti !== null} + notificationCount={homeNoti !== null ? abbreviateNumber(homeNoti.total) : 0} + isAlert={homeNoti?.highlight > 0} + /> + selectTab(cons.tabs.DIRECTS)} + tooltip="People" + iconSrc={UserIC} + isUnread={dmsNoti !== null} + notificationCount={dmsNoti !== null ? abbreviateNumber(dmsNoti.total) : 0} + isAlert={dmsNoti?.highlight > 0} + /> + openPublicRooms()} tooltip="Public rooms" iconSrc={HashSearchIC} />
-
+
+ { + [...roomList.spaceShortcut].map((shortcut) => { + const sRoomId = shortcut; + const room = mx.getRoom(sRoomId); + return ( + selectTab(shortcut)} + /> + ); + }) + } +
@@ -105,7 +185,9 @@ function SideBar() {
{ totalInvites !== 0 && ( openInviteList()} tooltip="Invites" iconSrc={InviteIC} diff --git a/src/app/organisms/navigation/SideBar.scss b/src/app/organisms/navigation/SideBar.scss index 0f4e677..48970fb 100644 --- a/src/app/organisms/navigation/SideBar.scss +++ b/src/app/organisms/navigation/SideBar.scss @@ -39,13 +39,12 @@ height: 8px; background: transparent; - // background-image: linear-gradient(to top, var(--bg-surface-low), transparent); - // It produce bug in safari - // To fix it, we have to set the color as a fully transparent version of that exact color. like: - // background-image: linear-gradient(to top, rgb(255, 255, 255), rgba(255, 255, 255, 0)); - // TODO: fix this bug while implementing spaces + background-image: linear-gradient( + to top, + var(--bg-surface-low), + var(--bg-surface-low-transparent)); position: sticky; - bottom: 0; + bottom: -1px; left: 0; } } diff --git a/src/app/organisms/profile-editor/ProfileEditor.jsx b/src/app/organisms/profile-editor/ProfileEditor.jsx new file mode 100644 index 0000000..7125f44 --- /dev/null +++ b/src/app/organisms/profile-editor/ProfileEditor.jsx @@ -0,0 +1,89 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; + +import initMatrix from '../../../client/initMatrix'; +import colorMXID from '../../../util/colorMXID'; + +import Button from '../../atoms/button/Button'; +import ImageUpload from '../../molecules/image-upload/ImageUpload'; +import Input from '../../atoms/input/Input'; + +import './ProfileEditor.scss'; + +// TODO Fix bug that prevents 'Save' button from enabling up until second changed. +function ProfileEditor({ + userId, +}) { + const mx = initMatrix.matrixClient; + const displayNameRef = useRef(null); + const bgColor = colorMXID(userId); + const [avatarSrc, setAvatarSrc] = useState(mx.mxcUrlToHttp(mx.getUser(mx.getUserId()).avatarUrl, 80, 80, 'crop') || null); + const [disabled, setDisabled] = useState(true); + + let username = mx.getUser(mx.getUserId()).displayName; + + // Sets avatar URL and updates the avatar component in profile editor to reflect new upload + function handleAvatarUpload(url) { + if (url === null) { + if (confirm('Are you sure you want to remove avatar?')) { + mx.setAvatarUrl(''); + setAvatarSrc(null); + } + return; + } + mx.setAvatarUrl(url); + setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop')); + } + + function saveDisplayName() { + const newDisplayName = displayNameRef.current.value; + if (newDisplayName !== null && newDisplayName !== username) { + mx.setDisplayName(newDisplayName); + username = newDisplayName; + setDisabled(true); + } + } + + function onDisplayNameInputChange() { + setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null); + } + function cancelDisplayNameChanges() { + displayNameRef.current.value = username; + onDisplayNameInputChange(); + } + + return ( +
{ e.preventDefault(); saveDisplayName(); }} + > + handleAvatarUpload(null)} + /> +
+ + + +
+ + ); +} + +ProfileEditor.defaultProps = { + userId: null, +}; + +ProfileEditor.propTypes = { + userId: PropTypes.string, +}; + +export default ProfileEditor; diff --git a/src/app/organisms/profile-editor/ProfileEditor.scss b/src/app/organisms/profile-editor/ProfileEditor.scss new file mode 100644 index 0000000..10d62c7 --- /dev/null +++ b/src/app/organisms/profile-editor/ProfileEditor.scss @@ -0,0 +1,30 @@ +.profile-editor { + display: flex; + align-items: flex-start; +} + +.profile-editor__input-wrapper { + flex: 1; + min-width: 0; + margin-top: 10px; + + display: flex; + align-items: flex-end; + flex-wrap: wrap; + + & > .input-container { + flex: 1; + } + & > button { + height: 46px; + margin-top: var(--sp-normal); + } + + & > * { + margin-left: var(--sp-normal); + [dir=rtl] & { + margin-left: 0; + margin-right: var(--sp-normal); + } + } +} \ No newline at end of file diff --git a/src/app/organisms/public-channels/PublicChannels.jsx b/src/app/organisms/public-rooms/PublicRooms.jsx similarity index 62% rename from src/app/organisms/public-channels/PublicChannels.jsx rename to src/app/organisms/public-rooms/PublicRooms.jsx index b7388e5..b8f9244 100644 --- a/src/app/organisms/public-channels/PublicChannels.jsx +++ b/src/app/organisms/public-rooms/PublicRooms.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import './PublicChannels.scss'; +import './PublicRooms.scss'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; @@ -13,7 +13,7 @@ import IconButton from '../../atoms/button/IconButton'; import Spinner from '../../atoms/spinner/Spinner'; import Input from '../../atoms/input/Input'; import PopupWindow from '../../molecules/popup-window/PopupWindow'; -import ChannelTile from '../../molecules/channel-tile/ChannelTile'; +import RoomTile from '../../molecules/room-tile/RoomTile'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg'; @@ -53,7 +53,7 @@ function TryJoinWithAlias({ alias, onRequestClose }) { } catch (e) { setStatus({ isJoining: false, - error: `Unable to join ${alias}. Either channel is private or doesn't exist.`, + error: `Unable to join ${alias}. Either room is private or doesn't exist.`, roomId: null, tempRoomId: null, }); @@ -84,38 +84,38 @@ TryJoinWithAlias.propTypes = { onRequestClose: PropTypes.func.isRequired, }; -function PublicChannels({ isOpen, searchTerm, onRequestClose }) { +function PublicRooms({ isOpen, searchTerm, onRequestClose }) { const [isSearching, updateIsSearching] = useState(false); const [isViewMore, updateIsViewMore] = useState(false); - const [publicChannels, updatePublicChannels] = useState([]); + const [publicRooms, updatePublicRooms] = useState([]); const [nextBatch, updateNextBatch] = useState(undefined); const [searchQuery, updateSearchQuery] = useState({}); - const [joiningChannels, updateJoiningChannels] = useState(new Set()); + const [joiningRooms, updateJoiningRooms] = useState(new Set()); - const channelNameRef = useRef(null); + const roomNameRef = useRef(null); const hsRef = useRef(null); const userId = initMatrix.matrixClient.getUserId(); - async function searchChannels(viewMore) { - let inputChannelName = channelNameRef?.current?.value || searchTerm; + async function searchRooms(viewMore) { + let inputRoomName = roomNameRef?.current?.value || searchTerm; let isInputAlias = false; - if (typeof inputChannelName === 'string') { - isInputAlias = inputChannelName[0] === '#' && inputChannelName.indexOf(':') > 1; + if (typeof inputRoomName === 'string') { + isInputAlias = inputRoomName[0] === '#' && inputRoomName.indexOf(':') > 1; } - const hsFromAlias = (isInputAlias) ? inputChannelName.slice(inputChannelName.indexOf(':') + 1) : null; + const hsFromAlias = (isInputAlias) ? inputRoomName.slice(inputRoomName.indexOf(':') + 1) : null; let inputHs = hsFromAlias || hsRef?.current?.value; if (typeof inputHs !== 'string') inputHs = userId.slice(userId.indexOf(':') + 1); - if (typeof inputChannelName !== 'string') inputChannelName = ''; + if (typeof inputRoomName !== 'string') inputRoomName = ''; if (isSearching) return; if (viewMore !== true - && inputChannelName === searchQuery.name + && inputRoomName === searchQuery.name && inputHs === searchQuery.homeserver ) return; updateSearchQuery({ - name: inputChannelName, + name: inputRoomName, homeserver: inputHs, }); if (isViewMore !== viewMore) updateIsViewMore(viewMore); @@ -128,26 +128,26 @@ function PublicChannels({ isOpen, searchTerm, onRequestClose }) { since: viewMore ? nextBatch : undefined, include_all_networks: true, filter: { - generic_search_term: inputChannelName, + generic_search_term: inputRoomName, }, }); - const totalChannels = viewMore ? publicChannels.concat(result.chunk) : result.chunk; - updatePublicChannels(totalChannels); + const totalRooms = viewMore ? publicRooms.concat(result.chunk) : result.chunk; + updatePublicRooms(totalRooms); updateNextBatch(result.next_batch); updateIsSearching(false); updateIsViewMore(false); - if (totalChannels.length === 0) { + if (totalRooms.length === 0) { updateSearchQuery({ - error: `No result found for "${inputChannelName}" on ${inputHs}`, - alias: isInputAlias ? inputChannelName : null, + error: `No result found for "${inputRoomName}" on ${inputHs}`, + alias: isInputAlias ? inputRoomName : null, }); } } catch (e) { - updatePublicChannels([]); + updatePublicRooms([]); updateSearchQuery({ error: 'Something went wrong!', - alias: isInputAlias ? inputChannelName : null, + alias: isInputAlias ? inputRoomName : null, }); updateIsSearching(false); updateNextBatch(undefined); @@ -156,13 +156,13 @@ function PublicChannels({ isOpen, searchTerm, onRequestClose }) { } useEffect(() => { - if (isOpen) searchChannels(); + if (isOpen) searchRooms(); }, [isOpen]); function handleOnRoomAdded(roomId) { - if (joiningChannels.has(roomId)) { - joiningChannels.delete(roomId); - updateJoiningChannels(new Set(Array.from(joiningChannels))); + if (joiningRooms.has(roomId)) { + joiningRooms.delete(roomId); + updateJoiningRooms(new Set(Array.from(joiningRooms))); } } useEffect(() => { @@ -170,36 +170,36 @@ function PublicChannels({ isOpen, searchTerm, onRequestClose }) { return () => { initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleOnRoomAdded); }; - }, [joiningChannels]); + }, [joiningRooms]); - function handleViewChannel(roomId) { + function handleViewRoom(roomId) { selectRoom(roomId); onRequestClose(); } - function joinChannel(roomIdOrAlias) { - joiningChannels.add(roomIdOrAlias); - updateJoiningChannels(new Set(Array.from(joiningChannels))); + function joinRoom(roomIdOrAlias) { + joiningRooms.add(roomIdOrAlias); + updateJoiningRooms(new Set(Array.from(joiningRooms))); roomActions.join(roomIdOrAlias, false); } - function renderChannelList(channels) { - return channels.map((channel) => { - const alias = typeof channel.canonical_alias === 'string' ? channel.canonical_alias : channel.room_id; - const name = typeof channel.name === 'string' ? channel.name : alias; - const isJoined = initMatrix.roomList.rooms.has(channel.room_id); + function renderRoomList(rooms) { + return rooms.map((room) => { + const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id; + const name = typeof room.name === 'string' ? room.name : alias; + const isJoined = initMatrix.roomList.rooms.has(room.room_id); return ( - - {isJoined && } - {!isJoined && (joiningChannels.has(channel.room_id) ? : )} + {isJoined && } + {!isJoined && (joiningRooms.has(room.room_id) ? : )} )} /> @@ -210,26 +210,26 @@ function PublicChannels({ isOpen, searchTerm, onRequestClose }) { return ( } onRequestClose={onRequestClose} > -
-
{ e.preventDefault(); searchChannels(); }}> -
- +
+ { e.preventDefault(); searchRooms(); }}> +
+
-
+
{ typeof searchQuery.name !== 'undefined' && isSearching && ( searchQuery.name === '' ? (
- {`Loading public channels from ${searchQuery.homeserver}...`} + {`Loading public rooms from ${searchQuery.homeserver}...`}
) : ( @@ -243,28 +243,28 @@ function PublicChannels({ isOpen, searchTerm, onRequestClose }) { { typeof searchQuery.name !== 'undefined' && !isSearching && ( searchQuery.name === '' - ? {`Public channels on ${searchQuery.homeserver}.`} + ? {`Public rooms on ${searchQuery.homeserver}.`} : {`Search result for "${searchQuery.name}" on ${searchQuery.homeserver}.`} ) } { searchQuery.error && ( <> - {searchQuery.error} + {searchQuery.error} {typeof searchQuery.alias === 'string' && ( )} )}
- { publicChannels.length !== 0 && ( -
- { renderChannelList(publicChannels) } + { publicRooms.length !== 0 && ( +
+ { renderRoomList(publicRooms) }
)} - { publicChannels.length !== 0 && publicChannels.length % SEARCH_LIMIT === 0 && ( -
+ { publicRooms.length !== 0 && publicRooms.length % SEARCH_LIMIT === 0 && ( +
{ isViewMore !== true && ( - + )} { isViewMore && }
@@ -274,14 +274,14 @@ function PublicChannels({ isOpen, searchTerm, onRequestClose }) { ); } -PublicChannels.defaultProps = { +PublicRooms.defaultProps = { searchTerm: undefined, }; -PublicChannels.propTypes = { +PublicRooms.propTypes = { isOpen: PropTypes.bool.isRequired, searchTerm: PropTypes.string, onRequestClose: PropTypes.func.isRequired, }; -export default PublicChannels; +export default PublicRooms; diff --git a/src/app/organisms/public-channels/PublicChannels.scss b/src/app/organisms/public-rooms/PublicRooms.scss similarity index 97% rename from src/app/organisms/public-channels/PublicChannels.scss rename to src/app/organisms/public-rooms/PublicRooms.scss index 3eef310..66b77c0 100644 --- a/src/app/organisms/public-channels/PublicChannels.scss +++ b/src/app/organisms/public-rooms/PublicRooms.scss @@ -1,4 +1,4 @@ -.public-channels { +.public-rooms { margin: 0 var(--sp-normal); margin-right: var(--sp-extra-tight); margin-top: var(--sp-extra-tight); @@ -75,7 +75,7 @@ } } - & .channel-tile { + & .room-tile { margin-top: var(--sp-normal); &__options { align-self: flex-end; diff --git a/src/app/organisms/pw/Windows.jsx b/src/app/organisms/pw/Windows.jsx index 8a0afd3..32a0ee1 100644 --- a/src/app/organisms/pw/Windows.jsx +++ b/src/app/organisms/pw/Windows.jsx @@ -4,17 +4,17 @@ import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import InviteList from '../invite-list/InviteList'; -import PublicChannels from '../public-channels/PublicChannels'; -import CreateChannel from '../create-channel/CreateChannel'; +import PublicRooms from '../public-rooms/PublicRooms'; +import CreateRoom from '../create-room/CreateRoom'; import InviteUser from '../invite-user/InviteUser'; import Settings from '../settings/Settings'; function Windows() { const [isInviteList, changeInviteList] = useState(false); - const [publicChannels, changePublicChannels] = useState({ + const [publicRooms, changePublicRooms] = useState({ isOpen: false, searchTerm: undefined, }); - const [isCreateChannel, changeCreateChannel] = useState(false); + const [isCreateRoom, changeCreateRoom] = useState(false); const [inviteUser, changeInviteUser] = useState({ isOpen: false, roomId: undefined, term: undefined, }); @@ -23,14 +23,14 @@ function Windows() { function openInviteList() { changeInviteList(true); } - function openPublicChannels(searchTerm) { - changePublicChannels({ + function openPublicRooms(searchTerm) { + changePublicRooms({ isOpen: true, searchTerm, }); } - function openCreateChannel() { - changeCreateChannel(true); + function openCreateRoom() { + changeCreateRoom(true); } function openInviteUser(roomId, searchTerm) { changeInviteUser({ @@ -45,14 +45,14 @@ function Windows() { useEffect(() => { navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); - navigation.on(cons.events.navigation.PUBLIC_CHANNELS_OPENED, openPublicChannels); - navigation.on(cons.events.navigation.CREATE_CHANNEL_OPENED, openCreateChannel); + navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); + navigation.on(cons.events.navigation.CREATE_ROOM_OPENED, openCreateRoom); navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings); return () => { navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); - navigation.removeListener(cons.events.navigation.PUBLIC_CHANNELS_OPENED, openPublicChannels); - navigation.removeListener(cons.events.navigation.CREATE_CHANNEL_OPENED, openCreateChannel); + navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); + navigation.removeListener(cons.events.navigation.CREATE_ROOM_OPENED, openCreateRoom); navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings); }; @@ -64,14 +64,14 @@ function Windows() { isOpen={isInviteList} onRequestClose={() => changeInviteList(false)} /> - changePublicChannels({ isOpen: false, searchTerm: undefined })} + changePublicRooms({ isOpen: false, searchTerm: undefined })} /> - changeCreateChannel(false)} + changeCreateRoom(false)} /> ( + rule.rule_id === roomId + && rule.actions[0] === 'dont_notify' + && rule.conditions[0].kind === 'event_match' + )); + + return isMuteOverride ? cons.notifs.MUTE : cons.notifs.DEFAULT; + } + if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES; + return cons.notifs.MENTIONS_AND_KEYWORDS; +} + +function setRoomNotifMute(roomId) { + const mx = initMatrix.matrixClient; + const roomPushRule = mx.getRoomPushRule('global', roomId); + + const promises = []; + if (roomPushRule) { + promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id)); + } + + promises.push(mx.addPushRule('global', 'override', roomId, { + conditions: [ + { + kind: 'event_match', + key: 'room_id', + pattern: roomId, + }, + ], + actions: [ + 'dont_notify', + ], + })); + + return Promise.all(promises); +} + +function setRoomNotifsState(newState, roomId) { + const mx = initMatrix.matrixClient; + const promises = []; + + const oldState = getNotifState(roomId); + if (oldState === cons.notifs.MUTE) { + promises.push(mx.deletePushRule('global', 'override', roomId)); + } + + if (newState === cons.notifs.DEFAULT) { + const roomPushRule = mx.getRoomPushRule('global', roomId); + if (roomPushRule) { + promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id)); + } + return Promise.all(promises); + } + + if (newState === cons.notifs.MENTIONS_AND_KEYWORDS) { + promises.push(mx.addPushRule('global', 'room', roomId, { + actions: [ + 'dont_notify', + ], + })); + promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true)); + return Promise.all(promises); + } + + // cons.notifs.ALL_MESSAGES + promises.push(mx.addPushRule('global', 'room', roomId, { + actions: [ + 'notify', + { + set_tweak: 'sound', + value: 'default', + }, + ], + })); + + promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true)); + + return Promise.all(promises); +} + +function setRoomNotifPushRule(notifState, roomId) { + if (notifState === cons.notifs.MUTE) { + setRoomNotifMute(roomId); + return; + } + setRoomNotifsState(notifState, roomId); +} + +let isRoomOptionVisible = false; +let roomId = null; +function RoomOptions() { + const openerRef = useRef(null); + const [notifState, setNotifState] = useState(cons.notifs.DEFAULT); + + function openRoomOptions(cords, rId) { + if (roomId !== null || isRoomOptionVisible) { + roomId = null; + if (cords.detail === 0) openerRef.current.click(); + return; + } + openerRef.current.style.transform = `translate(${cords.x}px, ${cords.y}px)`; + roomId = rId; + setNotifState(getNotifState(roomId)); + openerRef.current.click(); + } + + function afterRoomOptionsToggle(isVisible) { + isRoomOptionVisible = isVisible; + if (!isVisible) { + setTimeout(() => { + if (!isRoomOptionVisible) roomId = null; + }, 500); + } + } + + useEffect(() => { + navigation.on(cons.events.navigation.ROOMOPTIONS_OPENED, openRoomOptions); + return () => { + navigation.on(cons.events.navigation.ROOMOPTIONS_OPENED, openRoomOptions); + }; + }, []); + + const handleInviteClick = () => openInviteUser(roomId); + const handleLeaveClick = () => { + if (confirm('Are you really want to leave this room?')) roomActions.leave(roomId); + }; + + function setNotif(nState, currentNState) { + if (nState === currentNState) return; + setRoomNotifPushRule(nState, roomId); + setNotifState(nState); + } + + return ( + ( + <> + {`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`} + { + handleInviteClick(); toggleMenu(); + }} + > + Invite + + Leave + Notification + setNotif(cons.notifs.DEFAULT, notifState)} + > + Default + + setNotif(cons.notifs.ALL_MESSAGES, notifState)} + > + All messages + + setNotif(cons.notifs.MENTIONS_AND_KEYWORDS, notifState)} + > + Mentions & Keywords + + setNotif(cons.notifs.MUTE, notifState)} + > + Mute + + + )} + render={(toggleMenu) => ( + + )} + /> + ); +} + +export default RoomOptions; diff --git a/src/app/organisms/room-optons/RoomOptions.scss b/src/app/organisms/room-optons/RoomOptions.scss new file mode 100644 index 0000000..ae3f9c3 --- /dev/null +++ b/src/app/organisms/room-optons/RoomOptions.scss @@ -0,0 +1,20 @@ +.context-menu__item { + position: relative; +} + +.context-menu__item .btn-positive::before { + content: ''; + display: inline-block; + width: 3px; + height: 12px; + background: var(--bg-positive); + border-radius: 0 4px 4px 0; + position: absolute; + left: 0; + + [dir=rtl] & { + left: unset; + right: 0; + border-radius: 4px 0 0 4px; + } +} \ No newline at end of file diff --git a/src/app/organisms/channel/PeopleDrawer.jsx b/src/app/organisms/room/PeopleDrawer.jsx similarity index 81% rename from src/app/organisms/channel/PeopleDrawer.jsx rename to src/app/organisms/room/PeopleDrawer.jsx index 2a7b18d..ca975d1 100644 --- a/src/app/organisms/channel/PeopleDrawer.jsx +++ b/src/app/organisms/room/PeopleDrawer.jsx @@ -18,23 +18,15 @@ import PeopleSelector from '../../molecules/people-selector/PeopleSelector'; import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg'; function getPowerLabel(powerLevel) { - switch (powerLevel) { - case 100: - return 'Admin'; - case 50: - return 'Mod'; - default: - return null; - } + if (powerLevel > 9000) return 'Goku'; + if (powerLevel > 100) return 'Founder'; + if (powerLevel === 100) return 'Admin'; + if (powerLevel >= 50) return 'Mod'; + return null; } -function compare(m1, m2) { - let aName = m1.name; - let bName = m2.name; - - // remove "#" from the room name - // To ignore it in sorting - aName = aName.replaceAll('#', ''); - bName = bName.replaceAll('#', ''); +function AtoZ(m1, m2) { + const aName = m1.name; + const bName = m2.name; if (aName.toLowerCase() < bName.toLowerCase()) { return -1; @@ -45,25 +37,18 @@ function compare(m1, m2) { return 0; } function sortByPowerLevel(m1, m2) { - let pl1 = String(m1.powerLevel); - let pl2 = String(m2.powerLevel); + const pl1 = m1.powerLevel; + const pl2 = m2.powerLevel; - if (pl1 === '100') pl1 = '90.9'; - if (pl2 === '100') pl2 = '90.9'; - - if (pl1.toLowerCase() > pl2.toLowerCase()) { - return -1; - } - if (pl1.toLowerCase() < pl2.toLowerCase()) { - return 1; - } + if (pl1 > pl2) return -1; + if (pl1 < pl2) return 1; return 0; } function PeopleDrawer({ roomId }) { const PER_PAGE_MEMBER = 50; const room = initMatrix.matrixClient.getRoom(roomId); - const totalMemberList = room.getJoinedMembers().sort(compare).sort(sortByPowerLevel); + const totalMemberList = room.getJoinedMembers().sort(AtoZ).sort(sortByPowerLevel); const [memberList, updateMemberList] = useState([]); let isRoomChanged = false; @@ -75,7 +60,7 @@ function PeopleDrawer({ roomId }) { updateMemberList(totalMemberList.slice(0, PER_PAGE_MEMBER)); room.loadMembersIfNeeded().then(() => { if (isRoomChanged) return; - const newTotalMemberList = room.getJoinedMembers().sort(compare).sort(sortByPowerLevel); + const newTotalMemberList = room.getJoinedMembers().sort(AtoZ).sort(sortByPowerLevel); updateMemberList(newTotalMemberList.slice(0, PER_PAGE_MEMBER)); }); diff --git a/src/app/organisms/channel/PeopleDrawer.scss b/src/app/organisms/room/PeopleDrawer.scss similarity index 100% rename from src/app/organisms/channel/PeopleDrawer.scss rename to src/app/organisms/room/PeopleDrawer.scss diff --git a/src/app/organisms/channel/Channel.jsx b/src/app/organisms/room/Room.jsx similarity index 85% rename from src/app/organisms/channel/Channel.jsx rename to src/app/organisms/room/Room.jsx index d980152..6112d2b 100644 --- a/src/app/organisms/channel/Channel.jsx +++ b/src/app/organisms/room/Room.jsx @@ -1,14 +1,14 @@ import React, { useState, useEffect } from 'react'; -import './Channel.scss'; +import './Room.scss'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import Welcome from '../welcome/Welcome'; -import ChannelView from './ChannelView'; +import RoomView from './RoomView'; import PeopleDrawer from './PeopleDrawer'; -function Channel() { +function Room() { const [selectedRoomId, changeSelectedRoomId] = useState(null); const [isDrawerVisible, toggleDrawerVisiblity] = useState(navigation.isPeopleDrawerVisible); useEffect(() => { @@ -30,11 +30,11 @@ function Channel() { if (selectedRoomId === null) return ; return ( -
- +
+ { isDrawerVisible && }
); } -export default Channel; +export default Room; diff --git a/src/app/organisms/channel/Channel.scss b/src/app/organisms/room/Room.scss similarity index 60% rename from src/app/organisms/channel/Channel.scss rename to src/app/organisms/room/Room.scss index 1d6b6ee..cea4bad 100644 --- a/src/app/organisms/channel/Channel.scss +++ b/src/app/organisms/room/Room.scss @@ -1,4 +1,4 @@ -.channel-container { +.room-container { display: flex; height: 100%; } \ No newline at end of file diff --git a/src/app/organisms/channel/ChannelView.jsx b/src/app/organisms/room/RoomView.jsx similarity index 83% rename from src/app/organisms/channel/ChannelView.jsx rename to src/app/organisms/room/RoomView.jsx index 07b9bf1..edb427d 100644 --- a/src/app/organisms/channel/ChannelView.jsx +++ b/src/app/organisms/room/RoomView.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import './ChannelView.scss'; +import './RoomView.scss'; import EventEmitter from 'events'; @@ -8,11 +8,11 @@ import RoomTimeline from '../../../client/state/RoomTimeline'; import ScrollView from '../../atoms/scroll/ScrollView'; -import ChannelViewHeader from './ChannelViewHeader'; -import ChannelViewContent from './ChannelViewContent'; -import ChannelViewFloating from './ChannelViewFloating'; -import ChannelViewInput from './ChannelViewInput'; -import ChannelViewCmdBar from './ChannelViewCmdBar'; +import RoomViewHeader from './RoomViewHeader'; +import RoomViewContent from './RoomViewContent'; +import RoomViewFloating from './RoomViewFloating'; +import RoomViewInput from './RoomViewInput'; +import RoomViewCmdBar from './RoomViewCmdBar'; import { scrollToBottom, isAtBottom, autoScrollToBottom } from './common'; @@ -22,7 +22,7 @@ let lastScrollTop = 0; let lastScrollHeight = 0; let isReachedBottom = true; let isReachedTop = false; -function ChannelView({ roomId }) { +function RoomView({ roomId }) { const [roomTimeline, updateRoomTimeline] = useState(null); const timelineSVRef = useRef(null); @@ -101,13 +101,13 @@ function ChannelView({ roomId }) { } return ( -
- -
-
+
+ +
+
{roomTimeline !== null && ( - {roomTimeline !== null && ( - {roomTimeline !== null && ( -
- + - ); } -ChannelView.propTypes = { +RoomView.propTypes = { roomId: PropTypes.string.isRequired, }; -export default ChannelView; +export default RoomView; diff --git a/src/app/organisms/channel/ChannelView.scss b/src/app/organisms/room/RoomView.scss similarity index 57% rename from src/app/organisms/channel/ChannelView.scss rename to src/app/organisms/room/RoomView.scss index a50a9ae..dd7e961 100644 --- a/src/app/organisms/channel/ChannelView.scss +++ b/src/app/organisms/room/RoomView.scss @@ -1,24 +1,24 @@ -.channel-view-flexBox { +.room-view-flexBox { display: flex; flex-direction: column; } -.channel-view-flexItem { +.room-view-flexItem { flex: 1; min-height: 0; min-width: 0; } -.channel-view { - @extend .channel-view-flexItem; - @extend .channel-view-flexBox; +.room-view { + @extend .room-view-flexItem; + @extend .room-view-flexBox; &__content-wrapper { - @extend .channel-view-flexItem; - @extend .channel-view-flexBox; + @extend .room-view-flexItem; + @extend .room-view-flexBox; } &__scrollable { - @extend .channel-view-flexItem; + @extend .room-view-flexItem; position: relative; } diff --git a/src/app/organisms/channel/ChannelViewCmdBar.jsx b/src/app/organisms/room/RoomViewCmdBar.jsx similarity index 94% rename from src/app/organisms/channel/ChannelViewCmdBar.jsx rename to src/app/organisms/room/RoomViewCmdBar.jsx index 40d3ff5..329d46a 100644 --- a/src/app/organisms/channel/ChannelViewCmdBar.jsx +++ b/src/app/organisms/room/RoomViewCmdBar.jsx @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import './ChannelViewCmdBar.scss'; +import './RoomViewCmdBar.scss'; import parse from 'html-react-parser'; import twemoji from 'twemoji'; @@ -10,9 +10,10 @@ import cons from '../../../client/state/cons'; import { toggleMarkdown } from '../../../client/action/settings'; import * as roomActions from '../../../client/action/room'; import { + selectTab, selectRoom, - openCreateChannel, - openPublicChannels, + openCreateRoom, + openPublicRooms, openInviteUser, openReadReceipts, } from '../../../client/action/navigation'; @@ -41,17 +42,17 @@ const commands = [{ description: 'Start direct message with user. Example: /startDM/@johndoe.matrix.org', exe: (roomId, searchTerm) => openInviteUser(undefined, searchTerm), }, { - name: 'createChannel', - description: 'Create new channel', - exe: () => openCreateChannel(), + name: 'createRoom', + description: 'Create new room', + exe: () => openCreateRoom(), }, { name: 'join', isOptions: true, - description: 'Join channel with alias. Example: /join/#cinny:matrix.org', - exe: (roomId, searchTerm) => openPublicChannels(searchTerm), + description: 'Join room with alias. Example: /join/#cinny:matrix.org', + exe: (roomId, searchTerm) => openPublicRooms(searchTerm), }, { name: 'leave', - description: 'Leave current channel', + description: 'Leave current room', exe: (roomId) => roomActions.leave(roomId), }, { name: 'invite', @@ -70,10 +71,10 @@ function CmdHelp() { /command_name Go-to commands {'>*space_name'} - {'>#channel_name'} + {'>#room_name'} {'>@people_name'} - Autofill command - :emoji_name: + Autofill commands + :emoji_name @name )} @@ -174,7 +175,7 @@ function getCmdActivationMessage(prefix) { const cmd = { '/': () => genMessage('General command mode activated. ', 'Type command name for suggestions.'), '>*': () => genMessage('Go-to command mode activated. ', 'Type space name for suggestions.'), - '>#': () => genMessage('Go-to command mode activated. ', 'Type channel name for suggestions.'), + '>#': () => genMessage('Go-to command mode activated. ', 'Type room name for suggestions.'), '>@': () => genMessage('Go-to command mode activated. ', 'Type people name for suggestions.'), ':': () => genMessage('Emoji autofill command mode activated. ', 'Type emoji shortcut for suggestions.'), '@': () => genMessage('Name autofill command mode activated. ', 'Type name for suggestions.'), @@ -273,7 +274,7 @@ function getCmdSuggestions({ prefix, option, suggestions }, fireCmd) { const cmd = { '/': (cmds) => getGenCmdSuggestions(prefix, cmds), '>*': (spaces) => getRoomsSuggestion(prefix, spaces), - '>#': (channels) => getRoomsSuggestion(prefix, channels), + '>#': (rooms) => getRoomsSuggestion(prefix, rooms), '>@': (peoples) => getRoomsSuggestion(prefix, peoples), ':': (emos) => getEmojiSuggestion(prefix, emos), '@': (members) => getNameSuggestion(prefix, members), @@ -284,7 +285,7 @@ function getCmdSuggestions({ prefix, option, suggestions }, fireCmd) { const asyncSearch = new AsyncSearch(); let cmdPrefix; let cmdOption; -function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) { +function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) { const [cmd, setCmd] = useState(null); function displaySuggestions(suggestions) { @@ -357,7 +358,8 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) { } function fireCmd(myCmd) { if (myCmd.prefix.match(/^>[*#@]$/)) { - selectRoom(myCmd.result.roomId); + if (cmd.prefix === '>*') selectTab(myCmd.result.roomId); + else selectRoom(myCmd.result.roomId); viewEvent.emit('cmd_fired'); } if (myCmd.prefix === '/') { @@ -466,10 +468,10 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
); } -ChannelViewCmdBar.propTypes = { +RoomViewCmdBar.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, viewEvent: PropTypes.shape({}).isRequired, }; -export default ChannelViewCmdBar; +export default RoomViewCmdBar; diff --git a/src/app/organisms/channel/ChannelViewCmdBar.scss b/src/app/organisms/room/RoomViewCmdBar.scss similarity index 100% rename from src/app/organisms/channel/ChannelViewCmdBar.scss rename to src/app/organisms/room/RoomViewCmdBar.scss diff --git a/src/app/organisms/channel/ChannelViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx similarity index 95% rename from src/app/organisms/channel/ChannelViewContent.jsx rename to src/app/organisms/room/RoomViewContent.jsx index 063718b..efa8318 100644 --- a/src/app/organisms/channel/ChannelViewContent.jsx +++ b/src/app/organisms/room/RoomViewContent.jsx @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React, { useState, useEffect, useLayoutEffect } from 'react'; import PropTypes from 'prop-types'; -import './ChannelViewContent.scss'; +import './RoomViewContent.scss'; import dateFormat from 'dateformat'; @@ -10,7 +10,7 @@ import cons from '../../../client/state/cons'; import { redactEvent, sendReaction } from '../../../client/action/roomTimeline'; import { getUsername, getUsernameOfRoomMember, doesRoomHaveUnread } from '../../../util/matrixUtil'; import colorMXID from '../../../util/colorMXID'; -import { diffMinutes, isNotInSameDay } from '../../../util/common'; +import { diffMinutes, isNotInSameDay, getEventCords } from '../../../util/common'; import { openEmojiBoard, openReadReceipts } from '../../../client/action/navigation'; import Divider from '../../atoms/divider/Divider'; @@ -29,7 +29,7 @@ import { PlaceholderMessage, } from '../../molecules/message/Message'; import * as Media from '../../molecules/media/Media'; -import ChannelIntro from '../../molecules/channel-intro/ChannelIntro'; +import RoomIntro from '../../molecules/room-intro/RoomIntro'; import TimelineChange from '../../molecules/message/TimelineChange'; import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg'; @@ -131,20 +131,20 @@ function genMediaContent(mE) { } } -function genChannelIntro(mEvent, roomTimeline) { +function genRoomIntro(mEvent, roomTimeline) { const mx = initMatrix.matrixClient; const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic; const isDM = initMatrix.roomList.directs.has(roomTimeline.roomId); let avatarSrc = roomTimeline.room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop'); avatarSrc = isDM ? roomTimeline.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc; return ( - ); @@ -176,19 +176,14 @@ function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) { } function pickEmoji(e, roomId, eventId, roomTimeline) { - const boxInfo = e.target.getBoundingClientRect(); - openEmojiBoard({ - x: boxInfo.x, - y: boxInfo.y, - detail: e.detail, - }, (emoji) => { + openEmojiBoard(getEventCords(e), (emoji) => { toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline); e.target.click(); }); } let wasAtBottom = true; -function ChannelViewContent({ +function RoomViewContent({ roomId, roomTimeline, timelineScroll, viewEvent, }) { const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false); @@ -203,7 +198,9 @@ function ChannelViewContent({ } function trySendingReadReceipt() { const { room, timeline } = roomTimeline; - if (doesRoomHaveUnread(room) && timeline.length !== 0) { + if ( + (doesRoomHaveUnread(room) || initMatrix.notifications.hasNoti(roomId)) + && timeline.length !== 0) { mx.sendReadReceipt(timeline[timeline.length - 1]); } } @@ -517,7 +514,7 @@ function ChannelViewContent({ } function renderMessage(mEvent) { - if (mEvent.getType() === 'm.room.create') return genChannelIntro(mEvent, roomTimeline); + if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline); if ( mEvent.getType() !== 'm.room.message' && mEvent.getType() !== 'm.room.encrypted' @@ -562,20 +559,20 @@ function ChannelViewContent({ } return ( -
+
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && genPlaceholders() } - { roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genChannelIntro(undefined, roomTimeline)} + { roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genRoomIntro(undefined, roomTimeline)} { roomTimeline.timeline.map(renderMessage) }
); } -ChannelViewContent.propTypes = { +RoomViewContent.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, timelineScroll: PropTypes.shape({}).isRequired, viewEvent: PropTypes.shape({}).isRequired, }; -export default ChannelViewContent; +export default RoomViewContent; diff --git a/src/app/organisms/channel/ChannelViewContent.scss b/src/app/organisms/room/RoomViewContent.scss similarity index 90% rename from src/app/organisms/channel/ChannelViewContent.scss rename to src/app/organisms/room/RoomViewContent.scss index f270233..cfb328c 100644 --- a/src/app/organisms/channel/ChannelViewContent.scss +++ b/src/app/organisms/room/RoomViewContent.scss @@ -1,4 +1,4 @@ -.channel-view__content { +.room-view__content { min-height: 100%; display: flex; flex-direction: column; diff --git a/src/app/organisms/channel/ChannelViewFloating.jsx b/src/app/organisms/room/RoomViewFloating.jsx similarity index 86% rename from src/app/organisms/channel/ChannelViewFloating.jsx rename to src/app/organisms/room/RoomViewFloating.jsx index e3e65da..56b7a9b 100644 --- a/src/app/organisms/channel/ChannelViewFloating.jsx +++ b/src/app/organisms/room/RoomViewFloating.jsx @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import './ChannelViewFloating.scss'; +import './RoomViewFloating.scss'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; @@ -13,7 +13,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s import { getUsersActionJsx } from './common'; -function ChannelViewFloating({ +function RoomViewFloating({ roomId, roomTimeline, timelineScroll, viewEvent, }) { const [reachedBottom, setReachedBottom] = useState(true); @@ -53,11 +53,11 @@ function ChannelViewFloating({ return ( <> -
+
{getTypingMessage(typingMembers)}
-
+
{ timelineScroll.enableSmoothScroll(); @@ -71,7 +71,7 @@ function ChannelViewFloating({ ); } -ChannelViewFloating.propTypes = { +RoomViewFloating.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, timelineScroll: PropTypes.shape({ @@ -80,4 +80,4 @@ ChannelViewFloating.propTypes = { viewEvent: PropTypes.shape({}).isRequired, }; -export default ChannelViewFloating; +export default RoomViewFloating; diff --git a/src/app/organisms/channel/ChannelViewFloating.scss b/src/app/organisms/room/RoomViewFloating.scss similarity index 98% rename from src/app/organisms/channel/ChannelViewFloating.scss rename to src/app/organisms/room/RoomViewFloating.scss index 3c1593c..501c9f4 100644 --- a/src/app/organisms/channel/ChannelViewFloating.scss +++ b/src/app/organisms/room/RoomViewFloating.scss @@ -1,4 +1,4 @@ -.channel-view { +.room-view { &__typing { display: flex; padding: var(--sp-ultra-tight) var(--sp-normal); diff --git a/src/app/organisms/channel/ChannelViewHeader.jsx b/src/app/organisms/room/RoomViewHeader.jsx similarity index 56% rename from src/app/organisms/channel/ChannelViewHeader.jsx rename to src/app/organisms/room/RoomViewHeader.jsx index f89b634..e51cbd3 100644 --- a/src/app/organisms/channel/ChannelViewHeader.jsx +++ b/src/app/organisms/room/RoomViewHeader.jsx @@ -2,22 +2,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import initMatrix from '../../../client/initMatrix'; -import { togglePeopleDrawer, openInviteUser } from '../../../client/action/navigation'; -import * as roomActions from '../../../client/action/room'; +import { togglePeopleDrawer, openRoomOptions } from '../../../client/action/navigation'; import colorMXID from '../../../util/colorMXID'; +import { getEventCords } from '../../../util/common'; import Text from '../../atoms/text/Text'; import IconButton from '../../atoms/button/IconButton'; import Header, { TitleWrapper } from '../../atoms/header/Header'; import Avatar from '../../atoms/avatar/Avatar'; -import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu'; import UserIC from '../../../../public/res/ic/outlined/user.svg'; import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; -import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; -import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg'; -function ChannelViewHeader({ roomId }) { +function RoomViewHeader({ roomId }) { const mx = initMatrix.matrixClient; const isDM = initMatrix.roomList.directs.has(roomId); let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop'); @@ -33,30 +30,16 @@ function ChannelViewHeader({ roomId }) { { typeof roomTopic !== 'undefined' &&

{roomTopic}

} - ( - <> - Options - {/* */} - { - openInviteUser(roomId); toogleMenu(); - }} - > - Invite - - roomActions.leave(roomId)}>Leave - - )} - render={(toggleMenu) => } + openRoomOptions(getEventCords(e), roomId)} + tooltip="Options" + src={VerticalMenuIC} /> ); } -ChannelViewHeader.propTypes = { +RoomViewHeader.propTypes = { roomId: PropTypes.string.isRequired, }; -export default ChannelViewHeader; +export default RoomViewHeader; diff --git a/src/app/organisms/channel/ChannelViewInput.jsx b/src/app/organisms/room/RoomViewInput.jsx similarity index 88% rename from src/app/organisms/channel/ChannelViewInput.jsx rename to src/app/organisms/room/RoomViewInput.jsx index f335bb4..edad9c9 100644 --- a/src/app/organisms/channel/ChannelViewInput.jsx +++ b/src/app/organisms/room/RoomViewInput.jsx @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; -import './ChannelViewInput.scss'; +import './RoomViewInput.scss'; import TextareaAutosize from 'react-autosize-textarea'; @@ -9,7 +9,7 @@ import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import settings from '../../../client/state/settings'; import { openEmojiBoard } from '../../../client/action/navigation'; -import { bytesToSize } from '../../../util/common'; +import { bytesToSize, getEventCords } from '../../../util/common'; import { getUsername } from '../../../util/matrixUtil'; import colorMXID from '../../../util/colorMXID'; @@ -33,7 +33,7 @@ const CMD_REGEX = /(\/|>[#*@]|:|@)(\S*)$/; let isTyping = false; let isCmdActivated = false; let cmdCursorPos = null; -function ChannelViewInput({ +function RoomViewInput({ roomId, roomTimeline, timelineScroll, viewEvent, }) { const [attachment, setAttachment] = useState(null); @@ -285,6 +285,32 @@ function ChannelViewInput({ } } + function handlePaste(e) { + if (e.clipboardData === false) { + return; + } + + if (e.clipboardData.items === undefined) { + return; + } + + for (let i = 0; i < e.clipboardData.items.length; i += 1) { + const item = e.clipboardData.items[i]; + if (item.type.indexOf('image') !== -1) { + const image = item.getAsFile(); + if (attachment === null) { + setAttachment(image); + if (image !== null) { + roomsInput.setAttachment(roomId, image); + return; + } + } else { + return; + } + } + } + } + function addEmoji(emoji) { textAreaRef.current.value += emoji.unicode; } @@ -304,17 +330,18 @@ function ChannelViewInput({ function renderInputs() { return ( <> -
+
-
+
{roomTimeline.isEncryptedRoom() && } - + timelineScroll.autoReachBottom()} onKeyDown={handleKeyDown} placeholder="Send a message..." @@ -324,15 +351,13 @@ function ChannelViewInput({ {isMarkdown && }
-
+
{ - const boxInfo = e.target.getBoundingClientRect(); - openEmojiBoard({ - x: boxInfo.x + (document.dir === 'rtl' ? -80 : 80), - y: boxInfo.y - 250, - detail: e.detail, - }, addEmoji); + const cords = getEventCords(e); + cords.x += (document.dir === 'rtl' ? -80 : 80); + cords.y -= 250; + openEmojiBoard(cords, addEmoji); }} tooltip="Emoji" src={EmojiIC} @@ -346,14 +371,14 @@ function ChannelViewInput({ function attachFile() { const fileType = attachment.type.slice(0, attachment.type.indexOf('/')); return ( -
-
+
+
{fileType === 'image' && {attachment.name}} {fileType === 'video' && } {fileType === 'audio' && } {fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && }
-
+
{attachment.name} {`size: ${bytesToSize(attachment.size)}`}
@@ -363,7 +388,7 @@ function ChannelViewInput({ function attachReply() { return ( -
+
{ roomsInput.cancelReplyTo(roomId); @@ -387,17 +412,17 @@ function ChannelViewInput({ <> { replyTo !== null && attachReply()} { attachment !== null && attachFile() } -
{ e.preventDefault(); }}> + { e.preventDefault(); }}> { roomTimeline.room.isSpaceRoom() - ? Spaces are yet to be implemented + ? Spaces are yet to be implemented : renderInputs() }
); } -ChannelViewInput.propTypes = { +RoomViewInput.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, timelineScroll: PropTypes.shape({ @@ -410,4 +435,4 @@ ChannelViewInput.propTypes = { viewEvent: PropTypes.shape({}).isRequired, }; -export default ChannelViewInput; +export default RoomViewInput; diff --git a/src/app/organisms/channel/ChannelViewInput.scss b/src/app/organisms/room/RoomViewInput.scss similarity index 97% rename from src/app/organisms/channel/ChannelViewInput.scss rename to src/app/organisms/room/RoomViewInput.scss index 2bc0121..112a4c4 100644 --- a/src/app/organisms/channel/ChannelViewInput.scss +++ b/src/app/organisms/room/RoomViewInput.scss @@ -1,4 +1,4 @@ -.channel-input { +.room-input { padding: var(--sp-extra-tight) calc(var(--sp-normal) - 2px); display: flex; min-height: 48px; @@ -73,7 +73,7 @@ } } -.channel-attachment { +.room-attachment { --side-spacing: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight)); display: flex; align-items: center; @@ -112,7 +112,7 @@ } } -.channel-reply { +.room-reply { display: flex; align-items: center; background-color: var(--bg-surface-low); diff --git a/src/app/organisms/channel/common.jsx b/src/app/organisms/room/common.jsx similarity index 99% rename from src/app/organisms/channel/common.jsx rename to src/app/organisms/room/common.jsx index 46fbc5d..2d876d7 100644 --- a/src/app/organisms/channel/common.jsx +++ b/src/app/organisms/room/common.jsx @@ -9,7 +9,7 @@ function getTimelineJSXMessages() { return ( <> {user} - {' joined the channel'} + {' joined the room'} ); }, @@ -18,7 +18,7 @@ function getTimelineJSXMessages() { return ( <> {user} - {' left the channel'} + {' left the room'} {reasonMsg} ); diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index 37d3329..6f7bf24 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -16,6 +16,9 @@ import PopupWindow, { PWContentSelector } from '../../molecules/popup-window/Pop import SettingTile from '../../molecules/setting-tile/SettingTile'; import ImportE2ERoomKeys from '../../molecules/import-e2e-room-keys/ImportE2ERoomKeys'; +import ProfileEditor from '../profile-editor/ProfileEditor'; + +import SettingsIC from '../../../../public/res/ic/outlined/settings.svg'; import SunIC from '../../../../public/res/ic/outlined/sun.svg'; import LockIC from '../../../../public/res/ic/outlined/lock.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg'; @@ -23,6 +26,19 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CinnySVG from '../../../../public/res/svg/cinny.svg'; +function GeneralSection() { + return ( +
+ + )} + /> +
+ ); +} + function AppearanceSection() { const [, updateState] = useState({}); @@ -88,7 +104,7 @@ function AboutSection() {
Cinny - v1.2.1 + v1.3.0 Yet another matrix client @@ -104,6 +120,12 @@ function AboutSection() { function Settings({ isOpen, onRequestClose }) { const settingSections = [{ + name: 'General', + iconSrc: SettingsIC, + render() { + return ; + }, + }, { name: 'Appearance', iconSrc: SunIC, render() { diff --git a/src/app/templates/auth/Auth.jsx b/src/app/templates/auth/Auth.jsx index a8837ab..cf4b51d 100644 --- a/src/app/templates/auth/Auth.jsx +++ b/src/app/templates/auth/Auth.jsx @@ -8,12 +8,15 @@ import * as auth from '../../../client/action/auth'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; +import IconButton from '../../atoms/button/IconButton'; import Input from '../../atoms/input/Input'; import Spinner from '../../atoms/spinner/Spinner'; +import ScrollView from '../../atoms/scroll/ScrollView'; +import EyeIC from '../../../../public/res/ic/outlined/eye.svg'; import CinnySvg from '../../../../public/res/svg/cinny.svg'; -// This regex validates historical usernames, which don't satisy today's username requirements. +// This regex validates historical usernames, which don't satisfy today's username requirements. // See https://matrix.org/docs/spec/appendices#id13 for more info. const LOCALPART_LOGIN_REGEX = /.*/; const LOCALPART_SIGNUP_REGEX = /^[a-z0-9_\-.=/]+$/; @@ -206,24 +209,46 @@ function Auth({ type }) { required />
- validateOnChange(e, ((type === 'login') ? PASSWORD_REGEX : PASSWORD_STRENGHT_REGEX), BAD_PASSWORD_ERROR)} - id="auth_password" - type="password" - label="Password" - required - /> +
+ validateOnChange(e, ((type === 'login') ? PASSWORD_REGEX : PASSWORD_STRENGHT_REGEX), BAD_PASSWORD_ERROR)} + id="auth_password" + type="password" + label="Password" + required + /> + { + if (passwordRef.current.type === 'password') { + passwordRef.current.type = 'text'; + } else passwordRef.current.type = 'password'; + }} + size="extra-small" + src={EyeIC} + /> +
{type === 'register' && ( <> - validateOnChange(e, new RegExp(`^(${passwordRef.current.value})$`), CONFIRM_PASSWORD_ERROR)} - id="auth_confirmPassword" - type="password" - label="Confirm password" - required - /> +
+ validateOnChange(e, new RegExp(`^(${passwordRef.current.value})$`), CONFIRM_PASSWORD_ERROR)} + id="auth_confirmPassword" + type="password" + label="Confirm password" + required + /> + { + if (confirmPasswordRef.current.type === 'password') { + confirmPasswordRef.current.type = 'text'; + } else confirmPasswordRef.current.type = 'password'; + }} + size="extra-small" + src={EyeIC} + /> +
validateOnChange(e, EMAIL_REGEX, BAD_EMAIL_ERROR)} @@ -266,20 +291,22 @@ Auth.propTypes = { function StaticWrapper({ children }) { return ( -
-
-
-
- Cinny logo -
- Cinny - Yet another matrix client + +
+
+
+
+ Cinny logo +
+ Cinny + Yet another matrix client +
+ { children }
- { children }
-
+ ); } diff --git a/src/app/templates/auth/Auth.scss b/src/app/templates/auth/Auth.scss index 875801d..678b90f 100644 --- a/src/app/templates/auth/Auth.scss +++ b/src/app/templates/auth/Auth.scss @@ -122,6 +122,22 @@ } } +.password__wrapper { + margin-top: var(--sp-tight); + position: relative; + + & .ic-btn { + position: absolute; + right: 6px; + bottom: 6px; + border-radius: calc(var(--bo-radius) / 2); + [dir=rtl] & { + left: 6px; + right: unset; + } + } +} + @media (max-width: 462px) { .auth__wrapper { padding: 0; diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx index 3d8b45d..bf7a3e7 100644 --- a/src/app/templates/client/Client.jsx +++ b/src/app/templates/client/Client.jsx @@ -4,10 +4,11 @@ import './Client.scss'; import Text from '../../atoms/text/Text'; import Spinner from '../../atoms/spinner/Spinner'; import Navigation from '../../organisms/navigation/Navigation'; -import Channel from '../../organisms/channel/Channel'; +import Room from '../../organisms/room/Room'; import Windows from '../../organisms/pw/Windows'; import Dialogs from '../../organisms/pw/Dialogs'; import EmojiBoardOpener from '../../organisms/emoji-board/EmojiBoardOpener'; +import RoomOptions from '../../organisms/room-optons/RoomOptions'; import initMatrix from '../../../client/initMatrix'; @@ -38,12 +39,13 @@ function Client() {
-
- +
+
+
); } diff --git a/src/app/templates/client/Client.scss b/src/app/templates/client/Client.scss index f1d901e..0528098 100644 --- a/src/app/templates/client/Client.scss +++ b/src/app/templates/client/Client.scss @@ -6,7 +6,7 @@ .navigation__wrapper { width: var(--navigation-width); } -.channel__wrapper { +.room__wrapper { flex: 1; min-width: 0; background-color: var(--bg-surface); diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index 78c001f..d11aceb 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -1,13 +1,20 @@ import appDispatcher from '../dispatcher'; import cons from '../state/cons'; -function changeTab(tabId) { +function selectTab(tabId) { appDispatcher.dispatch({ - type: cons.actions.navigation.CHANGE_TAB, + type: cons.actions.navigation.SELECT_TAB, tabId, }); } +function selectSpace(roomId) { + appDispatcher.dispatch({ + type: cons.actions.navigation.SELECT_SPACE, + roomId, + }); +} + function selectRoom(roomId) { appDispatcher.dispatch({ type: cons.actions.navigation.SELECT_ROOM, @@ -27,16 +34,16 @@ function openInviteList() { }); } -function openPublicChannels(searchTerm) { +function openPublicRooms(searchTerm) { appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_PUBLIC_CHANNELS, + type: cons.actions.navigation.OPEN_PUBLIC_ROOMS, searchTerm, }); } -function openCreateChannel() { +function openCreateRoom() { appDispatcher.dispatch({ - type: cons.actions.navigation.OPEN_CREATE_CHANNEL, + type: cons.actions.navigation.OPEN_CREATE_ROOM, }); } @@ -70,15 +77,25 @@ function openReadReceipts(roomId, eventId) { }); } +function openRoomOptions(cords, roomId) { + appDispatcher.dispatch({ + type: cons.actions.navigation.OPEN_ROOMOPTIONS, + cords, + roomId, + }); +} + export { - changeTab, + selectTab, + selectSpace, selectRoom, togglePeopleDrawer, openInviteList, - openPublicChannels, - openCreateChannel, + openPublicRooms, + openCreateRoom, openInviteUser, openSettings, openEmojiBoard, openReadReceipts, + openRoomOptions, }; diff --git a/src/client/action/room.js b/src/client/action/room.js index 407a9e3..08e73e2 100644 --- a/src/client/action/room.js +++ b/src/client/action/room.js @@ -190,7 +190,22 @@ async function invite(roomId, userId) { } } +function createSpaceShortcut(roomId) { + appDispatcher.dispatch({ + type: cons.actions.room.CREATE_SPACE_SHORTCUT, + roomId, + }); +} + +function deleteSpaceShortcut(roomId) { + appDispatcher.dispatch({ + type: cons.actions.room.DELETE_SPACE_SHORTCUT, + roomId, + }); +} + export { join, leave, create, invite, + createSpaceShortcut, deleteSpaceShortcut, }; diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js index 26d07e6..91a41ea 100644 --- a/src/client/initMatrix.js +++ b/src/client/initMatrix.js @@ -4,6 +4,7 @@ import * as sdk from 'matrix-js-sdk'; import { secret } from './state/auth'; import RoomList from './state/RoomList'; import RoomsInput from './state/RoomsInput'; +import Notifications from './state/Notifications'; global.Olm = require('@matrix-org/olm'); @@ -56,6 +57,7 @@ class InitMatrix extends EventEmitter { if (prevState === null) { this.roomList = new RoomList(this.matrixClient); this.roomsInput = new RoomsInput(this.matrixClient); + this.notifications = new Notifications(this.roomList); this.emit('init_loading_finished'); } }, diff --git a/src/client/state/Notifications.js b/src/client/state/Notifications.js new file mode 100644 index 0000000..f5ecce2 --- /dev/null +++ b/src/client/state/Notifications.js @@ -0,0 +1,162 @@ +import EventEmitter from 'events'; +import cons from './cons'; + +class Notifications extends EventEmitter { + constructor(roomList) { + super(); + + this.matrixClient = roomList.matrixClient; + this.roomList = roomList; + + this.roomIdToNoti = new Map(); + + this._initNoti(); + this._listenEvents(); + + // TODO: + window.notifications = this; + } + + _initNoti() { + const addNoti = (roomId) => { + const room = this.matrixClient.getRoom(roomId); + if (this.doesRoomHaveUnread(room) === false) return; + const total = room.getUnreadNotificationCount('total'); + const highlight = room.getUnreadNotificationCount('highlight'); + const noti = this.getNoti(room.roomId); + this._setNoti(room.roomId, total - noti.total, highlight - noti.highlight); + }; + [...this.roomList.rooms].forEach(addNoti); + [...this.roomList.directs].forEach(addNoti); + } + + doesRoomHaveUnread(room) { + const userId = this.matrixClient.getUserId(); + const readUpToId = room.getEventReadUpTo(userId); + const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker']; + + if (room.timeline.length + && room.timeline[room.timeline.length - 1].sender + && room.timeline[room.timeline.length - 1].sender.userId === userId + && room.timeline[room.timeline.length - 1].getType() !== 'm.room.member') { + return false; + } + + for (let i = room.timeline.length - 1; i >= 0; i -= 1) { + const event = room.timeline[i]; + + if (event.getId() === readUpToId) return false; + + if (supportEvents.includes(event.getType())) { + return true; + } + } + return true; + } + + getNoti(roomId) { + return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null }; + } + + getTotalNoti(roomId) { + const { total } = this.getNoti(roomId); + return total; + } + + getHighlightNoti(roomId) { + const { highlight } = this.getNoti(roomId); + return highlight; + } + + getFromNoti(roomId) { + const { from } = this.getNoti(roomId); + return from; + } + + hasNoti(roomId) { + return this.roomIdToNoti.has(roomId); + } + + _setNoti(roomId, total, highlight, childId) { + const prevTotal = this.roomIdToNoti.get(roomId)?.total ?? null; + const noti = this.getNoti(roomId); + + noti.total += total; + noti.highlight += highlight; + if (childId) { + if (noti.from === null) noti.from = new Set(); + noti.from.add(childId); + } + + this.roomIdToNoti.set(roomId, noti); + this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal); + + const parentIds = this.roomList.roomIdToParents.get(roomId); + if (typeof parentIds === 'undefined') return; + [...parentIds].forEach((parentId) => this._setNoti(parentId, total, highlight, roomId)); + } + + _deleteNoti(roomId, total, highlight, childId) { + if (this.roomIdToNoti.has(roomId) === false) return; + + const noti = this.getNoti(roomId); + const prevTotal = noti.total; + noti.total -= total; + noti.highlight -= highlight; + if (childId && noti.from !== null) { + noti.from.delete(childId); + } + if (noti.from === null || noti.from.size === 0) { + this.roomIdToNoti.delete(roomId); + this.emit(cons.events.notifications.FULL_READ, roomId); + this.emit(cons.events.notifications.NOTI_CHANGED, roomId, null, prevTotal); + } else { + this.roomIdToNoti.set(roomId, noti); + this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal); + } + + const parentIds = this.roomList.roomIdToParents.get(roomId); + if (typeof parentIds === 'undefined') return; + [...parentIds].forEach((parentId) => this._deleteNoti(parentId, total, highlight, roomId)); + } + + _listenEvents() { + this.matrixClient.on('Room.timeline', (mEvent, room) => { + const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker']; + if (!supportEvents.includes(mEvent.getType())) return; + + const lastTimelineEvent = room.timeline[room.timeline.length - 1]; + if (lastTimelineEvent.getId() !== mEvent.getId()) return; + if (mEvent.getSender() === this.matrixClient.getUserId()) return; + + const total = room.getUnreadNotificationCount('total'); + const highlight = room.getUnreadNotificationCount('highlight'); + + const noti = this.getNoti(room.roomId); + this._setNoti(room.roomId, total - noti.total, highlight - noti.highlight); + }); + + this.matrixClient.on('Room.receipt', (mEvent, room) => { + if (mEvent.getType() === 'm.receipt') { + const content = mEvent.getContent(); + const readedEventId = Object.keys(content)[0]; + const readerUserId = Object.keys(content[readedEventId]['m.read'])[0]; + if (readerUserId !== this.matrixClient.getUserId()) return; + + if (this.hasNoti(room.roomId)) { + const noti = this.getNoti(room.roomId); + this._deleteNoti(room.roomId, noti.total, noti.highlight); + } + } + }); + + this.matrixClient.on('Room.myMembership', (room, membership) => { + if (membership === 'leave' && this.hasNoti(room.roomId)) { + const noti = this.getNoti(room.roomId); + this._deleteNoti(room.roomId, noti.total, noti.highlight); + } + }); + } +} + +export default Notifications; diff --git a/src/client/state/RoomList.js b/src/client/state/RoomList.js index 428d104..b746a46 100644 --- a/src/client/state/RoomList.js +++ b/src/client/state/RoomList.js @@ -2,12 +2,22 @@ import EventEmitter from 'events'; import appDispatcher from '../dispatcher'; import cons from './cons'; +function isMEventSpaceChild(mEvent) { + return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0; +} + class RoomList extends EventEmitter { constructor(matrixClient) { super(); this.matrixClient = matrixClient; this.mDirects = this.getMDirects(); + // Contains roomId to parent spaces roomId mapping of all spaces children. + // No matter if you have joined those children rooms or not. + this.roomIdToParents = new Map(); + + this.spaceShortcut = new Set(); + this.inviteDirects = new Set(); this.inviteSpaces = new Set(); this.inviteRooms = new Set(); @@ -19,18 +29,82 @@ class RoomList extends EventEmitter { this.processingRooms = new Map(); this._populateRooms(); + this._populateSpaceShortcut(); this._listenEvents(); appDispatcher.register(this.roomActions.bind(this)); } + _updateSpaceShortcutData(shortcutList) { + const spaceContent = this.matrixClient.getAccountData(cons['in.cinny.spaces'])?.getContent() || {}; + spaceContent.shortcut = shortcutList; + this.matrixClient.setAccountData(cons['in.cinny.spaces'], spaceContent); + } + + isOrphan(roomId) { + return !this.roomIdToParents.has(roomId); + } + + getOrphans() { + const rooms = [...this.spaces].concat([...this.rooms]); + return rooms.filter((roomId) => !this.roomIdToParents.has(roomId)); + } + + getSpaceChildren(roomId) { + const space = this.matrixClient.getRoom(roomId); + const mSpaceChild = space?.currentState.getStateEvents('m.space.child'); + const children = mSpaceChild?.map((mEvent) => { + const childId = mEvent.event.state_key; + if (isMEventSpaceChild(mEvent)) return childId; + return null; + }); + return children?.filter((childId) => childId !== null); + } + + addToRoomIdToParents(roomId, parentRoomId) { + if (!this.roomIdToParents.has(roomId)) { + this.roomIdToParents.set(roomId, new Set()); + } + const parents = this.roomIdToParents.get(roomId); + parents.add(parentRoomId); + } + + removeFromRoomIdToParents(roomId, parentRoomId) { + if (!this.roomIdToParents.has(roomId)) return; + const parents = this.roomIdToParents.get(roomId); + parents.delete(parentRoomId); + if (parents.size === 0) this.roomIdToParents.delete(roomId); + } + + addToSpaces(roomId) { + this.spaces.add(roomId); + const spaceChildren = this.getSpaceChildren(roomId); + spaceChildren?.forEach((childRoomId) => { + this.addToRoomIdToParents(childRoomId, roomId); + }); + } + + deleteFromSpaces(roomId) { + this.spaces.delete(roomId); + const spaceChildren = this.getSpaceChildren(roomId); + spaceChildren?.forEach((childRoomId) => { + this.removeFromRoomIdToParents(childRoomId, roomId); + }); + + if (this.spaceShortcut.has(roomId)) { + // if delete space has shortcut remove it. + this.spaceShortcut.delete(roomId); + this._updateSpaceShortcutData([...this.spaceShortcut]); + } + } + roomActions(action) { const addRoom = (roomId, isDM) => { const myRoom = this.matrixClient.getRoom(roomId); if (myRoom === null) return false; if (isDM) this.directs.add(roomId); - else if (myRoom.isSpaceRoom()) this.spaces.add(roomId); + else if (myRoom.isSpaceRoom()) this.addToSpaces(roomId); else this.rooms.add(roomId); return true; }; @@ -64,6 +138,18 @@ class RoomList extends EventEmitter { }); } }, + [cons.actions.room.CREATE_SPACE_SHORTCUT]: () => { + if (this.spaceShortcut.has(action.roomId)) return; + this.spaceShortcut.add(action.roomId); + this._updateSpaceShortcutData([...this.spaceShortcut]); + this.emit(cons.events.roomList.SPACE_SHORTCUT_UPDATED, action.roomId); + }, + [cons.actions.room.DELETE_SPACE_SHORTCUT]: () => { + if (!this.spaceShortcut.has(action.roomId)) return; + this.spaceShortcut.delete(action.roomId); + this._updateSpaceShortcutData([...this.spaceShortcut]); + this.emit(cons.events.roomList.SPACE_SHORTCUT_UPDATED, action.roomId); + }, }; actions[action.type]?.(); } @@ -83,8 +169,24 @@ class RoomList extends EventEmitter { return mDirectsId; } + _populateSpaceShortcut() { + this.spaceShortcut.clear(); + const spacesContent = this.matrixClient.getAccountData(cons['in.cinny.spaces'])?.getContent(); + + if (spacesContent && Array.isArray(spacesContent?.shortcut)) { + spacesContent.shortcut.forEach((shortcut) => { + if (this.spaces.has(shortcut)) this.spaceShortcut.add(shortcut); + }); + if (spacesContent.shortcut.length !== this.spaceShortcut.size) { + // update shortcut list from account data if shortcut space doesn't exist. + this._updateSpaceShortcutData([...this.spaceShortcut]); + } + } + } + _populateRooms() { this.directs.clear(); + this.roomIdToParents.clear(); this.spaces.clear(); this.rooms.clear(); this.inviteDirects.clear(); @@ -109,7 +211,7 @@ class RoomList extends EventEmitter { if (room.getMyMembership() !== 'join') return; if (this.mDirects.has(roomId)) this.directs.add(roomId); - else if (room.isSpaceRoom()) this.spaces.add(roomId); + else if (room.isSpaceRoom()) this.addToSpaces(roomId); else this.rooms.add(roomId); }); } @@ -123,6 +225,12 @@ class RoomList extends EventEmitter { _listenEvents() { // Update roomList when m.direct changes this.matrixClient.on('accountData', (event) => { + if (event.getType() === cons['in.cinny.spaces']) { + this._populateSpaceShortcut(); + this.emit(cons.events.roomList.SPACE_SHORTCUT_UPDATED); + return; + } + if (event.getType() !== 'm.direct') return; const latestMDirects = this.getMDirects(); @@ -155,18 +263,17 @@ class RoomList extends EventEmitter { this.matrixClient.on('Room.name', () => { this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); - this.matrixClient.on('Room.receipt', (event, room) => { - if (event.getType() === 'm.receipt') { - const content = event.getContent(); - const userReadEventId = Object.keys(content)[0]; - const eventReaderUserId = Object.keys(content[userReadEventId]['m.read'])[0]; - if (eventReaderUserId !== this.matrixClient.getUserId()) return; - this.emit(cons.events.roomList.MY_RECEIPT_ARRIVED, room.roomId); - } - }); - this.matrixClient.on('RoomState.events', (event) => { - if (event.getType() !== 'm.room.join_rules') return; + this.matrixClient.on('RoomState.events', (mEvent) => { + if (mEvent.getType() === 'm.space.child') { + const { event } = mEvent; + if (isMEventSpaceChild(mEvent)) { + this.addToRoomIdToParents(event.state_key, event.room_id); + } else this.removeFromRoomIdToParents(event.state_key, event.room_id); + this.emit(cons.events.roomList.ROOMLIST_UPDATED); + return; + } + if (mEvent.getType() !== 'm.room.join_rules') return; this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); @@ -207,7 +314,7 @@ class RoomList extends EventEmitter { const procRoomInfo = this.processingRooms.get(roomId); if (procRoomInfo.isDM) this.directs.add(roomId); - else if (room.isSpaceRoom()) this.spaces.add(roomId); + else if (room.isSpaceRoom()) this.addToSpaces(roomId); else this.rooms.add(roomId); if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId); @@ -218,7 +325,7 @@ class RoomList extends EventEmitter { return; } if (room.isSpaceRoom()) { - this.spaces.add(roomId); + this.addToSpaces(roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId); this.emit(cons.events.roomList.ROOMLIST_UPDATED); @@ -269,26 +376,17 @@ class RoomList extends EventEmitter { } // when room is not a DM add/remove it from rooms. if (membership === 'leave' || membership === 'kick' || membership === 'ban') { - if (room.isSpaceRoom()) this.spaces.delete(roomId); + if (room.isSpaceRoom()) this.deleteFromSpaces(roomId); else this.rooms.delete(roomId); this.emit(cons.events.roomList.ROOM_LEAVED, roomId); } if (membership === 'join') { - if (room.isSpaceRoom()) this.spaces.add(roomId); + if (room.isSpaceRoom()) this.addToSpaces(roomId); else this.rooms.add(roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId); } this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); - - this.matrixClient.on('Room.timeline', (event, room) => { - const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker']; - if (!supportEvents.includes(event.getType())) return; - - const lastTimelineEvent = room.timeline[room.timeline.length - 1]; - if (lastTimelineEvent.getId() !== event.getId()) return; - this.emit(cons.events.roomList.EVENT_ARRIVED, room.roomId); - }); } } export default RoomList; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index b5de3d6..fee81b5 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -6,23 +6,38 @@ const cons = { BASE_URL: 'cinny_hs_base_url', }, DEVICE_DISPLAY_NAME: 'Cinny Web', + 'in.cinny.spaces': 'in.cinny.spaces', + tabs: { + HOME: 'home', + DIRECTS: 'dm', + }, + notifs: { + DEFAULT: 'default', + ALL_MESSAGES: 'all_messages', + MENTIONS_AND_KEYWORDS: 'mentions_and_keywords', + MUTE: 'mute', + }, actions: { navigation: { - CHANGE_TAB: 'CHANGE_TAB', + SELECT_TAB: 'SELECT_TAB', + SELECT_SPACE: 'SELECT_SPACE', SELECT_ROOM: 'SELECT_ROOM', TOGGLE_PEOPLE_DRAWER: 'TOGGLE_PEOPLE_DRAWER', OPEN_INVITE_LIST: 'OPEN_INVITE_LIST', - OPEN_PUBLIC_CHANNELS: 'OPEN_PUBLIC_CHANNELS', - OPEN_CREATE_CHANNEL: 'OPEN_CREATE_CHANNEL', + OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS', + OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM', OPEN_INVITE_USER: 'OPEN_INVITE_USER', OPEN_SETTINGS: 'OPEN_SETTINGS', OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD', OPEN_READRECEIPTS: 'OPEN_READRECEIPTS', + OPEN_ROOMOPTIONS: 'OPEN_ROOMOPTIONS', }, room: { JOIN: 'JOIN', LEAVE: 'LEAVE', CREATE: 'CREATE', + CREATE_SPACE_SHORTCUT: 'CREATE_SPACE_SHORTCUT', + DELETE_SPACE_SHORTCUT: 'DELETE_SPACE_SHORTCUT', error: { CREATE: 'ERROR_CREATE', }, @@ -33,16 +48,18 @@ const cons = { }, events: { navigation: { - TAB_CHANGED: 'TAB_CHANGED', + TAB_SELECTED: 'TAB_SELECTED', + SPACE_SELECTED: 'SPACE_SELECTED', ROOM_SELECTED: 'ROOM_SELECTED', PEOPLE_DRAWER_TOGGLED: 'PEOPLE_DRAWER_TOGGLED', INVITE_LIST_OPENED: 'INVITE_LIST_OPENED', - PUBLIC_CHANNELS_OPENED: 'PUBLIC_CHANNELS_OPENED', - CREATE_CHANNEL_OPENED: 'CREATE_CHANNEL_OPENED', + PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED', + CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED', INVITE_USER_OPENED: 'INVITE_USER_OPENED', SETTINGS_OPENED: 'SETTINGS_OPENED', EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED', READRECEIPTS_OPENED: 'READRECEIPTS_OPENED', + ROOMOPTIONS_OPENED: 'ROOMOPTIONS_OPENED', }, roomList: { ROOMLIST_UPDATED: 'ROOMLIST_UPDATED', @@ -50,8 +67,11 @@ const cons = { ROOM_JOINED: 'ROOM_JOINED', ROOM_LEAVED: 'ROOM_LEAVED', ROOM_CREATED: 'ROOM_CREATED', - MY_RECEIPT_ARRIVED: 'MY_RECEIPT_ARRIVED', - EVENT_ARRIVED: 'EVENT_ARRIVED', + SPACE_SHORTCUT_UPDATED: 'SPACE_SHORTCUT_UPDATED', + }, + notifications: { + NOTI_CHANGED: 'NOTI_CHANGED', + FULL_READ: 'FULL_READ', }, roomTimeline: { EVENT: 'EVENT', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index 1aa6c0c..d7dabd7 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -6,29 +6,52 @@ class Navigation extends EventEmitter { constructor() { super(); - this.activeTab = 'home'; - this.activeRoomId = null; + this.selectedTab = cons.tabs.HOME; + this.selectedSpaceId = null; + this.selectedSpacePath = [cons.tabs.HOME]; + + this.selectedRoomId = null; this.isPeopleDrawerVisible = true; } - getActiveTab() { - return this.activeTab; - } - - getActiveRoomId() { - return this.activeRoomId; + _setSpacePath(roomId) { + if (roomId === null || roomId === cons.tabs.HOME) { + this.selectedSpacePath = [cons.tabs.HOME]; + return; + } + if (this.selectedSpacePath.includes(roomId)) { + const spIndex = this.selectedSpacePath.indexOf(roomId); + this.selectedSpacePath = this.selectedSpacePath.slice(0, spIndex + 1); + return; + } + this.selectedSpacePath.push(roomId); } navigate(action) { const actions = { - [cons.actions.navigation.CHANGE_TAB]: () => { - this.activeTab = action.tabId; - this.emit(cons.events.navigation.TAB_CHANGED, this.activeTab); + [cons.actions.navigation.SELECT_TAB]: () => { + this.selectedTab = action.tabId; + if (this.selectedTab !== cons.tabs.DIRECTS) { + if (this.selectedTab === cons.tabs.HOME) { + this.selectedSpacePath = [cons.tabs.HOME]; + this.selectedSpaceId = null; + } else { + this.selectedSpacePath = [this.selectedTab]; + this.selectedSpaceId = this.selectedTab; + } + this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId); + } else this.selectedSpaceId = null; + this.emit(cons.events.navigation.TAB_SELECTED, this.selectedTab); + }, + [cons.actions.navigation.SELECT_SPACE]: () => { + this._setSpacePath(action.roomId); + this.selectedSpaceId = action.roomId; + this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId); }, [cons.actions.navigation.SELECT_ROOM]: () => { - const prevActiveRoomId = this.activeRoomId; - this.activeRoomId = action.roomId; - this.emit(cons.events.navigation.ROOM_SELECTED, this.activeRoomId, prevActiveRoomId); + const prevSelectedRoomId = this.selectedRoomId; + this.selectedRoomId = action.roomId; + this.emit(cons.events.navigation.ROOM_SELECTED, this.selectedRoomId, prevSelectedRoomId); }, [cons.actions.navigation.TOGGLE_PEOPLE_DRAWER]: () => { this.isPeopleDrawerVisible = !this.isPeopleDrawerVisible; @@ -37,11 +60,11 @@ class Navigation extends EventEmitter { [cons.actions.navigation.OPEN_INVITE_LIST]: () => { this.emit(cons.events.navigation.INVITE_LIST_OPENED); }, - [cons.actions.navigation.OPEN_PUBLIC_CHANNELS]: () => { - this.emit(cons.events.navigation.PUBLIC_CHANNELS_OPENED, action.searchTerm); + [cons.actions.navigation.OPEN_PUBLIC_ROOMS]: () => { + this.emit(cons.events.navigation.PUBLIC_ROOMS_OPENED, action.searchTerm); }, - [cons.actions.navigation.OPEN_CREATE_CHANNEL]: () => { - this.emit(cons.events.navigation.CREATE_CHANNEL_OPENED); + [cons.actions.navigation.OPEN_CREATE_ROOM]: () => { + this.emit(cons.events.navigation.CREATE_ROOM_OPENED); }, [cons.actions.navigation.OPEN_INVITE_USER]: () => { this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm); @@ -62,6 +85,13 @@ class Navigation extends EventEmitter { action.eventId, ); }, + [cons.actions.navigation.OPEN_ROOMOPTIONS]: () => { + this.emit( + cons.events.navigation.ROOMOPTIONS_OPENED, + action.cords, + action.roomId, + ); + }, }; actions[action.type]?.(); } diff --git a/src/index.scss b/src/index.scss index a3819a9..77261e5 100644 --- a/src/index.scss +++ b/src/index.scss @@ -4,7 +4,9 @@ /* background color | --bg-[background type]: value */ --bg-surface: #FFFFFF; + --bg-surface-transparent: #FFFFFF00; --bg-surface-low: #F6F6F6; + --bg-surface-low-transparent: #F6F6F600; --bg-surface-hover: rgba(0, 0, 0, 3%); --bg-surface-active: rgba(0, 0, 0, 5%); --bg-surface-border: rgba(0, 0, 0, 6%); @@ -30,6 +32,7 @@ --bg-danger-border: rgba(240, 71, 71, 20%); --bg-tooltip: #353535; + --bg-badge: #989898; /* text color | --tc-[background type]-[priority]: value */ --tc-surface-high: #000000; @@ -55,6 +58,7 @@ --tc-code: #e62498; --tc-tooltip: white; + --tc-badge: white; /* system icons | --ic-[background type]-[priority]: value */ @@ -155,14 +159,18 @@ .silver-theme { /* background color | --bg-[background type]: value */ --bg-surface: hsl(0, 0%, 95%); + --bg-surface-transparent: hsla(0, 0%, 95%, 0); --bg-surface-low: hsl(0, 0%, 91%); + --bg-surface-low-transparent: hsla(0, 0%, 91%, 0); } .dark-theme, .butter-theme { /* background color | --bg-[background type]: value */ --bg-surface: hsl(208, 8%, 20%); + --bg-surface-transparent: hsla(208, 8%, 20%, 0); --bg-surface-low: hsl(208, 8%, 16%); + --bg-surface-low-transparent: hsla(208, 8%, 16%, 0); --bg-surface-hover: rgba(255, 255, 255, 3%); --bg-surface-active: rgba(255, 255, 255, 5%); --bg-surface-border: rgba(0, 0, 0, 20%); @@ -173,6 +181,7 @@ --bg-primary-border: rgba(59, 119, 191, 38%); --bg-tooltip: #000; + --bg-badge: hsl(0, 0%, 75%); /* text color | --tc-[background type]-[priority]: value */ --tc-surface-high: rgba(255, 255, 255, 94%); @@ -184,6 +193,7 @@ --tc-primary-low: rgba(255, 255, 255, 0.4); --tc-code: #e565b1; + --tc-badge: black; /* system icons | --ic-[background type]-[priority]: value */ --ic-surface-normal: rgba(255, 255, 255, 68%); @@ -206,7 +216,11 @@ .butter-theme { /* background color | --bg-[background type]: value */ --bg-surface: hsl(64, 6%, 14%); + --bg-surface-transparent: hsla(64, 6%, 14%, 0); --bg-surface-low: hsl(64, 6%, 10%); + --bg-surface-low-transparent: hsla(64, 6%, 14%, 0); + + --bg-badge: #c4c1ab; /* text color | --tc-[background type]-[priority]: value */ diff --git a/src/util/Postie.js b/src/util/Postie.js index c3bf806..668408d 100644 --- a/src/util/Postie.js +++ b/src/util/Postie.js @@ -30,8 +30,8 @@ class Postie { } hasTopicAndSubscriber(topic, address) { - return (this.isTopicExist(topic)) - ? this.isSubscriberExist(topic, address) + return (this.hasTopic(topic)) + ? this.hasSubscriber(topic, address) : false; } diff --git a/src/util/common.js b/src/util/common.js index 78bb349..f434c5b 100644 --- a/src/util/common.js +++ b/src/util/common.js @@ -19,3 +19,17 @@ export function isNotInSameDay(dt2, dt1) { || dt2.getYear() !== dt1.getYear() ); } + +export function getEventCords(ev) { + const boxInfo = ev.target.getBoundingClientRect(); + return { + x: boxInfo.x, + y: boxInfo.y, + detail: ev.detail, + }; +} + +export function abbreviateNumber(number) { + if (number > 99) return '99+'; + return number; +}