Merge branch 'dev' into user-timezone

This commit is contained in:
Krishan 2022-09-07 13:51:20 +05:30 committed by GitHub
commit ce5f0dc209
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 3486 additions and 5847 deletions

15
.github/renovate.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"labels": [ "Dependencies" ],
"packageRules": [
{
"matchUpdateTypes": [ "lockFileMaintenance" ]
}
],
"lockFileMaintenance": { "enabled": true },
"dependencyDashboard": true,
"dependencyDashboardApproval": true
}

View file

@ -6,6 +6,7 @@ on:
jobs: jobs:
build-pull-request: build-pull-request:
name: 'Build pull request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
PR_NUMBER: ${{github.event.number}} PR_NUMBER: ${{github.event.number}}
@ -16,23 +17,22 @@ jobs:
uses: actions/setup-node@v3.4.1 uses: actions/setup-node@v3.4.1
with: with:
node-version: 17.9.0 node-version: 17.9.0
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build app - name: Build app
run: npm ci && npm run build run: npm run build
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: previewbuild name: preview
path: dist path: dist
retention-days: 1 retention-days: 1
- name: Get PR info - name: Save pr number
uses: actions/github-script@v6.2.0 run: echo ${PR_NUMBER} > ./pr.txt
with: - name: Upload pr number
script: |
var fs = require('fs');
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
- name: Upload PR Info
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: pr.json name: pr
path: pr.json path: ./pr.txt
retention-days: 1 retention-days: 1

View file

@ -1,68 +1,40 @@
name: Upload Preview Build to Netlify name: Deploy PR to Netlify
on: on:
workflow_run: workflow_run:
workflows: ["Build pull request"] workflows: ["Build pull request"]
types: types: [completed]
- completed
jobs: jobs:
get-build-and-deploy: deploy-pull-request:
name: 'Deploy pull request'
runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
pull-requests: write pull-requests: write
runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: >
${{ github.event.workflow_run.conclusion == 'success' }}
steps: steps:
# There's a 'download artifact' action but it hasn't been updated for the - name: Download pr number
# workflow_run action (https://github.com/actions/download-artifact/issues/60) uses: dawidd6/action-download-artifact@7847792dd435a50521b8e3bd3576dae7459d1fa8
# so instead we get this mess: with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
name: pr
- name: Output pr number
id: pr
run: echo "::set-output name=id::$(<pr.txt)"
- name: Download artifact - name: Download artifact
uses: actions/github-script@v6.2.0 uses: dawidd6/action-download-artifact@7847792dd435a50521b8e3bd3576dae7459d1fa8
with: with:
script: | workflow: ${{ github.event.workflow.id }}
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ run_id: ${{ github.event.workflow_run.id }}
owner: context.repo.owner, name: preview
repo: context.repo.repo, path: dist
run_id: ${{github.event.workflow_run.id }},
});
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "previewbuild"
})[0];
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
var fs = require('fs');
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr.json"
})[0];
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: prInfoArtifact.id,
archive_format: 'zip',
});
var fs = require('fs');
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
- name: Extract Artifacts
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
- name: Read PR Info
id: readctx
uses: actions/github-script@v6.2.0
with:
script: |
var fs = require('fs');
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
console.log(`::set-output name=prnumber::${pr.number}`);
- name: Deploy to Netlify - name: Deploy to Netlify
id: netlify id: netlify
uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99 uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
with: with:
publish-dir: dist publish-dir: dist
deploy-message: "Deploy from GitHub Actions" deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
alias: ${{ steps.pr.outputs.id }}
# These don't work because we're in workflow_run # These don't work because we're in workflow_run
enable-pull-request-comment: false enable-pull-request-comment: false
enable-commit-comment: false enable-commit-comment: false
@ -75,7 +47,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
pull-request-number: ${{ steps.readctx.outputs.prnumber }} pull-request-number: ${{ steps.pr.outputs.id }}
description-message: | description-message: |
Preview: ${{ steps.netlify.outputs.deploy-url }} Preview: ${{ steps.netlify.outputs.deploy-url }}
⚠️ Exercise caution. Use test accounts. ⚠️ ⚠️ Exercise caution. Use test accounts. ⚠️

26
.github/workflows/lockfile.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: NPM Lockfile Changes
on:
pull_request:
paths:
- 'package-lock.json'
jobs:
lockfile_changes:
runs-on: ubuntu-latest
# Permission overwrite is required for Dependabot PRs, see "Common issues" below.
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v3.0.2
- name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Optional inputs, can be deleted safely if you are happy with default values.
collapsibleThreshold: 25
failOnDowngrade: false
path: package-lock.json
updateComment: true

View file

@ -7,10 +7,8 @@ on:
jobs: jobs:
deploy-to-netlify: deploy-to-netlify:
name: 'Deploy' name: 'Deploy to Netlify'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
@ -18,12 +16,22 @@ jobs:
uses: actions/setup-node@v3.4.1 uses: actions/setup-node@v3.4.1
with: with:
node-version: 17.9.0 node-version: 17.9.0
- name: Build and deploy to Netlify cache: 'npm'
uses: jsmrcaga/action-netlify-deploy@53de32e559b0b3833615b9788c7a090cd2fddb03 - name: Install dependencies
run: npm ci
- name: Build app
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
with: with:
install_command: "npm ci" publish-dir: dist
deploy-message: "Dev deploy ${{ github.sha }}"
enable-commit-comment: false
github-token: ${{ secrets.GITHUB_TOKEN }}
production-deploy: true
github-deployment-environment: nightly
github-deployment-description: 'Nightly deployment on each commit to dev branch'
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE2_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE2_ID }}
BUILD_DIRECTORY: "dist" timeout-minutes: 1
NETLIFY_DEPLOY_MESSAGE: "Dev deploy v${{ github.ref }}"
NETLIFY_DEPLOY_TO_PROD: true

View file

@ -5,8 +5,8 @@ on:
types: [published] types: [published]
jobs: jobs:
create-release-tar: deploy-and-tarball:
name: 'Create release tar' name: 'Netlify deploy and tarball'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@ -15,10 +15,25 @@ jobs:
uses: actions/setup-node@v3.4.1 uses: actions/setup-node@v3.4.1
with: with:
node-version: 17.9.0 node-version: 17.9.0
- name: Build cache: 'npm'
run: | - name: Install dependencies
npm ci run: npm ci
npm run build - name: Build app
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
with:
publish-dir: dist
deploy-message: "Prod deploy ${{ github.ref_name }}"
enable-commit-comment: false
github-token: ${{ secrets.GITHUB_TOKEN }}
production-deploy: true
github-deployment-environment: stable
github-deployment-description: 'Stable deployment on each release'
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
- name: Get version from tag - name: Get version from tag
id: vars id: vars
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
@ -41,29 +56,7 @@ jobs:
cinny-${{ steps.vars.outputs.tag }}.tar.gz cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
deploy-to-netlify: publish-image:
name: 'Deploy to Netlify'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
- name: Setup node
uses: actions/setup-node@v3.4.1
with:
node-version: 17.9.0
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@53de32e559b0b3833615b9788c7a090cd2fddb03
with:
install_command: "npm ci"
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
push-to-dockerhub:
name: Push Docker image to Docker Hub, ghcr name: Push Docker image to Docker Hub, ghcr
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:

2
.npmrc
View file

@ -1 +1,3 @@
legacy-peer-deps=true legacy-peer-deps=true
save-exact=true
@matrix-org:registry=https://gitlab.matrix.org/api/v4/projects/27/packages/npm/

BIN
olm.wasm

Binary file not shown.

8075
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,71 +15,69 @@
"author": "Ajay Bura", "author": "Ajay Bura",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/inter": "^4.5.12", "@fontsource/inter": "4.5.12",
"@fontsource/roboto": "^4.5.8", "@fontsource/roboto": "4.5.8",
"@khanacademy/simple-markdown": "^0.8.3", "@khanacademy/simple-markdown": "0.8.3",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz", "@matrix-org/olm": "3.2.12",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "4.2.6",
"babel-polyfill": "^6.26.0", "babel-polyfill": "6.26.0",
"blurhash": "^1.1.5", "blurhash": "1.1.5",
"browser-encrypt-attachment": "^0.3.0", "browser-encrypt-attachment": "0.3.0",
"dateformat": "^5.0.3", "dateformat": "5.0.3",
"emojibase-data": "^7.0.1", "emojibase-data": "7.0.1",
"file-saver": "^2.0.5", "file-saver": "2.0.5",
"flux": "^4.0.3", "flux": "4.0.3",
"formik": "^2.2.9", "formik": "2.2.9",
"html-react-parser": "^3.0.4", "html-react-parser": "3.0.4",
"katex": "^0.16.2", "katex": "0.16.2",
"linkify-html": "^4.0.0-beta.5", "linkify-html": "4.0.0-beta.5",
"linkifyjs": "^4.0.0-beta.5", "linkifyjs": "4.0.0-beta.5",
"matrix-js-sdk": "^19.4.0", "matrix-js-sdk": "19.4.0",
"prop-types": "^15.8.1", "prop-types": "15.8.1",
"react": "^17.0.2", "react": "17.0.2",
"react-autosize-textarea": "^7.1.0", "react-autosize-textarea": "7.1.0",
"react-blurhash": "^0.1.3", "react-blurhash": "0.1.3",
"react-dnd": "^15.1.2", "react-dnd": "15.1.2",
"react-dnd-html5-backend": "^15.1.3", "react-dnd-html5-backend": "15.1.3",
"react-dom": "^17.0.2", "react-dom": "17.0.2",
"react-google-recaptcha": "^2.1.0", "react-google-recaptcha": "2.1.0",
"react-modal": "^3.15.1", "react-modal": "3.15.1",
"sanitize-html": "^2.7.1", "sanitize-html": "2.7.1",
"tippy.js": "^6.3.7", "tippy.js": "6.3.7",
"twemoji": "^14.0.2" "twemoji": "14.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.13", "@babel/core": "7.18.13",
"@babel/preset-env": "^7.18.10", "@babel/preset-env": "7.18.10",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "7.18.6",
"assert": "^2.0.0", "assert": "2.0.0",
"babel-loader": "^8.2.5", "babel-loader": "8.2.5",
"browserify-fs": "^1.0.0", "browserify-fs": "1.0.0",
"buffer": "^6.0.3", "buffer": "6.0.3",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "11.0.0",
"crypto-browserify": "^3.12.0", "crypto-browserify": "3.12.0",
"css-loader": "^6.7.1", "css-loader": "6.7.1",
"css-minimizer-webpack-plugin": "^4.0.0", "css-minimizer-webpack-plugin": "4.0.0",
"eslint": "^8.23.0", "eslint": "8.23.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "^7.31.1", "eslint-plugin-react": "7.31.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"favicons": "^6.2.2", "html-loader": "4.1.0",
"favicons-webpack-plugin": "^5.0.2", "html-webpack-plugin": "5.3.1",
"html-loader": "^4.1.0", "mini-css-extract-plugin": "2.6.1",
"html-webpack-plugin": "^5.3.1", "path-browserify": "1.0.1",
"mini-css-extract-plugin": "^2.6.1", "sass": "1.54.5",
"path-browserify": "^1.0.1", "sass-loader": "13.0.2",
"sass": "^1.54.5", "stream-browserify": "3.0.0",
"sass-loader": "^13.0.2", "style-loader": "3.3.1",
"stream-browserify": "^3.0.0", "url": "0.11.0",
"style-loader": "^3.3.1", "util": "0.12.4",
"url": "^0.11.0", "webpack": "5.74.0",
"util": "^0.12.4", "webpack-cli": "4.10.0",
"webpack": "^5.74.0", "webpack-dev-server": "4.10.1",
"webpack-cli": "^4.10.0", "webpack-merge": "5.7.3"
"webpack-dev-server": "^4.10.1",
"webpack-merge": "^5.7.3"
} }
} }

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -15,6 +15,28 @@
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png"> <meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png">
<meta property="og:description" content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."> <meta property="og:description" content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source.">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<link id="favicon" rel="shortcut icon" href="./favicon.ico" />
<link rel="manifest" href="./manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="application-name" content="Cinny" />
<meta name="apple-mobile-web-app-title" content="Cinny" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="apple-touch-icon" sizes="57x57" href="./res/apple/apple-touch-icon-57x57.png"/>
<link rel="apple-touch-icon" sizes="60x60" href="./res/apple/apple-touch-icon-60x60.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="./res/apple/apple-touch-icon-72x72.png"/>
<link rel="apple-touch-icon" sizes="76x76" href="./res/apple/apple-touch-icon-76x76.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="./res/apple/apple-touch-icon-114x114.png"/>
<link rel="apple-touch-icon" sizes="120x120" href="./res/apple/apple-touch-icon-120x120.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="./res/apple/apple-touch-icon-144x144.png"/>
<link rel="apple-touch-icon" sizes="152x152" href="./res/apple/apple-touch-icon-152x152.png"/>
<link rel="apple-touch-icon" sizes="167x167" href="./res/apple/apple-touch-icon-167x167.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="./res/apple/apple-touch-icon-180x180.png"/>
</head> </head>
<body id="appBody"> <body id="appBody">
<div id="root"></div> <div id="root"></div>

59
public/manifest.json Normal file
View file

@ -0,0 +1,59 @@
{
"name": "Cinny",
"short_name": "Cinny",
"description": "Yet another matrix client",
"dir": "auto",
"lang": "en-US",
"display": "standalone",
"orientation": "portrait",
"start_url": "/",
"background_color": "#fff",
"theme_color": "#fff",
"icons": [
{
"src": "android-chrome-36x36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "android-chrome-48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "android-chrome-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,13 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2707_1961)">
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#45B83B"/>
</g>
<defs>
<clipPath id="clip0_2707_1961">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,13 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2707_2015)">
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#989898"/>
</g>
<defs>
<clipPath id="clip0_2707_2015">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,8 +1,4 @@
import { avatarInitials } from '../../../util/common'; import { avatarInitials, cssVar } from '../../../util/common';
function cssVar(name) {
return getComputedStyle(document.body).getPropertyValue(name);
}
// renders the avatar and returns it as an URL // renders the avatar and returns it as an URL
export default async function renderAvatar({ export default async function renderAvatar({

View file

@ -0,0 +1,22 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from 'react';
import initMatrix from '../../client/initMatrix';
export function useAccountData(eventType) {
const mx = initMatrix.matrixClient;
const [event, setEvent] = useState(mx.getAccountData(eventType));
useEffect(() => {
const handleChange = (mEvent) => {
if (mEvent.getType() !== eventType) return;
setEvent(mEvent);
};
mx.on('accountData', handleChange);
return () => {
mx.removeListener('accountData', handleChange);
};
}, [eventType]);
return event;
}

View file

@ -0,0 +1,174 @@
import React from 'react';
import initMatrix from '../../../client/initMatrix';
import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SettingTile from '../setting-tile/SettingTile';
import NotificationSelector from './NotificationSelector';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import { useAccountData } from '../../hooks/useAccountData';
export const notifType = {
ON: 'on',
OFF: 'off',
NOISY: 'noisy',
};
export const typeToLabel = {
[notifType.ON]: 'On',
[notifType.OFF]: 'Off',
[notifType.NOISY]: 'Noisy',
};
Object.freeze(notifType);
const DM = '.m.rule.room_one_to_one';
const ENC_DM = '.m.rule.encrypted_room_one_to_one';
const ROOM = '.m.rule.message';
const ENC_ROOM = '.m.rule.encrypted';
export function getActionType(rule) {
const { actions } = rule;
if (actions.find((action) => action?.set_tweak === 'sound')) return notifType.NOISY;
if (actions.find((action) => action?.set_tweak === 'highlight')) return notifType.ON;
if (actions.find((action) => action === 'dont_notify')) return notifType.OFF;
return notifType.OFF;
}
export function getTypeActions(type, highlightValue = false) {
if (type === notifType.OFF) return ['dont_notify'];
const highlight = { set_tweak: 'highlight' };
if (typeof highlightValue === 'boolean') highlight.value = highlightValue;
if (type === notifType.ON) return ['notify', highlight];
const sound = { set_tweak: 'sound', value: 'default' };
return ['notify', sound, highlight];
}
function useGlobalNotif() {
const mx = initMatrix.matrixClient;
const pushRules = useAccountData('m.push_rules')?.getContent();
const underride = pushRules?.global?.underride ?? [];
const rulesToType = {
[DM]: notifType.ON,
[ENC_DM]: notifType.ON,
[ROOM]: notifType.NOISY,
[ENC_ROOM]: notifType.NOISY,
};
const getRuleCondition = (rule) => {
const condition = [];
if (rule === DM || rule === ENC_DM) {
condition.push({ kind: 'room_member_count', is: '2' });
}
condition.push({
kind: 'event_match',
key: 'type',
pattern: [ENC_DM, ENC_ROOM].includes(rule) ? 'm.room.encrypted' : 'm.room.message',
});
return condition;
};
const setRule = (rule, type) => {
const content = pushRules ?? {};
if (!content.global) content.global = {};
if (!content.global.underride) content.global.underride = [];
const ur = content.global.underride;
let ruleContent = ur.find((action) => action?.rule_id === rule);
if (!ruleContent) {
ruleContent = {
conditions: getRuleCondition(type),
actions: [],
rule_id: rule,
default: true,
enabled: true,
};
ur.push(ruleContent);
}
ruleContent.actions = getTypeActions(type);
mx.setAccountData('m.push_rules', content);
};
const dmRule = underride.find((rule) => rule.rule_id === DM);
const encDmRule = underride.find((rule) => rule.rule_id === ENC_DM);
const roomRule = underride.find((rule) => rule.rule_id === ROOM);
const encRoomRule = underride.find((rule) => rule.rule_id === ENC_ROOM);
if (dmRule) rulesToType[DM] = getActionType(dmRule);
if (encDmRule) rulesToType[ENC_DM] = getActionType(encDmRule);
if (roomRule) rulesToType[ROOM] = getActionType(roomRule);
if (encRoomRule) rulesToType[ENC_ROOM] = getActionType(encRoomRule);
return [rulesToType, setRule];
}
function GlobalNotification() {
const [rulesToType, setRule] = useGlobalNotif();
const onSelect = (evt, rule) => {
openReusableContextMenu(
'bottom',
getEventCords(evt, '.btn-surface'),
(requestClose) => (
<NotificationSelector
value={rulesToType[rule]}
onSelect={(value) => {
if (rulesToType[rule] !== value) setRule(rule, value);
requestClose();
}}
/>
),
);
};
return (
<div className="global-notification">
<MenuHeader>Global Notifications</MenuHeader>
<SettingTile
title="Direct messages"
options={(
<Button onClick={(evt) => onSelect(evt, DM)} iconSrc={ChevronBottomIC}>
{ typeToLabel[rulesToType[DM]] }
</Button>
)}
content={<Text variant="b3">Default notification settings for all direct message.</Text>}
/>
<SettingTile
title="Encrypted direct messages"
options={(
<Button onClick={(evt) => onSelect(evt, ENC_DM)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ENC_DM]]}
</Button>
)}
content={<Text variant="b3">Default notification settings for all encrypted direct message.</Text>}
/>
<SettingTile
title="Rooms messages"
options={(
<Button onClick={(evt) => onSelect(evt, ROOM)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ROOM]]}
</Button>
)}
content={<Text variant="b3">Default notification settings for all room message.</Text>}
/>
<SettingTile
title="Encrypted rooms messages"
options={(
<Button onClick={(evt) => onSelect(evt, ENC_ROOM)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ENC_ROOM]]}
</Button>
)}
content={<Text variant="b3">Default notification settings for all encrypted room message.</Text>}
/>
</div>
);
}
export default GlobalNotification;

View file

@ -0,0 +1,64 @@
import React from 'react';
import './IgnoreUserList.scss';
import initMatrix from '../../../client/initMatrix';
import * as roomActions from '../../../client/action/room';
import Text from '../../atoms/text/Text';
import Chip from '../../atoms/chip/Chip';
import Input from '../../atoms/input/Input';
import Button from '../../atoms/button/Button';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SettingTile from '../setting-tile/SettingTile';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useAccountData } from '../../hooks/useAccountData';
function IgnoreUserList() {
useAccountData('m.ignored_user_list');
const ignoredUsers = initMatrix.matrixClient.getIgnoredUsers();
const handleSubmit = (evt) => {
evt.preventDefault();
const { ignoreInput } = evt.target.elements;
const value = ignoreInput.value.trim();
const userIds = value.split(' ').filter((v) => v.match(/^@\S+:\S+$/));
if (userIds.length === 0) return;
ignoreInput.value = '';
roomActions.ignore(userIds);
};
return (
<div className="ignore-user-list">
<MenuHeader>Ignored users</MenuHeader>
<SettingTile
title="Ignore user"
content={(
<div className="ignore-user-list__users">
<Text variant="b3">Ignore userId if you do not want to receive their messages or invites.</Text>
<form onSubmit={handleSubmit}>
<Input name="ignoreInput" required />
<Button variant="primary" type="submit">Ignore</Button>
</form>
{ignoredUsers.length > 0 && (
<div>
{ignoredUsers.map((uId) => (
<Chip
iconSrc={CrossIC}
key={uId}
text={uId}
iconColor={CrossIC}
onClick={() => roomActions.unignore([uId])}
/>
))}
</div>
)}
</div>
)}
/>
</div>
);
}
export default IgnoreUserList;

View file

@ -0,0 +1,17 @@
.ignore-user-list {
&__users {
& form,
& > div:last-child {
display: flex;
flex-wrap: wrap;
gap: var(--sp-tight);
}
& form {
margin: var(--sp-extra-tight) 0 var(--sp-normal);
.input-container {
flex-grow: 1;
}
}
}
}

View file

@ -0,0 +1,239 @@
import React from 'react';
import './KeywordNotification.scss';
import initMatrix from '../../../client/initMatrix';
import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import Text from '../../atoms/text/Text';
import Chip from '../../atoms/chip/Chip';
import Input from '../../atoms/input/Input';
import Button from '../../atoms/button/Button';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SettingTile from '../setting-tile/SettingTile';
import NotificationSelector from './NotificationSelector';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useAccountData } from '../../hooks/useAccountData';
import {
notifType, typeToLabel, getActionType, getTypeActions,
} from './GlobalNotification';
const DISPLAY_NAME = '.m.rule.contains_display_name';
const ROOM_PING = '.m.rule.roomnotif';
const USERNAME = '.m.rule.contains_user_name';
const KEYWORD = 'keyword';
function useKeywordNotif() {
const mx = initMatrix.matrixClient;
const pushRules = useAccountData('m.push_rules')?.getContent();
const override = pushRules?.global?.override ?? [];
const content = pushRules?.global?.content ?? [];
const rulesToType = {
[DISPLAY_NAME]: notifType.NOISY,
[ROOM_PING]: notifType.NOISY,
[USERNAME]: notifType.NOISY,
};
const setRule = (rule, type) => {
const evtContent = pushRules ?? {};
if (!evtContent.global) evtContent.global = {};
if (!evtContent.global.override) evtContent.global.override = [];
if (!evtContent.global.content) evtContent.global.content = [];
const or = evtContent.global.override;
const ct = evtContent.global.content;
if (rule === DISPLAY_NAME || rule === ROOM_PING) {
let orRule = or.find((r) => r?.rule_id === rule);
if (!orRule) {
orRule = {
conditions: [],
actions: [],
rule_id: rule,
default: true,
enabled: true,
};
or.push(orRule);
}
if (rule === DISPLAY_NAME) {
orRule.conditions = [{ kind: 'contains_display_name' }];
orRule.actions = getTypeActions(type, true);
} else {
orRule.conditions = [
{ kind: 'event_match', key: 'content.body', pattern: '@room' },
{ kind: 'sender_notification_permission', key: 'room' },
];
orRule.actions = getTypeActions(type, true);
}
} else if (rule === USERNAME) {
let usernameRule = ct.find((r) => r?.rule_id === rule);
if (!usernameRule) {
const userId = mx.getUserId();
const username = userId.match(/^@?(\S+):(\S+)$/)?.[1] ?? userId;
usernameRule = {
actions: [],
default: true,
enabled: true,
pattern: username,
rule_id: rule,
};
ct.push(usernameRule);
}
usernameRule.actions = getTypeActions(type, true);
} else {
const keyRules = ct.filter((r) => r.rule_id !== USERNAME);
keyRules.forEach((r) => {
// eslint-disable-next-line no-param-reassign
r.actions = getTypeActions(type, true);
});
}
mx.setAccountData('m.push_rules', evtContent);
};
const addKeyword = (keyword) => {
if (content.find((r) => r.rule_id === keyword)) return;
content.push({
rule_id: keyword,
pattern: keyword,
enabled: true,
default: false,
actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
});
mx.setAccountData('m.push_rules', pushRules);
};
const removeKeyword = (rule) => {
pushRules.global.content = content.filter((r) => r.rule_id !== rule.rule_id);
mx.setAccountData('m.push_rules', pushRules);
};
const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
const roomRule = override.find((rule) => rule.rule_id === ROOM_PING);
const usernameRule = content.find((rule) => rule.rule_id === USERNAME);
const keywordRule = content.find((rule) => rule.rule_id !== USERNAME);
if (dsRule) rulesToType[DISPLAY_NAME] = getActionType(dsRule);
if (roomRule) rulesToType[ROOM_PING] = getActionType(roomRule);
if (usernameRule) rulesToType[USERNAME] = getActionType(usernameRule);
if (keywordRule) rulesToType[KEYWORD] = getActionType(keywordRule);
return {
rulesToType,
pushRules,
setRule,
addKeyword,
removeKeyword,
};
}
function GlobalNotification() {
const {
rulesToType,
pushRules,
setRule,
addKeyword,
removeKeyword,
} = useKeywordNotif();
const keywordRules = pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
const onSelect = (evt, rule) => {
openReusableContextMenu(
'bottom',
getEventCords(evt, '.btn-surface'),
(requestClose) => (
<NotificationSelector
value={rulesToType[rule]}
onSelect={(value) => {
if (rulesToType[rule] !== value) setRule(rule, value);
requestClose();
}}
/>
),
);
};
const handleSubmit = (evt) => {
evt.preventDefault();
const { keywordInput } = evt.target.elements;
const value = keywordInput.value.trim();
if (value === '') return;
addKeyword(value);
keywordInput.value = '';
};
return (
<div className="keyword-notification">
<MenuHeader>Mentions & keywords</MenuHeader>
<SettingTile
title="Message containing my display name"
options={(
<Button onClick={(evt) => onSelect(evt, DISPLAY_NAME)} iconSrc={ChevronBottomIC}>
{ typeToLabel[rulesToType[DISPLAY_NAME]] }
</Button>
)}
content={<Text variant="b3">Default notification settings for all message containing your display name.</Text>}
/>
<SettingTile
title="Message containing my username"
options={(
<Button onClick={(evt) => onSelect(evt, USERNAME)} iconSrc={ChevronBottomIC}>
{ typeToLabel[rulesToType[USERNAME]] }
</Button>
)}
content={<Text variant="b3">Default notification settings for all message containing your username.</Text>}
/>
<SettingTile
title="Message containing @room"
options={(
<Button onClick={(evt) => onSelect(evt, ROOM_PING)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[ROOM_PING]]}
</Button>
)}
content={<Text variant="b3">Default notification settings for all messages containing @room.</Text>}
/>
{ rulesToType[KEYWORD] && (
<SettingTile
title="Message containing keywords"
options={(
<Button onClick={(evt) => onSelect(evt, KEYWORD)} iconSrc={ChevronBottomIC}>
{typeToLabel[rulesToType[KEYWORD]]}
</Button>
)}
content={<Text variant="b3">Default notification settings for all message containing keywords.</Text>}
/>
)}
<SettingTile
title="Keywords"
content={(
<div className="keyword-notification__keyword">
<Text variant="b3">Get notification when a message contains keyword.</Text>
<form onSubmit={handleSubmit}>
<Input name="keywordInput" required />
<Button variant="primary" type="submit">Add</Button>
</form>
{keywordRules.length > 0 && (
<div>
{keywordRules.map((rule) => (
<Chip
iconSrc={CrossIC}
key={rule.rule_id}
text={rule.pattern}
iconColor={CrossIC}
onClick={() => removeKeyword(rule)}
/>
))}
</div>
)}
</div>
)}
/>
</div>
);
}
export default GlobalNotification;

View file

@ -0,0 +1,17 @@
.keyword-notification {
&__keyword {
& form,
& > div:last-child {
display: flex;
flex-wrap: wrap;
gap: var(--sp-tight);
}
& form {
margin: var(--sp-extra-tight) 0 var(--sp-normal);
.input-container {
flex-grow: 1;
}
}
}
}

View file

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
function NotificationSelector({
value, onSelect,
}) {
return (
<div>
<MenuHeader>Notification</MenuHeader>
<MenuItem iconSrc={value === 'off' ? CheckIC : null} variant={value === 'off' ? 'positive' : 'surface'} onClick={() => onSelect('off')}>Off</MenuItem>
<MenuItem iconSrc={value === 'on' ? CheckIC : null} variant={value === 'on' ? 'positive' : 'surface'} onClick={() => onSelect('on')}>On</MenuItem>
<MenuItem iconSrc={value === 'noisy' ? CheckIC : null} variant={value === 'noisy' ? 'positive' : 'surface'} onClick={() => onSelect('noisy')}>Noisy</MenuItem>
</div>
);
}
NotificationSelector.propTypes = {
value: PropTypes.oneOf(['off', 'on', 'noisy']).isRequired,
onSelect: PropTypes.func.isRequired,
};
export default NotificationSelector;

View file

@ -38,7 +38,7 @@
@extend .cp-fx__column; @extend .cp-fx__column;
} }
&__nav-twemoji { &__nav-twemoji {
background: inherit; background-color: var(--bg-surface);
position: sticky; position: sticky;
bottom: -70%; bottom: -70%;
z-index: 999; z-index: 999;

View file

@ -56,7 +56,9 @@ function Drawer() {
useEffect(() => { useEffect(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = 0; scrollRef.current.scrollTop = 0;
}
}); });
}, [selectedTab]); }, [selectedTab]);

View file

@ -21,7 +21,7 @@ import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg
function DrawerBreadcrumb({ spaceId }) { function DrawerBreadcrumb({ spaceId }) {
const [, forceUpdate] = useState({}); const [, forceUpdate] = useState({});
const scrollRef = useRef(null); const scrollRef = useRef(null);
const { roomList, notifications } = initMatrix; const { roomList, notifications, accountData } = initMatrix;
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const spacePath = navigation.selectedSpacePath; const spacePath = navigation.selectedSpacePath;
@ -49,9 +49,9 @@ function DrawerBreadcrumb({ spaceId }) {
}, [spaceId]); }, [spaceId]);
function getHomeNotiExcept(childId) { function getHomeNotiExcept(childId) {
const orphans = roomList.getOrphans(); const orphans = roomList.getOrphans()
const childIndex = orphans.indexOf(childId); .filter((id) => (id !== childId))
if (childId !== -1) orphans.splice(childIndex, 1); .filter((id) => !accountData.spaceShortcut.has(id));
let noti = null; let noti = null;

View file

@ -16,11 +16,11 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import './ProfileEditor.scss'; import './ProfileEditor.scss';
// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
function ProfileEditor({ userId }) { function ProfileEditor({ userId }) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const user = mx.getUser(mx.getUserId()); const user = mx.getUser(mx.getUserId());
const fallbackUsername = userId.match(/^@?(\S+):(\S+)$/)[1];
const displayNameRef = useRef(null); const displayNameRef = useRef(null);
const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null); const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
@ -96,7 +96,7 @@ function ProfileEditor({ userId }) {
const renderInfo = () => ( const renderInfo = () => (
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}> <div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
<div> <div>
<Text variant="h2" primary weight="medium">{twemojify(username)}</Text> <Text variant="h2" primary weight="medium">{twemojify(username) ?? fallbackUsername}</Text>
<IconButton <IconButton
src={PencilIC} src={PencilIC}
size="extra-small" size="extra-small"
@ -111,7 +111,7 @@ function ProfileEditor({ userId }) {
return ( return (
<div className="profile-editor"> <div className="profile-editor">
<ImageUpload <ImageUpload
text={username} text={username ?? fallbackUsername}
bgColor={colorMXID(userId)} bgColor={colorMXID(userId)}
imageSrc={avatarSrc} imageSrc={avatarSrc}
onUpload={handleAvatarUpload} onUpload={handleAvatarUpload}

View file

@ -33,9 +33,10 @@ function RoomViewHeader({ roomId }) {
const [, forceUpdate] = useForceUpdate(); const [, forceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.has(roomId); const isDM = initMatrix.roomList.directs.has(roomId);
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop'); const room = mx.getRoom(roomId);
avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc; let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
const roomName = mx.getRoom(roomId).name; avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
const roomName = room.name;
const roomHeaderBtnRef = useRef(null); const roomHeaderBtnRef = useRef(null);
useEffect(() => { useEffect(() => {
@ -93,7 +94,7 @@ function RoomViewHeader({ roomId }) {
</TitleWrapper> </TitleWrapper>
<RawIcon src={ChevronBottomIC} /> <RawIcon src={ChevronBottomIC} />
</button> </button>
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} /> {mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} /> <IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} /> <IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
<IconButton <IconButton

View file

@ -168,7 +168,7 @@ function Search() {
} }
}; };
const notifs = initMatrix.notifications; const noti = initMatrix.notifications;
const renderRoomSelector = (item) => { const renderRoomSelector = (item) => {
let imageSrc = null; let imageSrc = null;
let iconSrc = null; let iconSrc = null;
@ -178,9 +178,6 @@ function Search() {
iconSrc = joinRuleToIconSrc(item.room.getJoinRule(), item.type === 'space'); iconSrc = joinRuleToIconSrc(item.room.getJoinRule(), item.type === 'space');
} }
const isUnread = notifs.hasNoti(item.roomId);
const noti = notifs.getNoti(item.roomId);
return ( return (
<RoomSelector <RoomSelector
key={item.roomId} key={item.roomId}
@ -189,9 +186,9 @@ function Search() {
roomId={item.roomId} roomId={item.roomId}
imageSrc={imageSrc} imageSrc={imageSrc}
iconSrc={iconSrc} iconSrc={iconSrc}
isUnread={isUnread} isUnread={noti.hasNoti(item.roomId)}
notificationCount={noti.total} notificationCount={noti.getTotalNoti(item.roomId)}
isAlert={noti.highlight > 0} isAlert={noti.getHighlightNoti(item.roomId) > 0}
onClick={() => openItem(item.roomId, item.type)} onClick={() => openItem(item.roomId, item.type)}
/> />
); );
@ -207,7 +204,7 @@ function Search() {
size="small" size="small"
> >
<div className="search-dialog"> <div className="search-dialog">
<form className="search-dialog__input" onSubmit={(e) => { e.preventDefault(); openFirstResult()}}> <form className="search-dialog__input" onSubmit={(e) => { e.preventDefault(); openFirstResult(); }}>
<RawIcon src={SearchIC} size="small" /> <RawIcon src={SearchIC} size="small" />
<Input <Input
onChange={handleOnChange} onChange={handleOnChange}

View file

@ -180,6 +180,7 @@ function DeviceManage() {
} }
content={( content={(
<> <>
{lastTS && (
<Text variant="b3"> <Text variant="b3">
Last activity Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}> <span style={{ color: 'var(--tc-surface-normal)' }}>
@ -187,6 +188,7 @@ function DeviceManage() {
</span> </span>
{lastIP ? ` at ${lastIP}` : ''} {lastIP ? ` at ${lastIP}` : ''}
</Text> </Text>
)}
{isCurrentDevice && ( {isCurrentDevice && (
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3"> <Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
{`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`} {`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}

View file

@ -25,6 +25,9 @@ import SettingTile from '../../molecules/setting-tile/SettingTile';
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys'; import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys'; import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack'; import { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack';
import GlobalNotification from '../../molecules/global-notification/GlobalNotification';
import KeywordNotification from '../../molecules/global-notification/KeywordNotification';
import IgnoreUserList from '../../molecules/global-notification/IgnoreUserList';
import ProfileEditor from '../profile-editor/ProfileEditor'; import ProfileEditor from '../profile-editor/ProfileEditor';
import CrossSigning from './CrossSigning'; import CrossSigning from './CrossSigning';
@ -150,6 +153,7 @@ function NotificationsSection() {
}; };
return ( return (
<>
<div className="settings-notifications"> <div className="settings-notifications">
<MenuHeader>Notification & Sound</MenuHeader> <MenuHeader>Notification & Sound</MenuHeader>
<SettingTile <SettingTile
@ -168,6 +172,10 @@ function NotificationsSection() {
content={<Text variant="b3">Play sound when new messages arrive.</Text>} content={<Text variant="b3">Play sound when new messages arrive.</Text>}
/> />
</div> </div>
<GlobalNotification />
<KeywordNotification />
<IgnoreUserList />
</>
); );
} }

View file

@ -38,6 +38,9 @@
} }
.settings-appearance__card, .settings-appearance__card,
.settings-notifications, .settings-notifications,
.global-notification,
.keyword-notification,
.ignore-user-list,
.settings-security__card, .settings-security__card,
.settings-security .device-manage, .settings-security .device-manage,
.settings-about__card, .settings-about__card,

View file

@ -67,7 +67,7 @@ class InitMatrix extends EventEmitter {
}, },
PREPARED: (prevState) => { PREPARED: (prevState) => {
console.log('PREPARED state'); console.log('PREPARED state');
console.log('previous state: ', prevState); console.log('Previous state: ', prevState);
// TODO: remove global.initMatrix at end // TODO: remove global.initMatrix at end
global.initMatrix = this; global.initMatrix = this;
if (prevState === null) { if (prevState === null) {
@ -76,6 +76,9 @@ class InitMatrix extends EventEmitter {
this.roomsInput = new RoomsInput(this.matrixClient, this.roomList); this.roomsInput = new RoomsInput(this.matrixClient, this.roomList);
this.notifications = new Notifications(this.roomList); this.notifications = new Notifications(this.roomList);
this.emit('init_loading_finished'); this.emit('init_loading_finished');
this.notifications._initNoti();
} else {
this.notifications._initNoti();
} }
}, },
RECONNECTING: () => { RECONNECTING: () => {

View file

@ -5,6 +5,11 @@ import { selectRoom } from '../action/navigation';
import cons from './cons'; import cons from './cons';
import navigation from './navigation'; import navigation from './navigation';
import settings from './settings'; import settings from './settings';
import { setFavicon } from '../../util/common';
import LogoSVG from '../../../public/res/svg/cinny.svg';
import LogoUnreadSVG from '../../../public/res/svg/cinny-unread.svg';
import LogoHighlightSVG from '../../../public/res/svg/cinny-highlight.svg';
function isNotifEvent(mEvent) { function isNotifEvent(mEvent) {
const eType = mEvent.getType(); const eType = mEvent.getType();
@ -32,22 +37,24 @@ class Notifications extends EventEmitter {
constructor(roomList) { constructor(roomList) {
super(); super();
this.initialized = false;
this.favicon = LogoSVG;
this.matrixClient = roomList.matrixClient; this.matrixClient = roomList.matrixClient;
this.roomList = roomList; this.roomList = roomList;
this.roomIdToNoti = new Map(); this.roomIdToNoti = new Map();
this._initNoti(); // this._initNoti();
this._listenEvents(); this._listenEvents();
// Ask for permission by default after loading // Ask for permission by default after loading
window.Notification?.requestPermission(); window.Notification?.requestPermission();
// TODO:
window.notifications = this;
} }
_initNoti() { async _initNoti() {
this.initialized = false;
this.roomIdToNoti = new Map();
const addNoti = (roomId) => { const addNoti = (roomId) => {
const room = this.matrixClient.getRoom(roomId); const room = this.matrixClient.getRoom(roomId);
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return; if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return;
@ -59,6 +66,9 @@ class Notifications extends EventEmitter {
}; };
[...this.roomList.rooms].forEach(addNoti); [...this.roomList.rooms].forEach(addNoti);
[...this.roomList.directs].forEach(addNoti); [...this.roomList.directs].forEach(addNoti);
this.initialized = true;
this._updateFavicon();
} }
doesRoomHaveUnread(room) { doesRoomHaveUnread(room) {
@ -129,6 +139,30 @@ class Notifications extends EventEmitter {
} }
} }
async _updateFavicon() {
if (!this.initialized) return;
let unread = false;
let highlight = false;
[...this.roomIdToNoti.values()].find((noti) => {
if (!unread) {
unread = noti.total > 0 || noti.highlight > 0;
}
highlight = noti.highlight > 0;
if (unread && highlight) return true;
return false;
});
let newFavicon = LogoSVG;
if (unread && !highlight) {
newFavicon = LogoUnreadSVG;
}
if (unread && highlight) {
newFavicon = LogoHighlightSVG;
}
if (newFavicon === this.favicon) return;
this.favicon = newFavicon;
setFavicon(this.favicon);
}
_setNoti(roomId, total, highlight) { _setNoti(roomId, total, highlight) {
const addNoti = (id, t, h, fromId) => { const addNoti = (id, t, h, fromId) => {
const prevTotal = this.roomIdToNoti.get(id)?.total ?? null; const prevTotal = this.roomIdToNoti.get(id)?.total ?? null;
@ -146,7 +180,7 @@ class Notifications extends EventEmitter {
}; };
const noti = this.getNoti(roomId); const noti = this.getNoti(roomId);
const addT = total - noti.total; const addT = (highlight > total ? highlight : total) - noti.total;
const addH = highlight - noti.highlight; const addH = highlight - noti.highlight;
if (addT < 0 || addH < 0) return; if (addT < 0 || addH < 0) return;
@ -155,6 +189,7 @@ class Notifications extends EventEmitter {
allParentSpaces.forEach((spaceId) => { allParentSpaces.forEach((spaceId) => {
addNoti(spaceId, addT, addH, roomId); addNoti(spaceId, addT, addH, roomId);
}); });
this._updateFavicon();
} }
_deleteNoti(roomId, total, highlight) { _deleteNoti(roomId, total, highlight) {
@ -187,6 +222,7 @@ class Notifications extends EventEmitter {
allParentSpaces.forEach((spaceId) => { allParentSpaces.forEach((spaceId) => {
removeNoti(spaceId, total, highlight, roomId); removeNoti(spaceId, total, highlight, roomId);
}); });
this._updateFavicon();
} }
async _displayPopupNoti(mEvent, room) { async _displayPopupNoti(mEvent, room) {

View file

@ -115,6 +115,16 @@ export function avatarInitials(text) {
return [...text][0]; return [...text][0];
} }
export function cssVar(name) {
return getComputedStyle(document.body).getPropertyValue(name);
}
export function setFavicon(url) {
const favicon = document.querySelector('#favicon');
if (!favicon) return;
favicon.setAttribute('href', url);
}
export function copyToClipboard(text) { export function copyToClipboard(text) {
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);

View file

@ -26,12 +26,14 @@ export const ALLOWED_BLOB_MIMETYPES = [
]; ];
export function getBlobSafeMimeType(mimetype) { export function getBlobSafeMimeType(mimetype) {
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { if (typeof mimetype !== 'string') return 'application/octet-stream';
const [type] = mimetype.split(';');
if (!ALLOWED_BLOB_MIMETYPES.includes(type)) {
return 'application/octet-stream'; return 'application/octet-stream';
} }
// Required for Chromium browsers // Required for Chromium browsers
if (mimetype === 'video/quicktime') { if (type === 'video/quicktime') {
return 'video/mp4'; return 'video/mp4';
} }
return mimetype; return type;
} }

View file

@ -1,5 +1,4 @@
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin"); const CopyPlugin = require("copy-webpack-plugin");
const webpack = require('webpack'); const webpack = require('webpack');
@ -52,27 +51,12 @@ module.exports = {
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }), new HtmlWebpackPlugin({ template: './public/index.html' }),
new FaviconsWebpackPlugin({
logo: './public/res/svg/cinny.svg',
mode: 'webapp',
devMode: 'light',
favicons: {
appName: 'Cinny',
appDescription: 'Yet another matrix client',
developerName: 'Ajay Bura',
developerURL: 'https://github.com/ajbura',
icons: {
coast: false,
yandex: false,
appleStartup: false,
}
}
}),
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
{ from: 'olm.wasm' }, { from: 'node_modules/@matrix-org/olm/olm.wasm' },
{ from: '_redirects' }, { from: '_redirects' },
{ from: 'config.json' }, { from: 'config.json' },
{ from: 'public/res/android'}
], ],
}), }),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({