Compare commits

...
35 Commits
Author SHA1 Message Date
Ajay Bura 8a38ec8fc3 Add reusable dialog
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 19:32:05 +05:30
Ajay Bura 9cb13a91cd Fix incorrect custom html crashing app (#429)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-25 10:37:35 +05:30
Ajay Bura 30f8930773 Fix style and string
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-24 18:47:53 +05:30
Krishan 05eaa8d3e0 General fix and consistency changes (#428) 2022-03-23 19:40:39 +05:30
dependabot[bot] a32ffdf6d4 Bump node-forge from 1.2.1 to 1.3.0 (#416)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.2.1...v1.3.0)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-23 18:46:35 +05:30
Ajay Bura 0f97de1b09 Fix can't open spaces from public room list (#419)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-23 18:45:13 +05:30
Ajay Bura e8d6ccec9a Fix crash on load and room creation (#418)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-23 18:44:53 +05:30
Ajay Bura 005434f79b Add support for managing sessions (#415)
* Allow node type prop in setting tile

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Update popup window max height

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add device management setting

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add password based login

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* truncate long list of verified devices

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-23 18:44:38 +05:30
Krishan fe997d8b01 Fix loading on older browsers (#424) 2022-03-23 18:33:19 +05:30
dependabot[bot] 7291932a0b Bump @babel/core from 7.17.7 to 7.17.8 (#407)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.7 to 7.17.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.8/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-22 19:03:38 +05:30
dependabot[bot] 49ade03a9a Bump html-react-parser from 1.4.8 to 1.4.9 (#408)
Bumps [html-react-parser](https://github.com/remarkablemark/html-react-parser) from 1.4.8 to 1.4.9.
- [Release notes](https://github.com/remarkablemark/html-react-parser/releases)
- [Changelog](https://github.com/remarkablemark/html-react-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/remarkablemark/html-react-parser/compare/v1.4.8...v1.4.9)

---
updated-dependencies:
- dependency-name: html-react-parser
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-22 19:03:15 +05:30
dependabot[bot] dd6dbd25da Bump node from 17.7.1-alpine3.15 to 17.7.2-alpine3.15 (#410)
Bumps node from 17.7.1-alpine3.15 to 17.7.2-alpine3.15.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-22 19:02:26 +05:30
Ajay Bura 50bf90fada Redesign app/user settings (#404)
* Redesign app settings

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Redesign user profile in settings

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Update string

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix bug

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-21 09:46:11 +05:30
Ajay Bura abb81b6390 Remove opacity from jump to unread and latest btn (#402)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 09:19:20 +05:30
Ajay Bura 36da3d3cba Add sound on incoming invites (#400)
* Add invite sound

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add credits

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Change invite sound to google material
2022-03-19 18:55:57 +05:30
Ajay Bura ae71a99aa4 Sort search results by activity (#399)
* Sort search result by activity

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Optimize generateResults function codes

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-19 18:55:38 +05:30
Ajay Bura a7034d6351 Improve jump to bottom btn
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 11:37:31 +05:30
Ajay Bura 6010b4c252 Fix cyclic dependency in initMatrix
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 09:22:16 +05:30
Ajay Bura 8330f4fba9 Improve jump to unread button (#396)
* Improve jump to unread button

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Remove unused cod

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix mark as read not hidding jump to unread btn

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add notification mark as read action

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add esc as hotkey to mark room as read

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add message icons

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Change jump to unread icon

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 09:09:14 +05:30
ginnyTheCat dc6e153b92 Notification sounds (#367)
* Basic notification sound support

* Add settings option for notification sounds

* Allow sound without desktop notifications
2022-03-18 09:07:11 +05:30
Nitan Alexandru Marcel a2655ee6a5 Fix loading on older browsers (#397) 2022-03-18 09:06:48 +05:30
Ajay Bura 13248962af Update recent clock icon
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 17:35:50 +05:30
ginnyTheCatandKrishan b698982186 Add recently used section to emoji board (#373)
* Add recent section to emoji board

* Add section to emoji board sidebar

* Add emoji limit like element web has

* Ignore custom emojis

* Filter out invalid emojis

* Update heart icon with clock

Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
2022-03-17 17:19:14 +05:30
Ajay Bura 5a299b21c5 Sort direct messages by activity (#393)
* Add sort util

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Use sort util for members

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Sort dms by activity

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Sort dms activily

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Chanege roomIdByLastActive func name

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 16:55:16 +05:30
Krishan bb90f11ec8 Formify issue templates and improve existing (#395)
* Update bug_report.md

* Rename bug_report.md to bug_report.yml

* Update and rename feature_request.md to feature_request.yml

* Create config.yml

* Update PULL_REQUEST_TEMPLATE.md
2022-03-17 16:47:42 +05:30
Ajay Bura 12369ba2ec v1.8.2
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 16:44:50 +05:30
Ajay Bura f5720bde14 Fix new message not appearing (#391)
* Fix new message no appearing

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix room not marking as read

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix room automatically gets mark as read

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix sending wrong read recipt

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix sending message not mark as read

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 08:16:49 +05:30
Krishan 0d59a4de48 v1.8.1 (#390) 2022-03-15 18:15:08 +05:30
dependabot[bot] 73c74d0477 Bump twemoji from 13.1.0 to 14.0.1 (#383)
Bumps [twemoji](https://github.com/twitter/twemoji) from 13.1.0 to 14.0.1.
- [Release notes](https://github.com/twitter/twemoji/releases)
- [Commits](https://github.com/twitter/twemoji/commits)

---
updated-dependencies:
- dependency-name: twemoji
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-15 17:50:47 +05:30
Ajay Bura 70ffd7ded8 Fix muted room show unread indicator (#386)
* Move getNotifType function

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix bug in getNotiType

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add isMuted prop in room selector

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix muted room show unread indicator

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix muted room notification visible in space

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix space shows muted room notification on load

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Toggle room mute when changed from other client

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-15 17:21:36 +05:30
Clament John 92a3a8d6fa Fix view source shows original event for an edited message (#377)
Signed-off-by: Clament John <cj@hackerlab.in>

fixes #376

When we click view source for an edited message we were showing
the original event (the unedited event) instead of the latest
edited event.
2022-03-15 17:20:43 +05:30
Krishan 6e9cd02b2b Fix workflow name (#389) 2022-03-15 17:19:18 +05:30
dependabot[bot] d0f90af251 Bump docker/build-push-action from 2.9.0 to 2.10.0 (#388)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2.9.0...v2.10.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-15 17:16:58 +05:30
dependabot[bot] c53eb27c6f Bump node from 17.6.0-alpine3.15 to 17.7.1-alpine3.15 (#382)
Bumps node from 17.6.0-alpine3.15 to 17.7.1-alpine3.15.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-15 17:16:18 +05:30
Krishan 38773e89ff Simplify GitHub actions (#387)
* Simplify production build actions 

This merges both the netlify-prod and docker action and also automatically add tarball to releases.

* Delete docker.yaml

* Delete netlify-prod.yaml

* Cosmetic changes and add dockerhub check

* Cosmetic changes

* Fix check runs on Tuesdays only
2022-03-15 17:04:14 +05:30
80 changed files with 1676 additions and 943 deletions

No files matched your search

-33
View File
@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
#### Describe the bug
A clear and concise description of what the bug is.
#### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
#### Expected behavior
A clear and concise description of what you expected to happen.
#### Screenshots
If applicable, add screenshots to help explain your problem.
#### Desktop (please complete the following information):
- OS: [e.g. Windows, MacOS]
- Browser: [e.g. chrome, firefox]
- Version: [e.g. 3.22]
- Matrix homeserver: [e.g. matrix.org]
#### Additional context
Add any other context about the problem here.
+58
View File
@@ -0,0 +1,58 @@
name: 🐞 Bug Report
description: Report a bug
labels: 'type: bug'
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Browser: [e.g. chrome 99.5, firefox 97.2]
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
4. Matrix homeserver: [e.g. matrix.org]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: 💬 Matrix Chat
url: https://matrix.to/#/#cinny:matrix.org
about: Ask questions and talk to other Cinny users and the maintainers
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
#### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
#### Additional context
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,34 @@
name: 💡 Feature Request
description: Suggest an idea
labels: 'type: feature'
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+2 -4
View File
@@ -1,15 +1,13 @@
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
Fixes #
#### Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+16 -10
View File
@@ -1,22 +1,28 @@
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule: {interval: weekly}
reviewers: [ajbura]
assignees: [ajbura]
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
- package-ecosystem: docker
directory: /
schedule: {interval: weekly}
reviewers: [ajbura]
assignees: [ajbura]
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
- package-ecosystem: npm
directory: /
schedule: {interval: weekly}
reviewers: [ajbura]
assignees: [ajbura]
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
+33 -26
View File
@@ -1,32 +1,39 @@
name: 'Build PR'
name: 'Build pull request'
on:
pull_request:
types: ['opened', 'synchronize']
jobs:
build:
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{github.event.number}}
steps:
- uses: actions/checkout@v3.0.0
- name: Build
run: npm ci && npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3.0.0
with:
name: previewbuild
path: dist
retention-days: 1
- uses: actions/github-script@v6.0.0
with:
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.0.0
with:
name: pr.json
path: pr.json
retention-days: 1
build-pull-request:
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.0
- name: Build app
run: npm ci && npm run build
- name: Upload artifact
uses: actions/upload-artifact@v3.0.0
with:
name: previewbuild
path: dist
retention-days: 1
- name: Get PR info
uses: actions/github-script@v6.0.0
with:
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.0.0
with:
name: pr.json
path: pr.json
retention-days: 1
- name: Build Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false
+75 -75
View File
@@ -1,78 +1,78 @@
name: Upload Preview Build to Netlify
on:
workflow_run:
workflows: ["Build PR"]
types:
- completed
workflow_run:
workflows: ["Build pull request"]
types:
- completed
jobs:
build:
runs-on: ubuntu-latest
if: >
${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# There's a 'download artifact' action but it hasn't been updated for the
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
# so instead we get this mess:
- name: 'Download artifact'
uses: actions/github-script@v6.0.0
with:
script: |
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
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.0.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
id: netlify
uses: nwtgck/actions-netlify@v1.2.3
with:
publish-dir: dist
deploy-message: "Deploy from GitHub Actions"
# These don't work because we're in workflow_run
enable-pull-request-comment: false
enable-commit-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
timeout-minutes: 1
- name: Edit PR Description
uses: velas/pr-description@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
description-message: |
Preview: ${{ steps.netlify.outputs.deploy-url }}
⚠️ Exercise caution. Use test accounts. ⚠️
get-build-and-deploy:
runs-on: ubuntu-latest
if: >
${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# There's a 'download artifact' action but it hasn't been updated for the
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
# so instead we get this mess:
- name: Download artifact
uses: actions/github-script@v6.0.0
with:
script: |
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
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.0.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
id: netlify
uses: nwtgck/actions-netlify@v1.2.3
with:
publish-dir: dist
deploy-message: "Deploy from GitHub Actions"
# These don't work because we're in workflow_run
enable-pull-request-comment: false
enable-commit-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
timeout-minutes: 1
- name: Edit PR Description
uses: velas/pr-description@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
description-message: |
Preview: ${{ steps.netlify.outputs.deploy-url }}
⚠️ Exercise caution. Use test accounts. ⚠️
-34
View File
@@ -1,34 +0,0 @@
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@v3.0.0
- name: Log in to Docker Hub
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3.6.2
with:
images: ajbura/cinny
- name: Build and push Docker image
uses: docker/build-push-action@v2.9.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+6 -4
View File
@@ -6,13 +6,15 @@ on:
- dev
jobs:
deploy:
deploy-to-netlify:
name: 'Deploy'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.0
- uses: jsmrcaga/action-netlify-deploy@v1.7.2
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.0
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2
with:
install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
-21
View File
@@ -1,21 +0,0 @@
name: 'Deploy to Netlify (prod)'
on:
release:
types: [published]
jobs:
deploy:
name: 'Deploy'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.0
- uses: jsmrcaga/action-netlify-deploy@v1.7.2
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
+56
View File
@@ -0,0 +1,56 @@
name: 'Production deploy'
on:
release:
types: [published]
jobs:
deploy-to-netlify:
name: 'Deploy to Netlify'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.0
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2
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
- name: Get version from tag
id: vars
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
- name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Upload tagged release
uses: softprops/action-gh-release@v0.1.14
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
push_to_dockerhub:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.0
- name: Login to Docker Hub
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3.6.2
with:
images: ajbura/cinny
- name: Build and push Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1 -1
View File
@@ -1,5 +1,5 @@
## Builder
FROM node:17.6.0-alpine3.15 as builder
FROM node:17.7.2-alpine3.15 as builder
WORKDIR /src
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Ajay Bura (ajbura) and other contributors
Copyright (c) 2021 Ajay Bura (ajbura) and 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
+71 -71
View File
@@ -1,12 +1,12 @@
{
"name": "cinny",
"version": "1.8.0",
"version": "1.8.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cinny",
"version": "1.8.0",
"version": "1.8.2",
"license": "MIT",
"dependencies": {
"@fontsource/inter": "^4.5.5",
@@ -20,7 +20,7 @@
"file-saver": "^2.0.5",
"flux": "^4.0.3",
"formik": "^2.2.9",
"html-react-parser": "^1.4.8",
"html-react-parser": "^1.4.9",
"linkifyjs": "^2.1.9",
"matrix-js-sdk": "^15.6.0",
"micromark": "^3.0.10",
@@ -38,10 +38,10 @@
"react-modal": "^3.14.4",
"sanitize-html": "^2.7.0",
"tippy.js": "^6.3.7",
"twemoji": "^13.1.0"
"twemoji": "^14.0.1"
},
"devDependencies": {
"@babel/core": "^7.17.7",
"@babel/core": "^7.17.8",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"assert": "^2.0.0",
@@ -115,9 +115,9 @@
}
},
"node_modules/@babel/core": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.7.tgz",
"integrity": "sha512-djHlEfFHnSnTAcPb7dATbiM5HxGOP98+3JLBZtjRb5I7RXrw7kFRoG2dXM8cm3H+o11A8IFH/uprmJpwFynRNQ==",
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz",
"integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
@@ -125,8 +125,8 @@
"@babel/generator": "^7.17.7",
"@babel/helper-compilation-targets": "^7.17.7",
"@babel/helper-module-transforms": "^7.17.7",
"@babel/helpers": "^7.17.7",
"@babel/parser": "^7.17.7",
"@babel/helpers": "^7.17.8",
"@babel/parser": "^7.17.8",
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.3",
"@babel/types": "^7.17.0",
@@ -483,9 +483,9 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.7.tgz",
"integrity": "sha512-TKsj9NkjJfTBxM7Phfy7kv6yYc4ZcOo+AaWGqQOKTPDOmcGkIFb5xNA746eKisQkm4yavUYh4InYM9S+VnO01w==",
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz",
"integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==",
"dev": true,
"dependencies": {
"@babel/template": "^7.16.7",
@@ -511,9 +511,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.7.tgz",
"integrity": "sha512-bm3AQf45vR4gKggRfvJdYJ0gFLoCbsPxiFLSH6hTVYABptNHY6l9NrhnucVjQ/X+SPtLANT9lc0fFhikj+VBRA==",
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -5362,9 +5362,9 @@
]
},
"node_modules/domhandler": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz",
"integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"dependencies": {
"domelementtype": "^2.2.0"
},
@@ -7249,11 +7249,11 @@
}
},
"node_modules/html-dom-parser": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.1.0.tgz",
"integrity": "sha512-x3MOz8S8BkihGgggtp2N0U+JAWlD9iGERbjuBJH+gIGF9B1kGQh5x489IQ0YPqi8HyMIWCRLdJY1q3IaQlqG/Q==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.1.1.tgz",
"integrity": "sha512-ddijSO0GluZEdlLNmiAjFI+CkAgzUgf8n3vpitQhvlsniPMbCSWtu+QYmioaVmEN0stytm9C38p6SsTDthv0cA==",
"dependencies": {
"domhandler": "4.3.0",
"domhandler": "4.3.1",
"htmlparser2": "7.2.0"
}
},
@@ -7334,12 +7334,12 @@
}
},
"node_modules/html-react-parser": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.8.tgz",
"integrity": "sha512-5XsBdFVhJLxdtRp7tWwZ6DwqOt6fJ+2lJc0lctwjX1yaxWNB41S5uzqii7vJcSCaabM+CK28U75e5f4wIMqdSg==",
"version": "1.4.9",
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.9.tgz",
"integrity": "sha512-V45RVd7GEGtk5bmL9iBV/AkaTE6jj9uhvUrJ/cWlq06DrcMzvz2kqMeZfWEV2pIWkRDzLAqrotCfmFSvdTjpHA==",
"dependencies": {
"domhandler": "4.3.0",
"html-dom-parser": "1.1.0",
"domhandler": "4.3.1",
"html-dom-parser": "1.1.1",
"react-property": "2.0.0",
"style-to-js": "1.1.0"
},
@@ -9822,9 +9822,9 @@
}
},
"node_modules/node-forge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
"dev": true,
"engines": {
"node": ">= 6.13.0"
@@ -13137,20 +13137,20 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"node_modules/twemoji": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-13.1.0.tgz",
"integrity": "sha512-e3fZRl2S9UQQdBFLYXtTBT6o4vidJMnpWUAhJA+yLGR+kaUTZAt3PixC0cGvvxWSuq2MSz/o0rJraOXrWw/4Ew==",
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.1.tgz",
"integrity": "sha512-eoqhea0sUhmC10iTacksyp1v9O4BP1jKmVqtK+Nztw40/dzawSHkXL3/xCpyh+mukmEvJ0Gw9VLvwZfQ9HKXDw==",
"dependencies": {
"fs-extra": "^8.0.1",
"jsonfile": "^5.0.0",
"twemoji-parser": "13.1.0",
"twemoji-parser": "14.0.0",
"universalify": "^0.1.2"
}
},
"node_modules/twemoji-parser": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-13.1.0.tgz",
"integrity": "sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg=="
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -14120,9 +14120,9 @@
"dev": true
},
"@babel/core": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.7.tgz",
"integrity": "sha512-djHlEfFHnSnTAcPb7dATbiM5HxGOP98+3JLBZtjRb5I7RXrw7kFRoG2dXM8cm3H+o11A8IFH/uprmJpwFynRNQ==",
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz",
"integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.1.0",
@@ -14130,8 +14130,8 @@
"@babel/generator": "^7.17.7",
"@babel/helper-compilation-targets": "^7.17.7",
"@babel/helper-module-transforms": "^7.17.7",
"@babel/helpers": "^7.17.7",
"@babel/parser": "^7.17.7",
"@babel/helpers": "^7.17.8",
"@babel/parser": "^7.17.8",
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.3",
"@babel/types": "^7.17.0",
@@ -14397,9 +14397,9 @@
}
},
"@babel/helpers": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.7.tgz",
"integrity": "sha512-TKsj9NkjJfTBxM7Phfy7kv6yYc4ZcOo+AaWGqQOKTPDOmcGkIFb5xNA746eKisQkm4yavUYh4InYM9S+VnO01w==",
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz",
"integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==",
"dev": true,
"requires": {
"@babel/template": "^7.16.7",
@@ -14419,9 +14419,9 @@
}
},
"@babel/parser": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.7.tgz",
"integrity": "sha512-bm3AQf45vR4gKggRfvJdYJ0gFLoCbsPxiFLSH6hTVYABptNHY6l9NrhnucVjQ/X+SPtLANT9lc0fFhikj+VBRA==",
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==",
"dev": true
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
@@ -18188,9 +18188,9 @@
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
},
"domhandler": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz",
"integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"requires": {
"domelementtype": "^2.2.0"
}
@@ -19693,11 +19693,11 @@
}
},
"html-dom-parser": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.1.0.tgz",
"integrity": "sha512-x3MOz8S8BkihGgggtp2N0U+JAWlD9iGERbjuBJH+gIGF9B1kGQh5x489IQ0YPqi8HyMIWCRLdJY1q3IaQlqG/Q==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.1.1.tgz",
"integrity": "sha512-ddijSO0GluZEdlLNmiAjFI+CkAgzUgf8n3vpitQhvlsniPMbCSWtu+QYmioaVmEN0stytm9C38p6SsTDthv0cA==",
"requires": {
"domhandler": "4.3.0",
"domhandler": "4.3.1",
"htmlparser2": "7.2.0"
},
"dependencies": {
@@ -19751,12 +19751,12 @@
}
},
"html-react-parser": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.8.tgz",
"integrity": "sha512-5XsBdFVhJLxdtRp7tWwZ6DwqOt6fJ+2lJc0lctwjX1yaxWNB41S5uzqii7vJcSCaabM+CK28U75e5f4wIMqdSg==",
"version": "1.4.9",
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.9.tgz",
"integrity": "sha512-V45RVd7GEGtk5bmL9iBV/AkaTE6jj9uhvUrJ/cWlq06DrcMzvz2kqMeZfWEV2pIWkRDzLAqrotCfmFSvdTjpHA==",
"requires": {
"domhandler": "4.3.0",
"html-dom-parser": "1.1.0",
"domhandler": "4.3.1",
"html-dom-parser": "1.1.1",
"react-property": "2.0.0",
"style-to-js": "1.1.0"
}
@@ -21566,9 +21566,9 @@
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-forge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
"dev": true
},
"node-releases": {
@@ -24053,20 +24053,20 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"twemoji": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-13.1.0.tgz",
"integrity": "sha512-e3fZRl2S9UQQdBFLYXtTBT6o4vidJMnpWUAhJA+yLGR+kaUTZAt3PixC0cGvvxWSuq2MSz/o0rJraOXrWw/4Ew==",
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.1.tgz",
"integrity": "sha512-eoqhea0sUhmC10iTacksyp1v9O4BP1jKmVqtK+Nztw40/dzawSHkXL3/xCpyh+mukmEvJ0Gw9VLvwZfQ9HKXDw==",
"requires": {
"fs-extra": "^8.0.1",
"jsonfile": "^5.0.0",
"twemoji-parser": "13.1.0",
"twemoji-parser": "14.0.0",
"universalify": "^0.1.2"
}
},
"twemoji-parser": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-13.1.0.tgz",
"integrity": "sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg=="
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="
},
"type-check": {
"version": "0.4.0",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "cinny",
"version": "1.8.0",
"version": "1.8.2",
"description": "Yet another matrix client",
"main": "index.js",
"engines": {
@@ -26,7 +26,7 @@
"file-saver": "^2.0.5",
"flux": "^4.0.3",
"formik": "^2.2.9",
"html-react-parser": "^1.4.8",
"html-react-parser": "^1.4.9",
"linkifyjs": "^2.1.9",
"matrix-js-sdk": "^15.6.0",
"micromark": "^3.0.10",
@@ -44,10 +44,10 @@
"react-modal": "^3.14.4",
"sanitize-html": "^2.7.0",
"tippy.js": "^6.3.7",
"twemoji": "^13.1.0"
"twemoji": "^14.0.1"
},
"devDependencies": {
"@babel/core": "^7.17.7",
"@babel/core": "^7.17.8",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"assert": "^2.0.0",
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
<g>
<circle fill="#010101" cx="19" cy="6" r="4"/>
</g>
<path fill="#010101" d="M13.3,8H7v2h7.5C14,9.4,13.6,8.7,13.3,8z"/>
<path fill="#010101" d="M19,12v5.6l-2.4-1.3L16.1,16h-0.5H5V6h8c0-0.7,0.1-1.4,0.3-2H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2
h10.8l5.4,3v-9.3C20.4,11.9,19.7,12,19,12z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 811 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path fill="#010101" d="M19,6v11.6l-2.4-1.3L16.1,16h-0.5H5V6H19 M19.2,4H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2h10.8l5.4,3V6
C21,4.9,20.2,4,19.2,4L19.2,4z"/>
<rect x="7" y="8" fill="#010101" width="10" height="2"/>
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 732 B

+7 -6
View File
@@ -4,13 +4,14 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<polygon fill="#010101" points="11,6 11,12 15.2,16.2 16.7,14.8 13,11.2 13,6 "/>
<path fill="#010101" d="M12,2C6.5,2,2,6.5,2,12H0.2L3,14.8L5.8,12H4c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8c-1.9,0-3.7-0.7-5-1.8
l-1.2,1.6C7.4,21.2,9.6,22,12,22c5.5,0,10-4.5,10-10S17.5,2,12,2z"/>
<polygon points="11,7 11,13 14.5,16.5 15.9,15.1 13,12.2 13,7 "/>
<path d="M12,2C8.7,2,5.8,3.6,4,6V4H2.5v5h5V7.5H5.4C6.9,5.4,9.3,4,12,4c4.4,0,8,3.6,8,8s-3.6,8-8,8s-8-3.6-8-8H2
c0,5.5,4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z"/>
</g>
<g>
<polygon fill="#010101" points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/>
<path fill="#010101" d="M50,40c-5.5,0-10,4.5-10,10h-1.8l2.8,2.8l2.8-2.8H42c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8
c-1.9,0-3.7-0.7-5-1.8l-1.2,1.6c1.7,1.4,3.9,2.2,6.3,2.2c5.5,0,10-4.5,10-10S55.5,40,50,40z"/>
<polygon points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/>
<polygon points="45.5,47 40.5,47 40.5,42 42,42 42,45.5 45.5,45.5 "/>
<path d="M50,40c-4.1,0-7.6,2.5-9.2,6h2.2c1.4-2.4,4-4,6.9-4c4.4,0,8,3.6,8,8s-3.6,8-8,8s-8-3.6-8-8h-2c0,5.5,4.5,10,10,10
s10-4.5,10-10S55.5,40,50,40z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 985 B

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -74,7 +74,7 @@ Tabs.defaultProps = {
Tabs.propTypes = {
items: PropTypes.arrayOf(
PropTypes.exact({
PropTypes.shape({
iconSrc: PropTypes.string,
text: PropTypes.string,
disabled: PropTypes.bool,
@@ -84,4 +84,4 @@ Tabs.propTypes = {
onSelect: PropTypes.func.isRequired,
};
export { Tabs as default };
export default Tabs;
+4 -1
View File
@@ -12,6 +12,7 @@ import RawModal from '../../atoms/modal/RawModal';
function Dialog({
className, isOpen, title, onAfterOpen, onAfterClose,
contentOptions, onRequestClose, closeFromOutside, children,
invisibleScroll,
}) {
return (
<RawModal
@@ -36,7 +37,7 @@ function Dialog({
{contentOptions}
</Header>
<div className="dialog__content__wrapper">
<ScrollView autoHide>
<ScrollView autoHide invisible={invisibleScroll}>
<div className="dialog__content-container">
{children}
</div>
@@ -55,6 +56,7 @@ Dialog.defaultProps = {
onAfterClose: null,
onRequestClose: null,
closeFromOutside: true,
invisibleScroll: false,
};
Dialog.propTypes = {
@@ -67,6 +69,7 @@ Dialog.propTypes = {
onRequestClose: PropTypes.func,
closeFromOutside: PropTypes.bool,
children: PropTypes.node.isRequired,
invisibleScroll: PropTypes.bool,
};
export default Dialog;
-5
View File
@@ -21,8 +21,3 @@
flex-direction: column;
}
}
.dialog__content-container {
padding-top: var(--sp-extra-tight);
padding-bottom: var(--sp-extra-loose);
}
@@ -0,0 +1,49 @@
import React, { useState, useEffect } from 'react';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import IconButton from '../../atoms/button/IconButton';
import Dialog from './Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function ReusableDialog() {
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const handleOpen = (title, render, afterClose) => {
setIsOpen(true);
setData({ title, render, afterClose });
};
navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
};
}, []);
const handleAfterClose = () => {
data.afterClose();
setData(null);
};
const handleRequestClose = () => {
setIsOpen(false);
};
return (
<Dialog
isOpen={isOpen}
title={data?.title || ''}
onAfterClose={handleAfterClose}
onRequestClose={handleRequestClose}
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />}
invisibleScroll
>
{data?.render(handleRequestClose) || <div />}
</Dialog>
);
}
export default ReusableDialog;
+24 -4
View File
@@ -186,9 +186,17 @@ const MessageBody = React.memo(({
// if body is not string it is a React element.
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
let content = isCustomHTML
? twemojify(sanitizeCustomHtml(body), undefined, true, false)
: twemojify(body, undefined, true);
let content = null;
if (isCustomHTML) {
try {
content = twemojify(sanitizeCustomHtml(body), undefined, true, false);
} catch {
console.error('Malformed custom html: ', body);
content = twemojify(body, undefined);
}
} else {
content = twemojify(body, undefined, true);
}
// Determine if this message should render with large emojis
// Criteria:
@@ -469,6 +477,18 @@ function isMedia(mE) {
);
}
// if editedTimeline has mEventId then pass editedMEvent else pass mEvent to openViewSource
function handleOpenViewSource(mEvent, roomTimeline) {
const eventId = mEvent.getId();
const { editedTimeline } = roomTimeline ?? {};
let editedMEvent;
if (editedTimeline?.has(eventId)) {
const editedList = editedTimeline.get(eventId);
editedMEvent = editedList[editedList.length - 1];
}
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
}
const MessageOptions = React.memo(({
roomTimeline, mEvent, edit, reply,
}) => {
@@ -516,7 +536,7 @@ const MessageOptions = React.memo(({
</MenuItem>
<MenuItem
iconSrc={CmdIC}
onClick={() => openViewSource(mEvent)}
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
>
View source
</MenuItem>
@@ -1,7 +1,7 @@
@use '../../partials/dir';
.pw-model {
--modal-height: 656px;
--modal-height: 774px;
max-height: var(--modal-height) !important;
height: 100%;
}
+2 -21
View File
@@ -9,6 +9,7 @@ import colorMXID from '../../../util/colorMXID';
import { openProfileViewer } from '../../../client/action/navigation';
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -19,26 +20,6 @@ import PeopleSelector from '../people-selector/PeopleSelector';
const PER_PAGE_MEMBER = 50;
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
function sortByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
function normalizeMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
@@ -65,7 +46,7 @@ function useMemberOfMembership(roomId, membership) {
if (event && event?.getRoomId() !== roomId) return;
const memberOfMembership = normalizeMembers(
room.getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel),
.sort(memberByAtoZ).sort(memberByPowerLevel),
);
setMembers(memberOfMembership);
};
@@ -32,29 +32,15 @@ const items = [{
type: cons.notifs.MUTE,
}];
function getNotifType(roomId) {
const mx = initMatrix.matrixClient;
const pushRule = mx.getRoomPushRule('global', roomId);
if (typeof pushRule === 'undefined') {
const overridePushRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
if (typeof overridePushRules === 'undefined') return 0;
const isMuteOverride = overridePushRules.find((rule) => (
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 setRoomNotifType(roomId, newType) {
const mx = initMatrix.matrixClient;
const roomPushRule = mx.getRoomPushRule('global', roomId);
const { notifications } = initMatrix;
let roomPushRule;
try {
roomPushRule = mx.getRoomPushRule('global', roomId);
} catch {
roomPushRule = undefined;
}
const promises = [];
if (newType === cons.notifs.MUTE) {
@@ -76,7 +62,7 @@ function setRoomNotifType(roomId, newType) {
return promises;
}
const oldState = getNotifType(roomId);
const oldState = notifications.getNotiType(roomId);
if (oldState === cons.notifs.MUTE) {
promises.push(mx.deletePushRule('global', 'override', roomId));
}
@@ -115,8 +101,9 @@ function setRoomNotifType(roomId, newType) {
}
function useNotifications(roomId) {
const [activeType, setActiveType] = useState(getNotifType(roomId));
useEffect(() => setActiveType(getNotifType(roomId)), [roomId]);
const { notifications } = initMatrix;
const [activeType, setActiveType] = useState(notifications.getNotiType(roomId));
useEffect(() => setActiveType(notifications.getNotiType(roomId)), [roomId]);
const setNotification = useCallback((item) => {
if (item.type === activeType.type) return;
@@ -6,6 +6,7 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openInviteUser } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
import { markAsRead } from '../../../client/action/notifications';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import RoomNotification from '../room-notification/RoomNotification';
@@ -20,10 +21,8 @@ function RoomOptions({ roomId, afterOptionSelect }) {
const canInvite = room?.canInvite(mx.getUserId());
const handleMarkAsRead = () => {
markAsRead(roomId);
afterOptionSelect();
if (!room) return;
const events = room.getLiveTimeline().getEvents();
mx.sendReadReceipt(events[events.length - 1]);
};
const handleInviteClick = () => {
@@ -11,13 +11,16 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
import { blurOnBubbling } from '../../atoms/button/script';
function RoomSelectorWrapper({
isSelected, isUnread, onClick,
isSelected, isMuted, isUnread, onClick,
content, options, onContextMenu,
}) {
let myClass = isUnread ? ' room-selector--unread' : '';
myClass += isSelected ? ' room-selector--selected' : '';
const classes = ['room-selector'];
if (isMuted) classes.push('room-selector--muted');
if (isUnread) classes.push('room-selector--unread');
if (isSelected) classes.push('room-selector--selected');
return (
<div className={`room-selector${myClass}`}>
<div className={classes.join(' ')}>
<button
className="room-selector__content"
type="button"
@@ -32,11 +35,13 @@ function RoomSelectorWrapper({
);
}
RoomSelectorWrapper.defaultProps = {
isMuted: false,
options: null,
onContextMenu: null,
};
RoomSelectorWrapper.propTypes = {
isSelected: PropTypes.bool.isRequired,
isMuted: PropTypes.bool,
isUnread: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
content: PropTypes.node.isRequired,
@@ -46,12 +51,13 @@ RoomSelectorWrapper.propTypes = {
function RoomSelector({
name, parentName, roomId, imageSrc, iconSrc,
isSelected, isUnread, notificationCount, isAlert,
isSelected, isMuted, isUnread, notificationCount, isAlert,
options, onClick, onContextMenu,
}) {
return (
<RoomSelectorWrapper
isSelected={isSelected}
isMuted={isMuted}
isUnread={isUnread}
content={(
<>
@@ -91,6 +97,7 @@ RoomSelector.defaultProps = {
isSelected: false,
imageSrc: null,
iconSrc: null,
isMuted: false,
options: null,
onContextMenu: null,
};
@@ -101,6 +108,7 @@ RoomSelector.propTypes = {
imageSrc: PropTypes.string,
iconSrc: PropTypes.string,
isSelected: PropTypes.bool,
isMuted: PropTypes.bool,
isUnread: PropTypes.bool.isRequired,
notificationCount: PropTypes.oneOfType([
PropTypes.string,
@@ -9,6 +9,10 @@
border-radius: var(--bo-radius);
cursor: pointer;
&--muted {
opacity: 0.6;
}
&--unread {
.room-selector__content > .text {
color: var(--tc-surface-high);
@@ -9,7 +9,11 @@ function SettingTile({ title, options, content }) {
<div className="setting-tile">
<div className="setting-tile__content">
<div className="setting-tile__title">
<Text variant="b1">{title}</Text>
{
typeof title === 'string'
? <Text variant="b1">{title}</Text>
: title
}
</div>
{content}
</div>
@@ -24,7 +28,7 @@ SettingTile.defaultProps = {
};
SettingTile.propTypes = {
title: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
options: PropTypes.node,
content: PropTypes.node,
};
@@ -88,7 +88,7 @@ function SpaceAddExistingContent({ roomId }) {
};
const handleSearch = (ev) => {
const term = ev.target.value.toLocaleLowerCase().replaceAll(' ', '');
const term = ev.target.value.toLocaleLowerCase().replace(/\s/g, '');
if (term === '') {
setSearchIds(null);
return;
@@ -100,7 +100,7 @@ function SpaceAddExistingContent({ roomId }) {
if (!name) return false;
name = name.normalize('NFKC')
.toLocaleLowerCase()
.replaceAll(' ', '');
.replace(/\s/g, '');
return name.includes(term);
});
setSearchIds(searchedIds);
+1 -1
View File
@@ -210,7 +210,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
/>
)}
content={(
<Text variant="b3">Override the default (100) power level.</Text>
<Text variant="b3">Founder (101) override the default Admin (100) power level.</Text>
)}
/>
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
@@ -2,6 +2,7 @@
@use '../../partials/dir';
.create-room {
margin: var(--sp-normal);
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
&__form > * {
+25 -6
View File
@@ -12,6 +12,7 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { addRecentEmoji, getRecentEmojis } from './recent';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -20,6 +21,7 @@ import Input from '../../atoms/input/Input';
import ScrollView from '../../atoms/scroll/ScrollView';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import RecentClockIC from '../../../../public/res/ic/outlined/recent-clock.svg';
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
import CupIC from '../../../../public/res/ic/outlined/cup.svg';
@@ -29,10 +31,11 @@ import BulbIC from '../../../../public/res/ic/outlined/bulb.svg';
import PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
const ROW_EMOJIS_COUNT = 7;
const EmojiGroup = React.memo(({ name, groupEmojis }) => {
function getEmojiBoard() {
const emojiBoard = [];
const ROW_EMOJIS_COUNT = 7;
const totalEmojis = groupEmojis.length;
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
@@ -147,8 +150,9 @@ function EmojiBoard({ onSelect, searchRef }) {
function selectEmoji(e) {
if (isTargetNotEmoji(e.target)) return;
const emoji = e.target;
onSelect(getEmojiDataFromTarget(emoji));
const emoji = getEmojiDataFromTarget(e.target);
onSelect(emoji);
if (emoji.hexcode) addRecentEmoji(emoji.unicode);
}
function setEmojiInfo(emoji) {
@@ -188,6 +192,9 @@ function EmojiBoard({ onSelect, searchRef }) {
}
const [availableEmojis, setAvailableEmojis] = useState([]);
const [recentEmojis, setRecentEmojis] = useState([]);
const recentOffset = recentEmojis.length > 0 ? 1 : 0;
useEffect(() => {
const updateAvailableEmoji = (selectedRoomId) => {
@@ -215,6 +222,9 @@ function EmojiBoard({ onSelect, searchRef }) {
const onOpen = () => {
searchRef.current.value = '';
handleSearchChange();
// only update when board is getting opened to prevent shifting UI
setRecentEmojis(getRecentEmojis(3 * ROW_EMOJIS_COUNT));
};
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
@@ -230,7 +240,7 @@ function EmojiBoard({ onSelect, searchRef }) {
const $emojiContent = scrollEmojisRef.current.firstElementChild;
const groupCount = $emojiContent.childElementCount;
if (groupCount > emojiGroups.length) {
tabIndex += groupCount - emojiGroups.length - availableEmojis.length;
tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
}
$emojiContent.children[tabIndex].scrollIntoView();
}
@@ -246,6 +256,7 @@ function EmojiBoard({ onSelect, searchRef }) {
<ScrollView ref={scrollEmojisRef} autoHide>
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
<SearchedEmoji />
{recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
{
availableEmojis.map((pack) => (
<EmojiGroup
@@ -271,13 +282,21 @@ function EmojiBoard({ onSelect, searchRef }) {
</div>
<ScrollView invisible>
<div className="emoji-board__nav">
{recentEmojis.length > 0 && (
<IconButton
onClick={() => openGroup(0)}
src={RecentClockIC}
tooltip="Recent"
tooltipPlacement="right"
/>
)}
<div className="emoji-board__nav-custom">
{
availableEmojis.map((pack) => {
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
return (
<IconButton
onClick={() => openGroup(pack.packIndex)}
onClick={() => openGroup(recentOffset + pack.packIndex)}
src={src}
key={pack.packIndex}
tooltip={pack.displayName}
@@ -301,7 +320,7 @@ function EmojiBoard({ onSelect, searchRef }) {
[7, FlagIC, 'Flags'],
].map(([indx, ico, name]) => (
<IconButton
onClick={() => openGroup(availableEmojis.length + indx)}
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
key={indx}
src={ico}
tooltip={name}
+36
View File
@@ -0,0 +1,36 @@
import initMatrix from '../../../client/initMatrix';
import { emojis } from './emoji';
const eventType = 'io.element.recent_emoji';
function getRecentEmojisRaw() {
return initMatrix.matrixClient.getAccountData(eventType).getContent().recent_emoji ?? [];
}
export function getRecentEmojis(limit) {
const res = [];
getRecentEmojisRaw()
.sort((a, b) => b[1] - a[1])
.find(([unicode]) => {
const emoji = emojis.find((e) => e.unicode === unicode);
if (emoji) return res.push(emoji) >= limit;
return false;
});
return res;
}
export function addRecentEmoji(unicode) {
const recent = getRecentEmojisRaw();
const i = recent.findIndex(([u]) => u === unicode);
let entry;
if (i < 0) {
entry = [unicode, 1];
} else {
[entry] = recent.splice(i, 1);
entry[1] += 1;
}
recent.unshift(entry);
initMatrix.matrixClient.setAccountData(eventType, {
recent_emoji: recent.slice(0, 100),
});
}
+26 -4
View File
@@ -1,18 +1,38 @@
import React, { useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie';
import { roomIdByActivity } from '../../../util/sort';
import RoomsCategory from './RoomsCategory';
import { AtoZ } from './common';
const drawerPostie = new Postie();
function Directs() {
const mx = initMatrix.matrixClient;
const { roomList, notifications } = initMatrix;
const directIds = [...roomList.directs].sort(AtoZ);
const [directIds, setDirectIds] = useState([]);
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), []);
useEffect(() => {
const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
if (!roomList.directs.has(room.roomId)) return;
if (!data.liveEvent) return;
if (directIds[0] === room.roomId) return;
const newDirectIds = [room.roomId];
directIds.forEach((id) => {
if (id === room.roomId) return;
newDirectIds.push(id);
});
setDirectIds(newDirectIds);
};
mx.on('Room.timeline', handleTimeline);
return () => {
mx.removeListener('Room.timeline', handleTimeline);
};
}, [directIds]);
useEffect(() => {
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
@@ -33,9 +53,11 @@ function Directs() {
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
return () => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
};
}, []);
+25 -17
View File
@@ -5,11 +5,11 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie';
import { roomIdByActivity, roomIdByAtoZ } from '../../../util/sort';
import RoomsCategory from './RoomsCategory';
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
import { AtoZ, RoomToDM } from './common';
const drawerPostie = new Postie();
function Home({ spaceId }) {
@@ -34,10 +34,6 @@ function Home({ spaceId }) {
roomIds = roomList.getOrphanRooms();
}
spaceIds.sort(AtoZ);
roomIds.sort(AtoZ);
directIds.sort(AtoZ);
if (isCategorized) {
categories = roomList.getCategorizedSpaces(spaceIds);
categories.delete(spaceId);
@@ -62,35 +58,47 @@ function Home({ spaceId }) {
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
return () => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
};
}, []);
return (
<>
{ !isCategorized && spaceIds.length !== 0 && (
<RoomsCategory name="Spaces" roomIds={spaceIds} drawerPostie={drawerPostie} />
<RoomsCategory name="Spaces" roomIds={spaceIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)}
{ roomIds.length !== 0 && (
<RoomsCategory name="Rooms" roomIds={roomIds} drawerPostie={drawerPostie} />
<RoomsCategory name="Rooms" roomIds={roomIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)}
{ directIds.length !== 0 && (
<RoomsCategory name="People" roomIds={directIds} drawerPostie={drawerPostie} />
<RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
)}
{ isCategorized && [...categories].map(([catId, childIds]) => (
<RoomsCategory
key={catId}
spaceId={catId}
name={mx.getRoom(catId).name}
roomIds={[...childIds].sort(AtoZ).sort(RoomToDM)}
drawerPostie={drawerPostie}
/>
))}
{ isCategorized && [...categories].map(([catId, childIds]) => {
const rms = [];
const dms = [];
childIds.forEach((id) => {
if (directs.has(id)) dms.push(id);
else rms.push(id);
});
rms.sort(roomIdByAtoZ);
dms.sort(roomIdByActivity);
return (
<RoomsCategory
key={catId}
spaceId={catId}
name={mx.getRoom(catId).name}
roomIds={rms.concat(dms)}
drawerPostie={drawerPostie}
/>
);
})}
</>
);
}
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import './RoomsCategory.scss';
import initMatrix from '../../../client/initMatrix';
import { selectSpace, selectRoom,openReusableContextMenu } from '../../../client/action/navigation';
import { selectSpace, selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
import Text from '../../atoms/text/Text';
+6 -1
View File
@@ -3,6 +3,7 @@ import React, { 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 { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords, abbreviateNumber } from '../../../util/common';
@@ -23,9 +24,12 @@ function Selector({
const mx = initMatrix.matrixClient;
const noti = initMatrix.notifications;
const room = mx.getRoom(roomId);
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
const [, forceUpdate] = useForceUpdate();
useEffect(() => {
@@ -56,7 +60,8 @@ function Selector({
imageSrc={isDM ? imageSrc : null}
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
isSelected={navigation.selectedRoomId === roomId}
isUnread={noti.hasNoti(roomId)}
isMuted={isMuted}
isUnread={!isMuted && noti.hasNoti(roomId)}
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
isAlert={noti.getHighlightNoti(roomId) !== 0}
onClick={onClick}
+1 -1
View File
@@ -72,7 +72,7 @@ function ProfileAvatarMenu() {
return (
<SidebarAvatar
onClick={openSettings}
tooltip={profile.displayName}
tooltip="Settings"
avatar={(
<Avatar
text={profile.displayName}
-30
View File
@@ -1,30 +0,0 @@
import initMatrix from '../../../client/initMatrix';
function AtoZ(aId, bId) {
let aName = initMatrix.matrixClient.getRoom(aId).name;
let bName = initMatrix.matrixClient.getRoom(bId).name;
// remove "#" from the room name
// To ignore it in sorting
aName = aName.replaceAll('#', '');
bName = bName.replaceAll('#', '');
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
const RoomToDM = (aId, bId) => {
const { directs } = initMatrix.roomList;
const aIsDm = directs.has(aId);
const bIsDm = directs.has(bId);
if (aIsDm && !bIsDm) return 1;
if (!aIsDm && bIsDm) return -1;
return 0;
};
export { AtoZ, RoomToDM };
@@ -1,35 +1,44 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import ImageUpload from '../../molecules/image-upload/ImageUpload';
import Input from '../../atoms/input/Input';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
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 mx = initMatrix.matrixClient;
const user = mx.getUser(mx.getUserId());
const displayNameRef = useRef(null);
const bgColor = colorMXID(userId);
const [avatarSrc, setAvatarSrc] = useState(null);
const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true);
let username = mx.getUser(mx.getUserId()).displayName;
useEffect(() => {
let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => {
if (!isMounted) return;
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
setUsername(info.displayname);
});
return () => {
isMounted = false;
};
}, [userId]);
// Sets avatar URL and updates the avatar component in profile editor to reflect new upload
function handleAvatarUpload(url) {
const handleAvatarUpload = (url) => {
if (url === null) {
if (confirm('Are you sure you want to remove avatar?')) {
mx.setAvatarUrl('');
@@ -39,48 +48,72 @@ function ProfileEditor({
}
mx.setAvatarUrl(url);
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
}
};
function saveDisplayName() {
const saveDisplayName = () => {
const newDisplayName = displayNameRef.current.value;
if (newDisplayName !== null && newDisplayName !== username) {
mx.setDisplayName(newDisplayName);
username = newDisplayName;
setUsername(newDisplayName);
setDisabled(true);
setIsEditing(false);
}
}
};
function onDisplayNameInputChange() {
const onDisplayNameInputChange = () => {
setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
}
function cancelDisplayNameChanges() {
};
const cancelDisplayNameChanges = () => {
displayNameRef.current.value = username;
onDisplayNameInputChange();
}
setIsEditing(false);
};
return (
const renderForm = () => (
<form
className="profile-editor"
className="profile-editor__form"
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
>
<Input
label={`Display name of ${mx.getUserId()}`}
onChange={onDisplayNameInputChange}
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</form>
);
const renderInfo = () => (
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
<div>
<Text variant="h2" primary weight="medium">{twemojify(username)}</Text>
<IconButton
src={PencilIC}
size="extra-small"
tooltip="Edit"
onClick={() => setIsEditing(true)}
/>
</div>
<Text variant="b2">{mx.getUserId()}</Text>
</div>
);
return (
<div className="profile-editor">
<ImageUpload
text={username}
bgColor={bgColor}
bgColor={colorMXID(userId)}
imageSrc={avatarSrc}
onUpload={handleAvatarUpload}
onRequestRemove={() => handleAvatarUpload(null)}
/>
<div className="profile-editor__input-wrapper">
<Input
label={`Display name of ${mx.getUserId()}`}
onChange={onDisplayNameInputChange}
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</div>
</form>
{
isEditing ? renderForm() : renderInfo()
}
</div>
);
}
@@ -1,28 +1,41 @@
@use '../../partials/dir';
@use '../../partials/flex';
.profile-editor {
display: flex;
align-items: flex-start;
align-items: flex-end;
}
.profile-editor__input-wrapper {
flex: 1;
min-width: 0;
margin-top: 10px;
.profile-editor__info,
.profile-editor__form {
@extend .cp-fx__item-one;
@include dir.side(margin, var(--sp-loose), 0);
display: flex;
align-items: flex-end;
}
.profile-editor__info {
flex-direction: column;
& > div:first-child {
display: flex;
align-items: center;
}
.ic-btn {
margin: 0 var(--sp-extra-tight);
}
}
.profile-editor__form {
margin-top: 10px;
flex-wrap: wrap;
align-items: flex-end;
& > .input-container {
flex: 1;
@extend .cp-fx__item-one;
}
& > button {
height: 46px;
margin-top: var(--sp-normal);
}
& > * {
@include dir.side(margin, var(--sp-normal), 0);
}
}
@@ -4,7 +4,7 @@ import './PublicRooms.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { selectRoom } from '../../../client/action/navigation';
import { selectRoom, selectTab } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
import Text from '../../atoms/text/Text';
@@ -179,7 +179,9 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
}, [joiningRooms]);
function handleViewRoom(roomId) {
selectRoom(roomId);
const room = initMatrix.matrixClient.getRoom(roomId);
if (room.isSpaceRoom()) selectTab(roomId);
else selectRoom(roomId);
onRequestClose();
}
@@ -193,7 +195,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
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);
const isJoined = initMatrix.matrixClient.getRoom(room.room_id) !== null;
return (
<RoomTile
key={room.room_id}
+4
View File
@@ -8,6 +8,8 @@ import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom';
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
function Dialogs() {
return (
<>
@@ -18,6 +20,8 @@ function Dialogs() {
<CreateRoom />
<SpaceAddExisting />
<Search />
<ReusableDialog />
</>
);
}
+1 -10
View File
@@ -18,7 +18,6 @@ function Windows() {
const [inviteUser, changeInviteUser] = useState({
isOpen: false, roomId: undefined, term: undefined,
});
const [settings, changeSettings] = useState(false);
function openInviteList() {
changeInviteList(true);
@@ -36,20 +35,15 @@ function Windows() {
searchTerm,
});
}
function openSettings() {
changeSettings(true);
}
useEffect(() => {
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
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_ROOMS_OPENED, openPublicRooms);
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
};
}, []);
@@ -70,10 +64,7 @@ function Windows() {
searchTerm={inviteUser.searchTerm}
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
/>
<Settings
isOpen={settings}
onRequestClose={() => changeSettings(false)}
/>
<Settings />
<SpaceSettings />
<SpaceManage />
</>
@@ -64,9 +64,11 @@ function ReadReceipts() {
onRequestClose={() => setIsOpen(false)}
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
>
{
readers.map(renderPeople)
}
<div style={{ marginTop: 'var(--sp-tight)', marginBottom: 'var(--sp-extra-loose)' }}>
{
readers.map(renderPeople)
}
</div>
</Dialog>
);
}
+2 -21
View File
@@ -9,6 +9,7 @@ import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil
import colorMXID from '../../../util/colorMXID';
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header';
@@ -24,26 +25,6 @@ import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
function sortByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
function simplyfiMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
@@ -111,7 +92,7 @@ function PeopleDrawer({ roomId }) {
setMemberList(
simplyfiMembers(
getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel),
.sort(memberByAtoZ).sort(memberByPowerLevel),
),
);
};
+28 -15
View File
@@ -13,33 +13,46 @@ import RoomSettings from './RoomSettings';
import PeopleDrawer from './PeopleDrawer';
function Room() {
const [roomTimeline, setRoomTimeline] = useState(null);
const [eventId, setEventId] = useState(null);
const [roomInfo, setRoomInfo] = useState({
roomTimeline: null,
eventId: null,
});
const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
const mx = initMatrix.matrixClient;
const handleRoomSelected = (rId, pRoomId, eId) => {
if (mx.getRoom(rId)) {
setRoomTimeline(new RoomTimeline(rId, initMatrix.notifications));
setEventId(eId);
} else {
// TODO: add ability to join room if roomId is invalid
setRoomTimeline(null);
setEventId(null);
}
};
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
useEffect(() => {
const handleRoomSelected = (rId, pRoomId, eId) => {
roomInfo.roomTimeline?.removeInternalListeners();
if (mx.getRoom(rId)) {
setRoomInfo({
roomTimeline: new RoomTimeline(rId),
eventId: eId ?? null,
});
} else {
// TODO: add ability to join room if roomId is invalid
setRoomInfo({
roomTimeline: null,
eventId: null,
});
}
};
navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
return () => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
};
}, [roomInfo]);
useEffect(() => {
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
return () => {
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
roomTimeline?.removeInternalListeners();
};
}, []);
const { roomTimeline, eventId } = roomInfo;
if (roomTimeline === null) return <Welcome />;
return (
@@ -21,6 +21,7 @@ import AsyncSearch from '../../../util/AsyncSearch';
import Text from '../../atoms/text/Text';
import ScrollView from '../../atoms/scroll/ScrollView';
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
import { addRecentEmoji } from '../emoji-board/recent';
const commands = [{
name: 'markdown',
@@ -237,6 +238,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
viewEvent.emit('cmd_fired');
}
if (myCmd.prefix === ':') {
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
viewEvent.emit('cmd_fired', {
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
});
+1 -1
View File
@@ -10,7 +10,7 @@
&__info {
display: flex;
width: 40px;
@include dir.side(margin, 10px, 14px);
@include dir.side(margin, 14px, 10px);
& > * {
margin: auto;
+39 -48
View File
@@ -14,6 +14,7 @@ import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { openProfileViewer } from '../../../client/action/navigation';
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
import { markAsRead } from '../../../client/action/notifications';
import Divider from '../../atoms/divider/Divider';
import ScrollView from '../../atoms/scroll/ScrollView';
@@ -145,13 +146,12 @@ function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
if (isSpecificEvent) {
focusEventIndex = roomTimeline.getEventIndex(eId);
} else if (!readUptoEvtStore.getItem()) {
}
if (!readUptoEvtStore.getItem() && roomTimeline.hasEventInTimeline(readUpToId)) {
// either opening live timeline or jump to unread.
focusEventIndex = roomTimeline.getUnreadEventIndex(readUpToId);
if (roomTimeline.hasEventInTimeline(readUpToId)) {
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
}
} else {
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
}
if (readUptoEvtStore.getItem() && !isSpecificEvent) {
focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId());
}
@@ -167,7 +167,6 @@ function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
setEventTimeline(eventId);
return () => {
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
roomTimeline.removeInternalListeners();
limit.setFrom(0);
};
}, [roomTimeline, eventId]);
@@ -255,7 +254,7 @@ function useHandleScroll(
);
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
if (isAtBottom && readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
});
autoPaginate();
@@ -265,7 +264,7 @@ function useHandleScroll(
const timelineScroll = timelineScrollRef.current;
const limit = eventLimitRef.current;
if (readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
if (roomTimeline.isServingLiveTimeline()) {
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
@@ -286,44 +285,39 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
useEffect(() => {
const timelineScroll = timelineScrollRef.current;
const limit = eventLimitRef.current;
const sendReadReceipt = (event) => {
if (event.isSending()) return;
const trySendReadReceipt = (event) => {
if (myUserId === event.getSender()) {
roomTimeline.markAllAsRead();
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
return;
}
const readUpToEvent = readUptoEvtStore.getItem();
const readUpToId = roomTimeline.getReadUpToEventId();
const isUnread = readUpToEvent?.getId() === readUpToId;
const isUnread = readUpToEvent ? readUpToEvent?.getId() === readUpToId : true;
// if user doesn't have focus on app don't mark messages as read.
if (document.visibilityState === 'hidden' || timelineScroll.bottom >= 16) {
if (isUnread) return;
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
if (isUnread === false) {
if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
} else {
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
}
return;
}
// user has not mark room as read
if (!isUnread) {
roomTimeline.markAllAsRead();
}
const { timeline } = roomTimeline;
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToEvent?.getId();
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
if (unreadMsgIsLast) {
roomTimeline.markAllAsRead();
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
};
const handleEvent = (event) => {
const tLength = roomTimeline.timeline.length;
const isUserViewingLive = (
roomTimeline.isServingLiveTimeline()
&& limit.length >= tLength - 1
&& timelineScroll.bottom < SCROLL_TRIGGER_POS
);
if (isUserViewingLive) {
const isViewingLive = roomTimeline.isServingLiveTimeline() && limit.length >= tLength - 1;
const isAttached = timelineScroll.bottom < SCROLL_TRIGGER_POS;
if (isViewingLive && isAttached) {
limit.setFrom(tLength - limit.maxEvents);
sendReadReceipt(event);
trySendReadReceipt(event);
setEvent(event);
return;
}
@@ -332,11 +326,8 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
setEvent(event);
return;
}
const isUserDitchedLive = (
roomTimeline.isServingLiveTimeline()
&& limit.length >= tLength - 1
);
if (isUserDitchedLive) {
if (isViewingLive) {
// This stateUpdate will help to put the
// loading msg placeholder at bottom
setEvent(event);
@@ -353,17 +344,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
};
}, [roomTimeline]);
useEffect(() => {
const timelineScroll = timelineScrollRef.current;
if (!roomTimeline.initialized) return;
if (timelineScroll.bottom < 16
&& !roomTimeline.canPaginateForward()
&& document.visibilityState === 'visible') {
timelineScroll.scrollToBottom();
} else {
timelineScroll.tryRestoringScroll();
}
}, [newEvent, roomTimeline]);
return newEvent;
}
let jumpToItemIndex = -1;
@@ -394,7 +375,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
timelineScrollRef,
eventLimitRef,
);
useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
const { timeline } = roomTimeline;
@@ -419,7 +400,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
const readUpToId = roomTimeline.getReadUpToEventId();
if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
}
jumpToItemIndex = -1;
@@ -448,6 +429,16 @@ function RoomViewContent({ eventId, roomTimeline }) {
timelineScroll.tryRestoringScroll();
}, [onLimitUpdate]);
useEffect(() => {
const timelineScroll = timelineScrollRef.current;
if (!roomTimeline.initialized) return;
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
timelineScroll.scrollToBottom();
} else {
timelineScroll.tryRestoringScroll();
}
}, [newEvent]);
const handleTimelineScroll = (event) => {
const timelineScroll = timelineScrollRef.current;
if (!event.target) return;
+15 -20
View File
@@ -5,12 +5,13 @@ import './RoomViewFloating.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { markAsRead } from '../../../client/action/notifications';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import MessageIC from '../../../../public/res/ic/outlined/message.svg';
import MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsersActionJsx } from './common';
@@ -23,7 +24,7 @@ function useJumpToEvent(roomTimeline) {
};
const cancelJumpToEvent = () => {
roomTimeline.markAllAsRead();
markAsRead(roomTimeline.roomId);
setEventId(null);
};
@@ -36,11 +37,12 @@ function useJumpToEvent(roomTimeline) {
setEventId(readEventId);
}
const { notifications } = initMatrix;
const handleMarkAsRead = () => setEventId(null);
roomTimeline.on(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead);
return () => {
roomTimeline.removeListener(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead);
setEventId(null);
};
}, [roomTimeline]);
@@ -96,28 +98,21 @@ function RoomViewFloating({
return (
<>
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
<Button onClick={jumpToEvent} variant="primary">
<Text variant="b2">Jump to unread</Text>
<Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
<Text variant="b3" weight="medium">Jump to unread messages</Text>
</Button>
<Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary">
<Text variant="b3" weight="bold">Mark as read</Text>
</Button>
<IconButton
onClick={cancelJumpToEvent}
variant="primary"
size="extra-small"
src={TickMarkIC}
tooltipPlacement="bottom"
tooltip="Mark as read"
/>
</div>
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
<div className="bouncing-loader"><div /></div>
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
</div>
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
<IconButton
onClick={handleScrollToBottom}
src={ChevronBottomIC}
tooltip="Scroll to Bottom"
/>
<Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
<Text variant="b3" weight="medium">Jump to latest</Text>
</Button>
</div>
</>
);
+27 -24
View File
@@ -72,51 +72,54 @@
}
}
&__STB,
&__unread {
overflow: hidden;
background-color: var(--bg-surface-low);
border-radius: var(--bo-radius);
& button {
justify-content: flex-start;
border-radius: 0;
box-shadow: none;
padding: 6px var(--sp-tight);
& .ic-raw {
width: 16px;
height: 16px;
}
}
}
&__STB {
position: absolute;
@include dir.prop(right, var(--sp-normal), unset);
@include dir.prop(left, unset, var(--sp-normal));
@include dir.prop(left, 50%, unset);
@include dir.prop(right, unset, 50%);
bottom: 0;
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
background-color: var(--bg-surface-low);
transition: transform 200ms ease-in-out;
transform: translateY(100%) scale(0);
transform: translate(-50%, 100%);
&--open {
transform: translateY(-28px) scale(1);
transform: translate(-50%, -28px);
}
}
&__unread {
position: absolute;
top: var(--sp-extra-tight);
@include dir.prop(right, var(--sp-extra-tight), unset);
@include dir.prop(left, unset, var(--sp-extra-tight));
@include dir.prop(left, var(--sp-normal), unset);
@include dir.prop(right, unset, var(--sp-normal));
z-index: 999;
display: none;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-primary-border);
overflow: hidden;
width: calc(100% - var(--sp-extra-loose));
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
&--open {
display: flex;
}
& .ic-btn {
padding: 6px var(--sp-extra-tight);
border-radius: 0;
}
& .btn-primary {
& button:first-child {
@extend .cp-fx__item-one;
@include dir.side(margin, 0, 1px);
border-radius: 0;
padding: 0 var(--sp-tight);
&:focus {
background-color: var(--bg-primary-hover);
}
}
}
}
+30 -33
View File
@@ -7,6 +7,7 @@ import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { selectRoom, selectTab } from '../../../client/action/navigation';
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
import { roomIdByActivity } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -47,27 +48,24 @@ function useVisiblityToggle(setResult) {
return [isOpen, requestClose];
}
function mapRoomIds(roomIds, type) {
function mapRoomIds(roomIds) {
const mx = initMatrix.matrixClient;
const { directs, roomIdToParents } = initMatrix.roomList;
return roomIds.map((roomId) => {
let roomType = type;
if (!roomType) {
roomType = directs.has(roomId) ? 'direct' : 'room';
}
const room = mx.getRoom(roomId);
const parentSet = roomIdToParents.get(roomId);
const parentNames = parentSet
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
: undefined;
const parentNames = parentSet ? [] : undefined;
parentSet?.forEach((parentId) => parentNames.push(mx.getRoom(parentId).name));
const parents = parentNames ? parentNames.join(', ') : null;
let type = 'room';
if (room.isSpaceRoom()) type = 'space';
else if (directs.has(roomId)) type = 'direct';
return ({
type: roomType,
type,
name: room.name,
parents,
roomId,
@@ -93,34 +91,32 @@ function Search() {
const generateResults = (term) => {
const prefix = term.match(/^[#@*]/)?.[0];
if (term.length === 1) {
const { roomList } = initMatrix;
const spaces = mapRoomIds([...roomList.spaces], 'space').reverse();
const rooms = mapRoomIds([...roomList.rooms], 'room').reverse();
const directs = mapRoomIds([...roomList.directs], 'direct').reverse();
if (prefix === '*') {
asyncSearch.setup(spaces, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(spaces, '*');
} else if (prefix === '#') {
asyncSearch.setup(rooms, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(rooms, '#');
} else if (prefix === '@') {
asyncSearch.setup(directs, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(directs, '@');
} else {
const dataList = spaces.concat(rooms, directs);
asyncSearch.setup(dataList, { keys: 'name', isContain: true, limit: 20 });
asyncSearch.search(term);
}
} else {
if (term.length > 1) {
asyncSearch.search(prefix ? term.slice(1) : term);
return;
}
const { spaces, rooms, directs } = initMatrix.roomList;
let ids = null;
if (prefix) {
if (prefix === '#') ids = [...rooms];
else if (prefix === '@') ids = [...directs];
else ids = [...spaces];
} else {
ids = [...rooms].concat([...directs], [...spaces]);
}
ids.sort(roomIdByActivity);
const mappedIds = mapRoomIds(ids);
asyncSearch.setup(mappedIds, { keys: 'name', isContain: true, limit: 20 });
if (prefix) handleSearchResults(mappedIds, prefix);
else asyncSearch.search(term);
};
const loadRecentRooms = () => {
const { recentRooms } = navigation;
handleSearchResults(mapRoomIds(recentRooms).reverse(), '');
handleSearchResults(mapRoomIds(recentRooms).reverse());
};
const handleAfterOpen = () => {
@@ -154,6 +150,7 @@ function Search() {
else {
searchRef.current.value = '';
searchRef.current.focus();
loadRecentRooms();
}
};
+219
View File
@@ -0,0 +1,219 @@
import React, { useState, useEffect } from 'react';
import './DeviceManage.scss';
import dateFormat from 'dateformat';
import initMatrix from '../../../client/initMatrix';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import Spinner from '../../atoms/spinner/Spinner';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { useStore } from '../../hooks/useStore';
function useDeviceList() {
const mx = initMatrix.matrixClient;
const [deviceList, setDeviceList] = useState(null);
useEffect(() => {
let isMounted = true;
const updateDevices = () => mx.getDevices().then((data) => {
if (!isMounted) return;
setDeviceList(data.devices || []);
});
updateDevices();
const handleDevicesUpdate = (users) => {
if (users.includes(mx.getUserId())) {
updateDevices();
}
};
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
return () => {
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
isMounted = false;
};
}, []);
return deviceList;
}
function isCrossVerified(deviceId) {
try {
const mx = initMatrix.matrixClient;
const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId());
const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId);
const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
return deviceTrust.isCrossSigningVerified();
} catch {
return false;
}
}
function DeviceManage() {
const TRUNCATED_COUNT = 4;
const mx = initMatrix.matrixClient;
const deviceList = useDeviceList();
const [processing, setProcessing] = useState([]);
const [truncated, setTruncated] = useState(true);
const mountStore = useStore();
mountStore.setItem(true);
useEffect(() => {
setProcessing([]);
}, [deviceList]);
const addToProcessing = (device) => {
const old = [...processing];
old.push(device.device_id);
setProcessing(old);
};
const removeFromProcessing = () => {
setProcessing([]);
};
if (deviceList === null) {
return (
<div className="device-manage">
<div className="device-manage__loading">
<Spinner size="small" />
<Text>Loading devices...</Text>
</div>
</div>
);
}
const handleRename = async (device) => {
const newName = window.prompt('Edit session name', device.display_name);
if (newName === null || newName.trim() === '') return;
if (newName.trim() === device.display_name) return;
addToProcessing(device);
try {
await mx.setDeviceDetails(device.device_id, {
display_name: newName,
});
} catch {
if (!mountStore.getItem()) return;
removeFromProcessing(device);
}
};
const handleRemove = async (device, auth = undefined) => {
if (auth === undefined
? window.confirm(`You are about to logout "${device.display_name}" session?`)
: true
) {
addToProcessing(device);
try {
await mx.deleteDevice(device.device_id, auth);
} catch (e) {
if (e.httpStatus === 401 && e.data?.flows) {
const { flows } = e.data;
const flow = flows.find((f) => f.stages.includes('m.login.password'));
if (flow) {
const password = window.prompt('Please enter account password', '');
if (password && password.trim() !== '') {
handleRemove(device, {
session: e.data.session,
type: 'm.login.password',
password,
identifier: {
type: 'm.id.user',
user: mx.getUserId(),
},
});
return;
}
}
}
window.alert('Failed to remove session!');
if (!mountStore.getItem()) return;
removeFromProcessing(device);
}
}
};
const renderDevice = (device, isVerified) => {
const deviceId = device.device_id;
const displayName = device.display_name;
const lastIP = device.last_seen_ip;
const lastTS = device.last_seen_ts;
return (
<SettingTile
key={deviceId}
title={(
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
{displayName}
<Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
</Text>
)}
options={
processing.includes(deviceId)
? <Spinner size="small" />
: (
<>
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
</>
)
}
content={(
<Text variant="b3">
Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}>
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
</span>
{lastIP ? ` at ${lastIP}` : ''}
</Text>
)}
/>
);
};
const unverified = [];
const verified = [];
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
if (isCrossVerified(device.device_id)) verified.push(device);
else unverified.push(device);
});
return (
<div className="device-manage">
<div>
<MenuHeader>Unverified sessions</MenuHeader>
{
unverified.length > 0
? unverified.map((device) => renderDevice(device, false))
: <Text className="device-manage__info">No unverified session</Text>
}
</div>
<div>
<MenuHeader>Verified sessions</MenuHeader>
{
verified.length > 0
? verified.map((device, index) => {
if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true);
})
: <Text className="device-manage__info">No verified session</Text>
}
{ verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
{truncated ? `View ${verified.length - 4} more` : 'View less'}
</Button>
)}
{ deviceList.length > 0 && (
<Text className="device-manage__info" variant="b3">Session names are visible to everyone, so do not put any private info here.</Text>
)}
</div>
</div>
);
}
export default DeviceManage;
@@ -0,0 +1,18 @@
@use '../../partials/flex';
.device-manage {
&__loading {
@extend .cp-fx__row--c-c;
padding: var(--sp-extra-loose) var(--sp-normal);
.text {
margin: 0 var(--sp-normal);
}
}
&__info {
margin: var(--sp-normal);
}
& .setting-tile:last-of-type {
border-bottom: none;
}
}
+221 -194
View File
@@ -1,13 +1,13 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import React, { useState, useEffect } from 'react';
import './Settings.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import settings from '../../../client/state/settings';
import navigation from '../../../client/state/navigation';
import {
toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
toggleNotifications,
toggleNotifications, toggleNotificationSounds,
} from '../../../client/action/settings';
import logout from '../../../client/action/logout';
import { usePermission } from '../../hooks/usePermission';
@@ -16,16 +16,18 @@ import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Toggle from '../../atoms/button/Toggle';
import Tabs from '../../atoms/tabs/Tabs';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
import PopupWindow, { PWContentSelector } from '../../molecules/popup-window/PopupWindow';
import PopupWindow from '../../molecules/popup-window/PopupWindow';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import ProfileEditor from '../profile-editor/ProfileEditor';
import DeviceManage from './DeviceManage';
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 BellIC from '../../../../public/res/ic/outlined/bell.svg';
@@ -35,85 +37,74 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import CinnySVG from '../../../../public/res/svg/cinny.svg';
function GeneralSection() {
return (
<div className="settings-content">
<SettingTile
title=""
content={(
<ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
)}
/>
</div>
);
}
function AppearanceSection() {
const [, updateState] = useState({});
return (
<div className="settings-content">
<SettingTile
title="Follow system theme"
options={(
<Toggle
isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }}
/>
)}
content={<Text variant="b3">Use light or dark mode based on the system's settings.</Text>}
/>
{(() => {
if (!settings.useSystemTheme) {
return (
<SettingTile
title="Theme"
content={(
<SegmentedControls
selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
]}
onSelect={(index) => settings.setTheme(index)}
/>
)}
<div className="settings-appearance">
<div className="settings-appearance__card">
<MenuHeader>Theme</MenuHeader>
<SettingTile
title="Follow system theme"
options={(
<Toggle
isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }}
/>
);
}
})()}
<SettingTile
title="Markdown formatting"
options={(
<Toggle
isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }}
)}
content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
/>
{!settings.useSystemTheme && (
<SettingTile
title="Theme"
content={(
<SegmentedControls
selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
]}
onSelect={(index) => settings.setTheme(index)}
/>
)}
/>
)}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
/>
<SettingTile
title="Hide membership events"
options={(
<Toggle
isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
/>
<SettingTile
title="Hide nick/avatar events"
options={(
<Toggle
isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
/>
</div>
<div className="settings-appearance__card">
<MenuHeader>Room messages</MenuHeader>
<SettingTile
title="Markdown formatting"
options={(
<Toggle
isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }}
/>
)}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
/>
<SettingTile
title="Hide membership events"
options={(
<Toggle
isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
/>
<SettingTile
title="Hide nick/avatar events"
options={(
<Toggle
isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
/>
</div>
</div>
);
}
@@ -125,7 +116,7 @@ function NotificationsSection() {
const renderOptions = () => {
if (window.Notification === undefined) {
return <Text className="set-notifications__not-supported">Not supported in this browser.</Text>;
return <Text className="settings-notifications__not-supported">Not supported in this browser.</Text>;
}
if (permission === 'granted') {
@@ -152,11 +143,22 @@ function NotificationsSection() {
};
return (
<div className="set-notifications settings-content">
<div className="settings-notifications">
<MenuHeader>Notification & Sound</MenuHeader>
<SettingTile
title="Show desktop notifications"
title="Desktop notification"
options={renderOptions()}
content={<Text variant="b3">Show notifications when new messages arrive.</Text>}
content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
/>
<SettingTile
title="Notification Sound"
options={(
<Toggle
isActive={settings.isNotificationSounds}
onToggle={() => { toggleNotificationSounds(); updateState({}); }}
/>
)}
content={<Text variant="b3">Play sound when new messages arrive.</Text>}
/>
</div>
);
@@ -164,149 +166,174 @@ function NotificationsSection() {
function SecuritySection() {
return (
<div className="set-security settings-content">
<SettingTile
title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
/>
<SettingTile
title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
/>
<SettingTile
title="Export E2E room keys"
content={(
<>
<Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
<ExportE2ERoomKeys />
</>
)}
/>
<SettingTile
title="Import E2E room keys"
content={(
<>
<Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
<ImportE2ERoomKeys />
</>
)}
/>
<div className="settings-security">
<div className="settings-security__card">
<MenuHeader>Session Info</MenuHeader>
<SettingTile
title={`Session ID: ${initMatrix.matrixClient.getDeviceId()}`}
/>
<SettingTile
title={`Session key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
content={<Text variant="b3">Use this session ID-key combo to verify or manage this session.</Text>}
/>
</div>
<DeviceManage />
<div className="settings-security__card">
<MenuHeader>Encryption</MenuHeader>
<SettingTile
title="Export E2E room keys"
content={(
<>
<Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
<ExportE2ERoomKeys />
</>
)}
/>
<SettingTile
title="Import E2E room keys"
content={(
<>
<Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
<ImportE2ERoomKeys />
</>
)}
/>
</div>
</div>
);
}
function AboutSection() {
return (
<div className="settings-content set__about">
<div className="set-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div>
<Text variant="h2" weight='medium'>
Cinny
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text>
<Text>Yet another matrix client</Text>
<div className="settings-about">
<div className="settings-about__card">
<MenuHeader>Application</MenuHeader>
<div className="settings-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div>
<Text variant="h2" weight="medium">
Cinny
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text>
<Text>Yet another matrix client</Text>
<div className="set-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
<div className="settings-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
</div>
</div>
</div>
</div>
<div className="set-about__credits">
<Text variant="s1" weight="medium">Credits</Text>
<ul>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
</ul>
<div className="settings-about__card">
<MenuHeader>Credits</MenuHeader>
<div className="settings-about__credits">
<ul>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://material.io/design/sound/sound-resources.html" target="_blank" rel="noreferrer noopener">Material sound resources</a> are © <a href="https://google.com" target="_blank" rel="noreferrer noopener">Google</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
</ul>
</div>
</div>
</div>
);
}
function Settings({ isOpen, onRequestClose }) {
const settingSections = [{
name: 'General',
iconSrc: SettingsIC,
render() {
return <GeneralSection />;
},
}, {
name: 'Appearance',
iconSrc: SunIC,
render() {
return <AppearanceSection />;
},
}, {
name: 'Notifications',
iconSrc: BellIC,
render() {
return <NotificationsSection />;
},
}, {
name: 'Security & Privacy',
iconSrc: LockIC,
render() {
return <SecuritySection />;
},
}, {
name: 'Help & About',
iconSrc: InfoIC,
render() {
return <AboutSection />;
},
}];
const [selectedSection, setSelectedSection] = useState(settingSections[0]);
const tabText = {
APPEARANCE: 'Appearance',
NOTIFICATIONS: 'Notifications',
SECURITY: 'Security',
ABOUT: 'About',
};
const tabItems = [{
text: tabText.APPEARANCE,
iconSrc: SunIC,
disabled: false,
render: () => <AppearanceSection />,
}, {
text: tabText.NOTIFICATIONS,
iconSrc: BellIC,
disabled: false,
render: () => <NotificationsSection />,
}, {
text: tabText.SECURITY,
iconSrc: LockIC,
disabled: false,
render: () => <SecuritySection />,
}, {
text: tabText.ABOUT,
iconSrc: InfoIC,
disabled: false,
render: () => <AboutSection />,
}];
function useWindowToggle(setSelectedTab) {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const openSettings = (tab) => {
const tabItem = tabItems.find((item) => item.text === tab);
if (tabItem) setSelectedTab(tabItem);
setIsOpen(true);
};
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => {
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
};
}, []);
const requestClose = () => setIsOpen(false);
return [isOpen, requestClose];
}
function Settings() {
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
const handleTabChange = (tabItem) => setSelectedTab(tabItem);
const handleLogout = () => {
if (confirm('Confirm logout')) logout();
};
return (
<PopupWindow
className="settings-window"
isOpen={isOpen}
onRequestClose={onRequestClose}
title="Settings"
contentTitle={selectedSection.name}
drawer={(
className="settings-window"
title={<Text variant="s1" weight="medium" primary>Settings</Text>}
contentOptions={(
<>
{
settingSections.map((section) => (
<PWContentSelector
key={section.name}
selected={selectedSection.name === section.name}
onClick={() => setSelectedSection(section)}
iconSrc={section.iconSrc}
>
{section.name}
</PWContentSelector>
))
}
<PWContentSelector
variant="danger"
onClick={handleLogout}
iconSrc={PowerIC}
>
<Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
Logout
</PWContentSelector>
</Button>
<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />
</>
)}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{selectedSection.render()}
{isOpen && (
<div className="settings-window__content">
<ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
<Tabs
items={tabItems}
defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
onSelect={handleTabChange}
/>
<div className="settings-window__cards-wrapper">
{ selectedTab.render() }
</div>
</div>
)}
</PopupWindow>
);
}
Settings.propTypes = {
isOpen: PropTypes.bool.isRequired,
onRequestClose: PropTypes.func.isRequired,
};
export default Settings;
+60 -31
View File
@@ -2,58 +2,87 @@
@use '../../partials/dir';
.settings-window {
& .pw__drawer__content {
@extend .cp-fx__column;
min-height: 100%;
padding-bottom: var(--sp-extra-tight);
& .pw {
background-color: var(--bg-surface-low);
}
.header .btn-danger {
margin: 0 var(--sp-tight);
box-shadow: none;
}
& > .pw-content-selector:last-child {
margin-top: auto;
& .profile-editor {
padding: var(--sp-loose) var(--sp-extra-loose);
}
& .tabs__content {
padding: 0 var(--sp-normal);
}
&__cards-wrapper {
padding: 0 var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
}
}
.settings-window__card {
margin: var(--sp-normal) 0;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
overflow: hidden;
& > .context-menu__header:first-child {
margin-top: 2px;
}
}
.settings-appearance__card,
.settings-notifications,
.settings-security__card,
.settings-security .device-manage,
.settings-about__card {
@extend .settings-window__card;
}
.settings-window__cards-wrapper{
& .setting-tile {
margin: 0 var(--sp-normal);
margin-top: var(--sp-normal);
padding-bottom: 16px;
border-bottom: 1px solid var(--bg-surface-border);
&:last-child {
border-bottom: none;
}
}
& .pw__content-container {
min-height: 100%;
}
}
.settings-content {
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
& .setting-tile {
margin-top: var(--sp-normal);
border-bottom: 1px solid var(--bg-surface-border);
padding-bottom: 16px;
}
}
.set-notifications {
.settings-notifications {
&__not-supported {
padding: 0 var(--sp-ultra-tight);
}
}
.set-about {
.settings-about {
&__branding {
margin-top: var(--sp-extra-tight);
margin-bottom: var(--sp-normal);
padding: var(--sp-normal);
display: flex;
& > div {
margin: 0 var(--sp-loose);
}
}
&__btns {
margin: 0;
margin-top: var(--sp-normal);
& button:last-child {
margin: 0 var(--sp-tight)
& button {
margin-top: var(--sp-tight);
@include dir.side(margin, 0, var(--sp-tight));
}
}
&__credits {
margin-top: var(--sp-loose);
padding: 0 var(--sp-normal);
& ul {
color: var(--tc-surface-low);
padding: var(--sp-normal);
margin: var(--sp-extra-tight) 0;
}
}
@@ -6,6 +6,7 @@ import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/accountData';
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
import { roomIdByAtoZ } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -20,7 +21,6 @@ import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
import { AtoZ } from '../navigation/common';
function ShortcutSpacesContent() {
const mx = initMatrix.matrixClient;
@@ -29,7 +29,7 @@ function ShortcutSpacesContent() {
const [spaceShortcut] = useSpaceShortcut();
const spaceWithoutShortcut = [...spaces].filter(
(spaceId) => !spaceShortcut.includes(spaceId),
).sort(AtoZ);
).sort(roomIdByAtoZ);
const [process, setProcess] = useState(null);
const [selected, setSelected] = useState([]);
@@ -7,14 +7,10 @@
& .room-profile {
padding: var(--sp-loose) var(--sp-extra-loose);
}
}
& .tabs {
box-shadow: inset 0 -1px 0 var(--bg-surface-border);
&__content {
padding: 0 var(--sp-normal);
}
& .tabs__content {
padding: 0 var(--sp-normal);
}
&__cards-wrapper {
+5
View File
@@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react';
import './Client.scss';
import { initHotkeys } from '../../../client/event/hotkeys';
import { initRoomListListener } from '../../../client/event/roomList';
import Text from '../../atoms/text/Text';
import Spinner from '../../atoms/spinner/Spinner';
import Navigation from '../../organisms/navigation/Navigation';
@@ -38,6 +41,8 @@ function Client() {
}, 15000);
initMatrix.once('init_loading_finished', () => {
clearInterval(iId);
initHotkeys();
initRoomListListener(initMatrix.roomList);
changeLoading(false);
});
initMatrix.init();
+11 -1
View File
@@ -95,9 +95,10 @@ export function openProfileViewer(userId, roomId) {
});
}
export function openSettings() {
export function openSettings(tabText) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_SETTINGS,
tabText,
});
}
@@ -149,3 +150,12 @@ export function openReusableContextMenu(placement, cords, render, afterClose) {
afterClose,
});
}
export function openReusableDialog(title, render, afterClose) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_REUSABLE_DIALOG,
title,
render,
afterClose,
});
}
+26
View File
@@ -0,0 +1,26 @@
import initMatrix from '../initMatrix';
// eslint-disable-next-line import/prefer-default-export
export async function markAsRead(roomId) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
if (!room) return;
initMatrix.notifications.deleteNoti(roomId);
const timeline = room.getLiveTimeline().getEvents();
const readEventId = room.getEventReadUpTo(mx.getUserId());
const getLatestValidEvent = () => {
for (let i = timeline.length - 1; i >= 0; i -= 1) {
const latestEvent = timeline[i];
if (latestEvent.getId() === readEventId) return null;
if (!latestEvent.isSending()) return latestEvent;
}
return null;
};
if (timeline.length === 0) return;
const latestEvent = getLatestValidEvent();
if (latestEvent === null) return;
await mx.sendReadReceipt(latestEvent);
}
+6
View File
@@ -36,3 +36,9 @@ export function toggleNotifications() {
type: cons.actions.settings.TOGGLE_NOTIFICATIONS,
});
}
export function toggleNotificationSounds() {
appDispatcher.dispatch({
type: cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS,
});
}
+12 -3
View File
@@ -1,5 +1,6 @@
import { openSearch, toggleRoomSettings } from '../action/navigation';
import navigation from '../state/navigation';
import { markAsRead } from '../action/notifications';
function listenKeyboard(event) {
// Ctrl/Cmd +
@@ -18,11 +19,19 @@ function listenKeyboard(event) {
return;
}
// esc - close room settings panel
if (event.keyCode === 27 && navigation.isRoomSettings) {
toggleRoomSettings();
// esc
if (event.keyCode === 27) {
if (navigation.isRoomSettings) {
toggleRoomSettings();
return;
}
if (navigation.selectedRoomId) {
markAsRead(navigation.selectedRoomId);
return;
}
}
// Don't allow these keys to type/focus message field
if ((event.keyCode !== 8 && event.keyCode < 48)
|| (event.keyCode >= 91 && event.keyCode <= 93)
|| (event.keyCode >= 112 && event.keyCode <= 183)) {
-4
View File
@@ -7,8 +7,6 @@ import RoomList from './state/RoomList';
import AccountData from './state/AccountData';
import RoomsInput from './state/RoomsInput';
import Notifications from './state/Notifications';
import { initHotkeys } from './event/hotkeys';
import { initRoomListListener } from './event/roomList';
global.Olm = require('@matrix-org/olm');
@@ -66,8 +64,6 @@ class InitMatrix extends EventEmitter {
this.accountData = new AccountData(this.roomList);
this.roomsInput = new RoomsInput(this.matrixClient);
this.notifications = new Notifications(this.roomList);
initHotkeys();
initRoomListListener(this.roomList);
this.emit('init_loading_finished');
}
},
+125 -20
View File
@@ -6,6 +6,9 @@ import cons from './cons';
import navigation from './navigation';
import settings from './settings';
import NotificationSound from '../../../public/sound/notification.ogg';
import InviteSound from '../../../public/sound/invite.ogg';
function isNotifEvent(mEvent) {
const eType = mEvent.getType();
if (!cons.supportEventTypes.includes(eType)) return false;
@@ -17,6 +20,17 @@ function isNotifEvent(mEvent) {
return true;
}
function isMutedRule(rule) {
return rule.actions[0] === 'dont_notify' && rule.conditions[0].kind === 'event_match';
}
function findMutedRule(overrideRules, roomId) {
return overrideRules.find((rule) => (
rule.rule_id === roomId
&& isMutedRule(rule)
));
}
class Notifications extends EventEmitter {
constructor(roomList) {
super();
@@ -39,7 +53,9 @@ class Notifications extends EventEmitter {
_initNoti() {
const addNoti = (roomId) => {
const room = this.matrixClient.getRoom(roomId);
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return;
if (this.doesRoomHaveUnread(room) === false) return;
const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight');
this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
@@ -65,6 +81,27 @@ class Notifications extends EventEmitter {
return true;
}
getNotiType(roomId) {
const mx = this.matrixClient;
let pushRule;
try {
pushRule = mx.getRoomPushRule('global', roomId);
} catch {
pushRule = undefined;
}
if (pushRule === undefined) {
const overrideRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
if (overrideRules === undefined) return cons.notifs.DEFAULT;
const isMuted = findMutedRule(overrideRules, roomId);
return isMuted ? cons.notifs.MUTE : cons.notifs.DEFAULT;
}
if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES;
return cons.notifs.MENTIONS_AND_KEYWORDS;
}
getNoti(roomId) {
return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null };
}
@@ -156,7 +193,7 @@ class Notifications extends EventEmitter {
}
async _displayPopupNoti(mEvent, room) {
if (!settings.showNotifications) return;
if (!settings.showNotifications && !settings.isNotificationSounds) return;
const actions = this.matrixClient.getPushActionsForEvent(mEvent);
if (!actions?.notify) return;
@@ -167,34 +204,57 @@ class Notifications extends EventEmitter {
await mEvent.attemptDecryption(this.matrixClient.crypto);
}
let title;
if (!mEvent.sender || room.name === mEvent.sender.name) {
title = room.name;
} else if (mEvent.sender) {
title = `${mEvent.sender.name} (${room.name})`;
if (settings.showNotifications) {
let title;
if (!mEvent.sender || room.name === mEvent.sender.name) {
title = room.name;
} else if (mEvent.sender) {
title = `${mEvent.sender.name} (${room.name})`;
}
const iconSize = 36;
const icon = await renderAvatar({
text: mEvent.sender.name,
bgColor: cssColorMXID(mEvent.getSender()),
imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
size: iconSize,
borderRadius: 8,
scale: 8,
});
const noti = new window.Notification(title, {
body: mEvent.getContent().body,
icon,
silent: settings.isNotificationSounds,
});
if (settings.isNotificationSounds) {
noti.onshow = () => this._playNotiSound();
}
noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
} else {
this._playNotiSound();
}
}
const iconSize = 36;
const icon = await renderAvatar({
text: mEvent.sender.name,
bgColor: cssColorMXID(mEvent.getSender()),
imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
size: iconSize,
borderRadius: 8,
scale: 8,
});
_playNotiSound() {
if (!this._notiAudio) {
this._notiAudio = new Audio(NotificationSound);
}
this._notiAudio.play();
}
const noti = new window.Notification(title, {
body: mEvent.getContent().body,
icon,
});
noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
_playInviteSound() {
if (!this._inviteAudio) {
this._inviteAudio = new Audio(InviteSound);
}
this._inviteAudio.play();
}
_listenEvents() {
this.matrixClient.on('Room.timeline', (mEvent, room) => {
if (room.isSpaceRoom()) return;
if (!isNotifEvent(mEvent)) return;
const liveEvents = room.getLiveTimeline().getEvents();
const lastTimelineEvent = liveEvents[liveEvents.length - 1];
@@ -204,6 +264,11 @@ class Notifications extends EventEmitter {
const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight');
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) {
this.deleteNoti(room.roomId, total ?? 0, highlight ?? 0);
return;
}
this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
if (this.matrixClient.getSyncState() === 'SYNCING') {
@@ -211,6 +276,43 @@ class Notifications extends EventEmitter {
}
});
this.matrixClient.on('accountData', (mEvent, oldMEvent) => {
if (mEvent.getType() === 'm.push_rules') {
const override = mEvent?.getContent()?.global?.override;
const oldOverride = oldMEvent?.getContent()?.global?.override;
if (!override || !oldOverride) return;
const isMuteToggled = (rule, otherOverride) => {
const roomId = rule.rule_id;
const room = this.matrixClient.getRoom(roomId);
if (room === null) return false;
if (room.isSpaceRoom()) return false;
const isMuted = isMutedRule(rule);
if (!isMuted) return false;
const isOtherMuted = findMutedRule(otherOverride, roomId);
if (isOtherMuted) return false;
return true;
};
const mutedRules = override.filter((rule) => isMuteToggled(rule, oldOverride));
const unMutedRules = oldOverride.filter((rule) => isMuteToggled(rule, override));
mutedRules.forEach((rule) => {
this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, true);
this.deleteNoti(rule.rule_id);
});
unMutedRules.forEach((rule) => {
this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, false);
const room = this.matrixClient.getRoom(rule.rule_id);
if (!this.doesRoomHaveUnread(room)) return;
const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight');
this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
});
}
});
this.matrixClient.on('Room.receipt', (mEvent, room) => {
if (mEvent.getType() === 'm.receipt') {
if (room.isSpaceRoom()) return;
@@ -227,6 +329,9 @@ class Notifications extends EventEmitter {
if (membership === 'leave' && this.hasNoti(room.roomId)) {
this.deleteNoti(room.roomId);
}
if (membership === 'invite') {
this._playInviteSound();
}
});
}
}
+1 -12
View File
@@ -77,7 +77,7 @@ function isTimelineLinked(tm1, tm2) {
}
class RoomTimeline extends EventEmitter {
constructor(roomId, notifications) {
constructor(roomId) {
super();
// These are local timelines
this.timeline = [];
@@ -88,7 +88,6 @@ class RoomTimeline extends EventEmitter {
this.matrixClient = initMatrix.matrixClient;
this.roomId = roomId;
this.room = this.matrixClient.getRoom(roomId);
this.notifications = notifications;
this.liveTimeline = this.room.getLiveTimeline();
this.activeTimeline = this.liveTimeline;
@@ -228,16 +227,6 @@ class RoomTimeline extends EventEmitter {
return Promise.allSettled(decryptionPromises);
}
markAllAsRead() {
const readEventId = this.getReadUpToEventId();
this.notifications.deleteNoti(this.roomId);
if (this.timeline.length === 0) return;
const latestEvent = this.timeline[this.timeline.length - 1];
if (readEventId === latestEvent.getId()) return;
this.matrixClient.sendReadReceipt(latestEvent);
this.emit(cons.events.roomTimeline.MARKED_AS_READ, latestEvent);
}
hasEventInTimeline(eventId, timeline = this.activeTimeline) {
const timelineSet = this.getUnfilteredTimelineSet();
const eventTimeline = timelineSet.getTimelineForEvent(eventId);
+2 -2
View File
@@ -97,13 +97,13 @@ function getFormattedBody(markdown) {
function getReplyFormattedBody(roomId, reply) {
const replyToLink = `<a href="https://matrix.to/#/${roomId}/${reply.eventId}">In reply to</a>`;
const userLink = `<a href="https://matrix.to/#/${reply.userId}">${reply.userId}</a>`;
const formattedReply = getFormattedBody(reply.body.replaceAll('\n', '\n> '));
const formattedReply = getFormattedBody(reply.body.replace(/\n/g, '\n> '));
return `<mx-reply><blockquote>${replyToLink}${userLink}<br />${formattedReply}</blockquote></mx-reply>`;
}
function bindReplyToContent(roomId, reply, content) {
const newContent = { ...content };
newContent.body = `> <${reply.userId}> ${reply.body.replaceAll('\n', '\n> ')}`;
newContent.body = `> <${reply.userId}> ${reply.body.replace(/\n/g, '\n> ')}`;
newContent.body += `\n\n${content.body}`;
newContent.format = 'org.matrix.custom.html';
newContent['m.relates_to'] = content['m.relates_to'] || {};
+6 -2
View File
@@ -1,5 +1,5 @@
const cons = {
version: '1.8.0',
version: '1.8.2',
secretKey: {
ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id',
@@ -47,6 +47,7 @@ const cons = {
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
OPEN_SEARCH: 'OPEN_SEARCH',
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
},
room: {
JOIN: 'JOIN',
@@ -67,6 +68,7 @@ const cons = {
TOGGLE_MEMBERSHIP_EVENT: 'TOGGLE_MEMBERSHIP_EVENT',
TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT',
TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS',
TOGGLE_NOTIFICATION_SOUNDS: 'TOGGLE_NOTIFICATION_SOUNDS',
},
},
events: {
@@ -91,6 +93,7 @@ const cons = {
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
SEARCH_OPENED: 'SEARCH_OPENED',
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
@@ -107,6 +110,7 @@ const cons = {
notifications: {
NOTI_CHANGED: 'NOTI_CHANGED',
FULL_READ: 'FULL_READ',
MUTE_TOGGLED: 'MUTE_TOGGLED',
},
roomTimeline: {
READY: 'READY',
@@ -114,7 +118,6 @@ const cons = {
PAGINATED: 'PAGINATED',
TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED',
LIVE_RECEIPT: 'LIVE_RECEIPT',
MARKED_AS_READ: 'MARKED_AS_READ',
EVENT_REDACTED: 'EVENT_REDACTED',
AT_BOTTOM: 'AT_BOTTOM',
SCROLL_TO_LIVE: 'SCROLL_TO_LIVE',
@@ -134,6 +137,7 @@ const cons = {
MEMBERSHIP_EVENTS_TOGGLED: 'MEMBERSHIP_EVENTS_TOGGLED',
NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED',
NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED',
NOTIFICATION_SOUNDS_TOGGLED: 'NOTIFICATION_SOUNDS_TOGGLED',
},
},
};
+11 -2
View File
@@ -129,12 +129,13 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId);
},
[cons.actions.navigation.OPEN_SETTINGS]: () => {
this.emit(cons.events.navigation.SETTINGS_OPENED);
this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText);
},
[cons.actions.navigation.OPEN_EMOJIBOARD]: () => {
this.emit(
cons.events.navigation.EMOJIBOARD_OPENED,
action.cords, action.requestEmojiCallback,
action.cords,
action.requestEmojiCallback,
);
},
[cons.actions.navigation.OPEN_READRECEIPTS]: () => {
@@ -173,6 +174,14 @@ class Navigation extends EventEmitter {
action.afterClose,
);
},
[cons.actions.navigation.OPEN_REUSABLE_DIALOG]: () => {
this.emit(
cons.events.navigation.REUSABLE_DIALOG_OPENED,
action.title,
action.render,
action.afterClose,
);
},
};
actions[action.type]?.();
}
+15
View File
@@ -29,6 +29,7 @@ class Settings extends EventEmitter {
this.hideMembershipEvents = this.getHideMembershipEvents();
this.hideNickAvatarEvents = this.getHideNickAvatarEvents();
this._showNotifications = this.getShowNotifications();
this.isNotificationSounds = this.getIsNotificationSounds();
this.isTouchScreenDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
}
@@ -125,6 +126,15 @@ class Settings extends EventEmitter {
return settings.showNotifications;
}
getIsNotificationSounds() {
if (typeof this.isNotificationSounds === 'boolean') return this.isNotificationSounds;
const settings = getSettings();
if (settings === null) return true;
if (typeof settings.isNotificationSounds === 'undefined') return true;
return settings.isNotificationSounds;
}
setter(action) {
const actions = {
[cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
@@ -164,6 +174,11 @@ class Settings extends EventEmitter {
setSettings('showNotifications', this._showNotifications);
this.emit(cons.events.settings.NOTIFICATIONS_TOGGLED, this._showNotifications);
},
[cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS]: () => {
this.isNotificationSounds = !this.isNotificationSounds;
setSettings('isNotificationSounds', this.isNotificationSounds);
this.emit(cons.events.settings.NOTIFICATION_SOUNDS_TOGGLED, this.isNotificationSounds);
},
};
actions[action.type]?.();
+1 -1
View File
@@ -123,7 +123,7 @@ class AsyncSearch extends EventEmitter {
_normalize(item) {
let myItem = item.normalize(this.normalizeUnicode ? 'NFKC' : 'NFC');
if (!this.isCaseSensitive) myItem = myItem.toLocaleLowerCase();
if (this.ignoreWhitespace) myItem = myItem.replaceAll(' ', '');
if (this.ignoreWhitespace) myItem = myItem.replace(/\s/g, '');
return myItem;
}
+47
View File
@@ -0,0 +1,47 @@
import initMatrix from '../client/initMatrix';
export function roomIdByActivity(id1, id2) {
const room1 = initMatrix.matrixClient.getRoom(id1);
const room2 = initMatrix.matrixClient.getRoom(id2);
return room2.getLastActiveTimestamp() - room1.getLastActiveTimestamp();
}
export function roomIdByAtoZ(aId, bId) {
let aName = initMatrix.matrixClient.getRoom(aId).name;
let bName = initMatrix.matrixClient.getRoom(bId).name;
// remove "#" from the room name
// To ignore it in sorting
aName = aName.replace(/#/g, '');
bName = bName.replace(/#/g, '');
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
export function memberByAtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
export function memberByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
+1 -1
View File
@@ -39,7 +39,7 @@ module.exports = {
use: ['html-loader'],
},
{
test: /\.(png|jpe?g|gif|otf|ttf|woff|woff2)$/,
test: /\.(png|jpe?g|gif|otf|ttf|woff|woff2|ogg)$/,
type: 'asset/resource',
},
{