Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c0836756 | ||
|
|
9b5f71cf6c | ||
|
|
746c5e36bd | ||
|
|
0b12173e74 | ||
|
|
7dd2546b0f | ||
|
|
7c72679806 | ||
|
|
a33178adac | ||
|
|
4a2bb36458 | ||
|
|
3b01cb5329 | ||
|
|
8afea0ca2f | ||
|
|
ba0de8800a | ||
|
|
cd5ae4cb7f | ||
|
|
6575542281 | ||
|
|
bf2559da80 | ||
|
|
fedc207de2 | ||
|
|
0d58478a73 | ||
|
|
abf24d1942 | ||
|
|
afe3f2f3f3 | ||
|
|
1d1cb567da | ||
|
|
699bbee544 | ||
|
|
9c54915e73 | ||
|
|
ed4390b99d | ||
|
|
039d9bae68 | ||
|
|
93ab48ac9a | ||
|
|
c95e312acb | ||
|
|
9279bc7060 | ||
|
|
fe61576dcd | ||
|
|
3f6e3074f2 | ||
|
|
53a8e2aa57 | ||
|
|
44ab6f181c | ||
|
|
3a3a830706 | ||
|
|
1a6e3e73c5 | ||
|
|
7d508e5a7d | ||
|
|
370c224d3a | ||
|
|
7bce501069 | ||
|
|
9d15445eba | ||
|
|
16ee13f1f7 | ||
|
|
8d25eb0acd | ||
|
|
9cb13a91cd | ||
|
|
30f8930773 | ||
|
|
05eaa8d3e0 | ||
|
|
a32ffdf6d4 | ||
|
|
0f97de1b09 | ||
|
|
e8d6ccec9a | ||
|
|
005434f79b | ||
|
|
fe997d8b01 | ||
|
|
7291932a0b | ||
|
|
49ade03a9a | ||
|
|
dd6dbd25da | ||
|
|
50bf90fada | ||
|
|
abb81b6390 | ||
|
|
36da3d3cba | ||
|
|
ae71a99aa4 | ||
|
|
a7034d6351 | ||
|
|
6010b4c252 | ||
|
|
8330f4fba9 | ||
|
|
dc6e153b92 | ||
|
|
a2655ee6a5 | ||
|
|
13248962af | ||
|
|
b698982186 | ||
|
|
5a299b21c5 | ||
|
|
bb90f11ec8 | ||
|
|
12369ba2ec | ||
|
|
f5720bde14 |
No files matched your search
@@ -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.
|
||||
@@ -0,0 +1,57 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Report a 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.
|
||||
@@ -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
|
||||
@@ -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,33 @@
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea
|
||||
|
||||
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.
|
||||
@@ -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)
|
||||
|
||||
+14
-12
@@ -1,28 +1,30 @@
|
||||
# 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
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
open-pull-requests-limit: 15
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
open-pull-requests-limit: 5
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
env:
|
||||
PR_NUMBER: ${{github.event.number}}
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Build app
|
||||
run: npm ci && npm run build
|
||||
@@ -32,8 +32,3 @@ jobs:
|
||||
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
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
# 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'
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v6.0.0
|
||||
with:
|
||||
script: |
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
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'
|
||||
- name: Read PR Info
|
||||
id: readctx
|
||||
uses: actions/github-script@v6.0.0
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
name: 'Docker check'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PR_NUMBER: ${{github.event.number}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v2.10.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -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
|
||||
- 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 }}
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
name: 'Deploy to Netlify'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Build and deploy to Netlify
|
||||
uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Create tar.gz
|
||||
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
||||
- name: Upload tagged release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
with:
|
||||
files: |
|
||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1.14.1
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3.6.2
|
||||
uses: docker/metadata-action@v3.7.0
|
||||
with:
|
||||
images: ajbura/cinny
|
||||
- name: Build and push Docker image
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
## Builder
|
||||
FROM node:17.7.1-alpine3.15 as builder
|
||||
FROM node:17.8.0-alpine3.15 as builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+648
-939
File diff suppressed because it is too large.
Load diff
+15
-15
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
@@ -15,8 +15,8 @@
|
||||
"author": "Ajay Bura",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.5",
|
||||
"@fontsource/roboto": "^4.5.3",
|
||||
"@fontsource/inter": "^4.5.7",
|
||||
"@fontsource/roboto": "^4.5.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
@@ -26,9 +26,9 @@
|
||||
"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",
|
||||
"matrix-js-sdk": "^17.0.0",
|
||||
"micromark": "^3.0.10",
|
||||
"micromark-extension-gfm": "^2.0.1",
|
||||
"micromark-util-chunked": "^1.0.0",
|
||||
@@ -37,21 +37,21 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-autosize-textarea": "^7.1.0",
|
||||
"react-dnd": "^15.1.1",
|
||||
"react-dnd-html5-backend": "^15.1.2",
|
||||
"react-dnd": "^15.1.2",
|
||||
"react-dnd-html5-backend": "^15.1.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-modal": "^3.14.4",
|
||||
"sanitize-html": "^2.7.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twemoji": "^14.0.1"
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.7",
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"assert": "^2.0.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-loader": "^8.2.4",
|
||||
"browserify-fs": "^1.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
@@ -59,9 +59,9 @@
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
@@ -71,15 +71,15 @@
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sass": "^1.49.9",
|
||||
"sass": "^1.50.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.70.0",
|
||||
"webpack": "^5.72.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4",
|
||||
"webpack-dev-server": "^4.8.1",
|
||||
"webpack-merge": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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:
|
||||
@@ -539,7 +547,7 @@ const MessageOptions = React.memo(({
|
||||
variant="danger"
|
||||
iconSrc={BinIC}
|
||||
onClick={() => {
|
||||
if (window.confirm('Are you sure you want to delete this event')) {
|
||||
if (window.confirm('Are you sure that you want to delete this event?')) {
|
||||
redactEvent(roomId, mEvent.getId());
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use '../../partials/dir';
|
||||
|
||||
.pw-model {
|
||||
--modal-height: 656px;
|
||||
--modal-height: 774px;
|
||||
max-height: var(--modal-height) !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -17,19 +17,19 @@ const visibility = {
|
||||
|
||||
const items = [{
|
||||
iconSrc: null,
|
||||
text: 'World readable (anyone can read)',
|
||||
text: 'Anyone (including guests)',
|
||||
type: visibility.WORLD_READABLE,
|
||||
}, {
|
||||
iconSrc: null,
|
||||
text: 'Member shared (since the point in time of selecting this option)',
|
||||
text: 'Members (all messages)',
|
||||
type: visibility.SHARED,
|
||||
}, {
|
||||
iconSrc: null,
|
||||
text: 'Member invited (since they were invited)',
|
||||
text: 'Members (messages after invite)',
|
||||
type: visibility.INVITED,
|
||||
}, {
|
||||
iconSrc: null,
|
||||
text: 'Member joined (since they joined)',
|
||||
text: 'Members (messages after join)',
|
||||
type: visibility.JOINED,
|
||||
}];
|
||||
|
||||
@@ -87,7 +87,7 @@ function RoomHistoryVisibility({ roomId }) {
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
<Text variant="b3">Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.</Text>
|
||||
<Text variant="b3">Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect.</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,12 @@ const items = [{
|
||||
function setRoomNotifType(roomId, newType) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const { notifications } = initMatrix;
|
||||
const roomPushRule = mx.getRoomPushRule('global', roomId);
|
||||
let roomPushRule;
|
||||
try {
|
||||
roomPushRule = mx.getRoomPushRule('global', roomId);
|
||||
} catch {
|
||||
roomPushRule = undefined;
|
||||
}
|
||||
const promises = [];
|
||||
|
||||
if (newType === cons.notifs.MUTE) {
|
||||
|
||||
@@ -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 = () => {
|
||||
@@ -31,7 +30,7 @@ function RoomOptions({ roomId, afterOptionSelect }) {
|
||||
afterOptionSelect();
|
||||
};
|
||||
const handleLeaveClick = () => {
|
||||
if (confirm('Are you really want to leave this room?')) {
|
||||
if (confirm('Are you sure that you want to leave this room?')) {
|
||||
roomActions.leave(roomId);
|
||||
afterOptionSelect();
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ function RoomPermissions({ roomId }) {
|
||||
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
||||
const permissions = pLEvent.getContent();
|
||||
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
||||
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
|
||||
|
||||
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
|
||||
const handlePowerLevelChange = (newPowerLevel) => {
|
||||
@@ -208,7 +209,7 @@ function RoomPermissions({ roomId }) {
|
||||
(closeMenu) => (
|
||||
<PowerLevelSelector
|
||||
value={powerLevel}
|
||||
max={100}
|
||||
max={myPowerLevel}
|
||||
onSelect={(pl) => {
|
||||
closeMenu();
|
||||
handlePowerLevelChange(pl);
|
||||
|
||||
@@ -117,7 +117,7 @@ function RoomProfile({ roomId }) {
|
||||
|
||||
const handleAvatarUpload = async (url) => {
|
||||
if (url === null) {
|
||||
if (confirm('Are you sure you want to remove avatar?')) {
|
||||
if (confirm('Are you sure that you want to remove room avatar?')) {
|
||||
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
}
|
||||
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -55,7 +55,7 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||
};
|
||||
|
||||
const handleLeaveClick = () => {
|
||||
if (confirm('Are you really want to leave this space?')) {
|
||||
if (confirm('Are you sure that you want to leave this space?')) {
|
||||
leave(roomId);
|
||||
afterOptionSelect();
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
|
||||
/>
|
||||
)}
|
||||
content={(
|
||||
<Text variant="b3">Override the default (100) power level.</Text>
|
||||
<Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</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 > * {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
@@ -73,26 +69,36 @@ function Home({ spaceId }) {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function ProfileAvatarMenu() {
|
||||
return (
|
||||
<SidebarAvatar
|
||||
onClick={openSettings}
|
||||
tooltip={profile.displayName}
|
||||
tooltip="Settings"
|
||||
avatar={(
|
||||
<Avatar
|
||||
text={profile.displayName}
|
||||
|
||||
@@ -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,37 +1,46 @@
|
||||
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?')) {
|
||||
if (confirm('Are you sure that you want to remove avatar?')) {
|
||||
mx.setAvatarUrl('');
|
||||
setAvatarSrc(null);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ function Room() {
|
||||
roomInfo.roomTimeline?.removeInternalListeners();
|
||||
if (mx.getRoom(rId)) {
|
||||
setRoomInfo({
|
||||
roomTimeline: new RoomTimeline(rId, initMatrix.notifications),
|
||||
roomTimeline: new RoomTimeline(rId),
|
||||
eventId: eId ?? null,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -86,7 +86,7 @@ function GeneralSettings({ roomId }) {
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (confirm('Are you really want to leave this room?')) {
|
||||
if (confirm('Are you sure that you want to leave this room?')) {
|
||||
roomActions.leave(roomId);
|
||||
}
|
||||
}}
|
||||
@@ -123,7 +123,7 @@ function SecuritySettings({ roomId }) {
|
||||
<RoomEncryption roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Message history visibility (Who can read history)</MenuHeader>
|
||||
<MenuHeader>Message history visibility</MenuHeader>
|
||||
<RoomHistoryVisibility roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
&__info {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
@include dir.side(margin, 10px, 14px);
|
||||
@include dir.side(margin, 14px, 10px);
|
||||
|
||||
& > * {
|
||||
margin: auto;
|
||||
|
||||
@@ -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';
|
||||
@@ -62,7 +63,7 @@ function genRoomIntro(mEvent, roomTimeline) {
|
||||
avatarSrc={avatarSrc}
|
||||
name={roomTimeline.room.name}
|
||||
heading={`Welcome to ${roomTimeline.room.name}`}
|
||||
desc={`This is the beginning of ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
|
||||
desc={`This is the beginning of the ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
|
||||
time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
|
||||
/>
|
||||
);
|
||||
@@ -253,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();
|
||||
@@ -263,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,7 +287,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
|
||||
const limit = eventLimitRef.current;
|
||||
const trySendReadReceipt = (event) => {
|
||||
if (myUserId === event.getSender()) {
|
||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
return;
|
||||
}
|
||||
const readUpToEvent = readUptoEvtStore.getItem();
|
||||
@@ -295,7 +296,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
|
||||
|
||||
if (isUnread === false) {
|
||||
if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
|
||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
} else {
|
||||
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||
}
|
||||
@@ -305,7 +306,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
|
||||
const { timeline } = roomTimeline;
|
||||
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
|
||||
if (unreadMsgIsLast) {
|
||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
||||
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -399,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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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([]);
|
||||
|
||||
@@ -90,7 +90,7 @@ function GeneralSettings({ roomId }) {
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (confirm('Are you really want to leave this space?')) {
|
||||
if (confirm('Are you sure that you want to leave this space?')) {
|
||||
leave(roomId);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -80,7 +83,12 @@ class Notifications extends EventEmitter {
|
||||
|
||||
getNotiType(roomId) {
|
||||
const mx = this.matrixClient;
|
||||
const pushRule = mx.getRoomPushRule('global', roomId);
|
||||
let pushRule;
|
||||
try {
|
||||
pushRule = mx.getRoomPushRule('global', roomId);
|
||||
} catch {
|
||||
pushRule = undefined;
|
||||
}
|
||||
|
||||
if (pushRule === undefined) {
|
||||
const overrideRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
|
||||
@@ -185,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;
|
||||
@@ -196,28 +204,50 @@ 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() {
|
||||
@@ -299,6 +329,9 @@ class Notifications extends EventEmitter {
|
||||
if (membership === 'leave' && this.hasNoti(room.roomId)) {
|
||||
this.deleteNoti(room.roomId);
|
||||
}
|
||||
if (membership === 'invite') {
|
||||
this._playInviteSound();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ class RoomList extends EventEmitter {
|
||||
this.matrixClient.getRooms().forEach((room) => {
|
||||
const { roomId } = room;
|
||||
const tombstone = room.currentState.events.get('m.room.tombstone');
|
||||
if (typeof tombstone !== 'undefined') {
|
||||
if (tombstone?.get('') !== undefined) {
|
||||
const repRoomId = tombstone.get('').getContent().replacement_room;
|
||||
const repRoomMembership = this.matrixClient.getRoom(repRoomId)?.getMyMembership();
|
||||
if (repRoomMembership === 'join') return;
|
||||
|
||||
@@ -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,25 +227,6 @@ class RoomTimeline extends EventEmitter {
|
||||
return Promise.allSettled(decryptionPromises);
|
||||
}
|
||||
|
||||
markAllAsRead() {
|
||||
const readEventId = this.getReadUpToEventId();
|
||||
const getLatestValidEvent = () => {
|
||||
for (let i = this.timeline.length - 1; i >= 0; i -= 1) {
|
||||
const latestEvent = this.timeline[i];
|
||||
if (latestEvent.getId() === readEventId) return null;
|
||||
if (!latestEvent.isSending()) return latestEvent;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
this.notifications.deleteNoti(this.roomId);
|
||||
if (this.timeline.length === 0) return;
|
||||
const latestEvent = getLatestValidEvent();
|
||||
if (latestEvent === null) return;
|
||||
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);
|
||||
|
||||
@@ -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'] || {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const cons = {
|
||||
version: '1.8.1',
|
||||
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',
|
||||
@@ -115,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',
|
||||
@@ -135,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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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]?.();
|
||||
}
|
||||
|
||||
@@ -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]?.();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user