Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b70c7e490 | ||
|
|
0539836714 | ||
|
|
c08b0e654b | ||
|
|
b3cb48319a | ||
|
|
44553cc375 | ||
|
|
fbe287a702 | ||
|
|
5863dcdf67 | ||
|
|
f77bee25ef | ||
|
|
c11328a064 | ||
|
|
d04de2fba0 | ||
|
|
d2b435618c | ||
|
|
7525bb78e5 | ||
|
|
2075a572fe | ||
|
|
73723ba6ba | ||
|
|
0791820a6c | ||
|
|
931f352873 | ||
|
|
7c7d2e0fa4 | ||
|
|
3372fb6f74 | ||
|
|
bc856269ff | ||
|
|
06bae231ef | ||
|
|
65a0edc3a6 | ||
|
|
b7c322d473 | ||
|
|
0776a04362 | ||
|
|
e51fc5a585 | ||
|
|
3afc068a02 | ||
|
|
5cdad44abf | ||
|
|
43762df998 | ||
|
|
95228c6dd9 | ||
|
|
205fcf8487 | ||
|
|
336e8921ee | ||
|
|
ef149b9fcf | ||
|
|
766b4c13c3 | ||
|
|
f5605258e3 | ||
|
|
2ba4d2f2b7 | ||
|
|
2e050c066e | ||
|
|
3f83514427 | ||
|
|
8c227843c9 | ||
|
|
ba084c0a10 | ||
|
|
3fdd42706d | ||
|
|
b49b51a671 | ||
|
|
e5bb386dd2 | ||
|
|
2867bb3bc3 | ||
|
|
416fd02069 | ||
|
|
581963cfb4 | ||
|
|
3b14543e66 | ||
|
|
5dd2194eba | ||
|
|
5b7880f817 | ||
|
|
bafe1c5781 | ||
|
|
d760be58c3 | ||
|
|
3da9b70632 | ||
|
|
b7c5902f67 | ||
|
|
9a22b25564 | ||
|
|
44c3dec9dc | ||
|
|
87f3afd8fd | ||
|
|
53f1129242 | ||
|
|
74216f75e2 | ||
|
|
ed8eca0c1d | ||
|
|
dc8e6e53c7 | ||
|
|
989ab5a432 | ||
|
|
ec26c03d58 | ||
|
|
3b1b3387e7 | ||
|
|
62c03d1334 | ||
|
|
51e12184d7 | ||
|
|
8c01eb9c00 | ||
|
|
bf264d5add | ||
|
|
8b3bd38bad | ||
|
|
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 | ||
|
|
0d59a4de48 | ||
|
|
73c74d0477 | ||
|
|
70ffd7ded8 | ||
|
|
92a3a8d6fa | ||
|
|
6e9cd02b2b | ||
|
|
d0f90af251 | ||
|
|
c53eb27c6f | ||
|
|
38773e89ff | ||
|
|
19cb30d360 | ||
|
|
50a734b977 | ||
|
|
b988758ac2 | ||
|
|
2567328e8b | ||
|
|
a7568fcbbf | ||
|
|
27a06ae90c | ||
|
|
211fd19031 | ||
|
|
fe18611b4b | ||
|
|
5e9b45ad5f | ||
|
|
af833daee4 | ||
|
|
1ad5317d6e | ||
|
|
7cf5df80ce | ||
|
|
d6b880d110 | ||
|
|
cf58a4376e | ||
|
|
a76dcb289a | ||
|
|
48ec6224e7 | ||
|
|
127dd8baf4 | ||
|
|
d25c3ff4fc | ||
|
|
f0e9de4cf9 | ||
|
|
92607a788e | ||
|
|
82948c1f55 |
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
|
||||||
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -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.
|
||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
contact_links:
|
||||||
|
- name: 💬 Matrix Chat
|
||||||
|
url: https://matrix.to/#/#cinny:matrix.org
|
||||||
|
about: Ask questions and talk to other Cinny users and the maintainers
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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.
|
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -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.
|
||||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,15 +1,13 @@
|
|||||||
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
|
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
|
||||||
|
|
||||||
### Description
|
### 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
|
#### Type of change
|
||||||
|
|
||||||
Please delete options that are not relevant.
|
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
|||||||
36
.github/dependabot.yml
vendored
36
.github/dependabot.yml
vendored
@@ -1,22 +1,30 @@
|
|||||||
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
|
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: "tuesday"
|
||||||
|
time: "01:00"
|
||||||
|
timezone: "Asia/Kolkata"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: /
|
directory: /
|
||||||
schedule: {interval: weekly}
|
schedule:
|
||||||
reviewers: [ajbura]
|
interval: weekly
|
||||||
assignees: [ajbura]
|
day: "tuesday"
|
||||||
|
time: "01:00"
|
||||||
|
timezone: "Asia/Kolkata"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: /
|
directory: /
|
||||||
schedule: {interval: weekly}
|
schedule:
|
||||||
reviewers: [ajbura]
|
interval: weekly
|
||||||
assignees: [ajbura]
|
day: "tuesday"
|
||||||
|
time: "01:00"
|
||||||
- package-ecosystem: npm
|
timezone: "Asia/Kolkata"
|
||||||
directory: /
|
open-pull-requests-limit: 5
|
||||||
schedule: {interval: weekly}
|
|
||||||
reviewers: [ajbura]
|
|
||||||
assignees: [ajbura]
|
|
||||||
|
|||||||
54
.github/workflows/build-pull-request.yml
vendored
54
.github/workflows/build-pull-request.yml
vendored
@@ -1,32 +1,34 @@
|
|||||||
name: 'Build PR'
|
name: 'Build pull request'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: ['opened', 'synchronize']
|
types: ['opened', 'synchronize']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-pull-request:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{github.event.number}}
|
PR_NUMBER: ${{github.event.number}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.0.0
|
- name: Checkout repository
|
||||||
- name: Build
|
uses: actions/checkout@v3.0.2
|
||||||
run: npm ci && npm run build
|
- name: Build app
|
||||||
- name: Upload Artifact
|
run: npm ci && npm run build
|
||||||
uses: actions/upload-artifact@v3.0.0
|
- name: Upload artifact
|
||||||
with:
|
uses: actions/upload-artifact@v3.0.0
|
||||||
name: previewbuild
|
with:
|
||||||
path: dist
|
name: previewbuild
|
||||||
retention-days: 1
|
path: dist
|
||||||
- uses: actions/github-script@v6.0.0
|
retention-days: 1
|
||||||
with:
|
- name: Get PR info
|
||||||
script: |
|
uses: actions/github-script@v6.0.0
|
||||||
var fs = require('fs');
|
with:
|
||||||
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
script: |
|
||||||
- name: Upload PR Info
|
var fs = require('fs');
|
||||||
uses: actions/upload-artifact@v3.0.0
|
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
||||||
with:
|
- name: Upload PR Info
|
||||||
name: pr.json
|
uses: actions/upload-artifact@v3.0.0
|
||||||
path: pr.json
|
with:
|
||||||
retention-days: 1
|
name: pr.json
|
||||||
|
path: pr.json
|
||||||
|
retention-days: 1
|
||||||
|
|||||||
150
.github/workflows/deploy-pull-request.yml
vendored
150
.github/workflows/deploy-pull-request.yml
vendored
@@ -1,78 +1,78 @@
|
|||||||
name: Upload Preview Build to Netlify
|
name: Upload Preview Build to Netlify
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Build PR"]
|
workflows: ["Build pull request"]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
get-build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: >
|
||||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
# There's a 'download artifact' action but it hasn't been updated for the
|
# 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)
|
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
|
||||||
# so instead we get this mess:
|
# so instead we get this mess:
|
||||||
- name: 'Download artifact'
|
- name: Download artifact
|
||||||
uses: actions/github-script@v6.0.0
|
uses: actions/github-script@v6.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
run_id: ${{github.event.workflow_run.id }},
|
run_id: ${{github.event.workflow_run.id }},
|
||||||
});
|
});
|
||||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
return artifact.name == "previewbuild"
|
return artifact.name == "previewbuild"
|
||||||
})[0];
|
})[0];
|
||||||
var download = await github.rest.actions.downloadArtifact({
|
var download = await github.rest.actions.downloadArtifact({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
artifact_id: matchArtifact.id,
|
artifact_id: matchArtifact.id,
|
||||||
archive_format: 'zip',
|
archive_format: 'zip',
|
||||||
});
|
});
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
||||||
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
|
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
return artifact.name == "pr.json"
|
return artifact.name == "pr.json"
|
||||||
})[0];
|
})[0];
|
||||||
var download = await github.rest.actions.downloadArtifact({
|
var download = await github.rest.actions.downloadArtifact({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
artifact_id: prInfoArtifact.id,
|
artifact_id: prInfoArtifact.id,
|
||||||
archive_format: 'zip',
|
archive_format: 'zip',
|
||||||
});
|
});
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
|
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
|
||||||
- name: Extract Artifacts
|
- name: Extract Artifacts
|
||||||
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
|
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
|
id: readctx
|
||||||
uses: actions/github-script@v6.0.0
|
uses: actions/github-script@v6.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
|
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
|
||||||
console.log(`::set-output name=prnumber::${pr.number}`);
|
console.log(`::set-output name=prnumber::${pr.number}`);
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: netlify
|
id: netlify
|
||||||
uses: nwtgck/actions-netlify@v1.2.3
|
uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: "Deploy from GitHub Actions"
|
deploy-message: "Deploy from GitHub Actions"
|
||||||
# These don't work because we're in workflow_run
|
# These don't work because we're in workflow_run
|
||||||
enable-pull-request-comment: false
|
enable-pull-request-comment: false
|
||||||
enable-commit-comment: false
|
enable-commit-comment: false
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
- name: Edit PR Description
|
- name: Edit PR Description
|
||||||
uses: velas/pr-description@v1.0.1
|
uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
||||||
description-message: |
|
description-message: |
|
||||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||||
|
|||||||
21
.github/workflows/docker-pr.yml
vendored
Normal file
21
.github/workflows/docker-pr.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: 'Docker check'
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'Dockerfile'
|
||||||
|
- '.github/workflows/docker-pr.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{github.event.number}}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3.0.2
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v2.10.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
34
.github/workflows/docker.yaml
vendored
34
.github/workflows/docker.yaml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Publish Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_registry:
|
|
||||||
name: Push Docker image to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v3.0.0
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v1.14.1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v3.6.2
|
|
||||||
with:
|
|
||||||
images: ajbura/cinny
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v2.9.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
@@ -6,13 +6,15 @@ on:
|
|||||||
- dev
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy-to-netlify:
|
||||||
name: 'Deploy'
|
name: 'Deploy'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.0.0
|
- name: Checkout repository
|
||||||
- uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
uses: actions/checkout@v3.0.2
|
||||||
|
- name: Build and deploy to Netlify
|
||||||
|
uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
|
||||||
with:
|
with:
|
||||||
install_command: "npm ci"
|
install_command: "npm ci"
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
21
.github/workflows/netlify-prod.yaml
vendored
21
.github/workflows/netlify-prod.yaml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: 'Deploy to Netlify (prod)'
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: 'Deploy'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3.0.0
|
|
||||||
- uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
|
||||||
with:
|
|
||||||
install_command: "npm ci"
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
|
||||||
BUILD_DIRECTORY: "dist"
|
|
||||||
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
|
|
||||||
NETLIFY_DEPLOY_TO_PROD: true
|
|
||||||
64
.github/workflows/prod-deploy.yml
vendored
Normal file
64
.github/workflows/prod-deploy.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: 'Production deploy'
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-to-netlify:
|
||||||
|
name: 'Deploy to Netlify'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3.0.2
|
||||||
|
- name: Build and deploy to Netlify
|
||||||
|
uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
|
||||||
|
with:
|
||||||
|
install_command: "npm ci"
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||||
|
BUILD_DIRECTORY: "dist"
|
||||||
|
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
|
||||||
|
NETLIFY_DEPLOY_TO_PROD: true
|
||||||
|
- name: Get version from tag
|
||||||
|
id: vars
|
||||||
|
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||||
|
- name: Create tar.gz
|
||||||
|
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
||||||
|
- name: Sign tar.gz
|
||||||
|
uses: actionhippie/gpgsign@4e28208b142cae93e1582401dcda1cf79e4f72c0
|
||||||
|
with:
|
||||||
|
private_key: ${{ secrets.GNUPG_KEY }}
|
||||||
|
passphrase: ${{ secrets.GNUPG_PASSPHRASE }}
|
||||||
|
detach_sign: true
|
||||||
|
files: cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
|
- name: Upload tagged release
|
||||||
|
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
|
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
|
||||||
|
|
||||||
|
push_to_dockerhub:
|
||||||
|
name: Push Docker image to Docker Hub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3.0.2
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1.14.1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3.8.0
|
||||||
|
with:
|
||||||
|
images: ajbura/cinny
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v2.10.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
## Builder
|
## Builder
|
||||||
FROM node:17.6.0-alpine3.15 as builder
|
FROM node:17.9.0-alpine3.15 as builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY package.json package-lock.json /src
|
COPY package.json package-lock.json /src/
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . /src
|
COPY . /src/
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Ajay Bura (ajbura) and other contributors
|
Copyright (c) 2021 Ajay Bura (ajbura)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Cinny
|
# Cinny
|
||||||
|
|
||||||
|
[](https://github.com/ajbura/cinny/tree/dev)
|
||||||
|
[](https://matrix.to/#/#cinny:matrix.org)
|
||||||
|
[](https://twitter.com/@cinnyapp)
|
||||||
|
[](https://opencollective.com/cinny)
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [About](#about)
|
- [About](#about)
|
||||||
@@ -11,7 +16,7 @@
|
|||||||
|
|
||||||
Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, elegant and secure interface.
|
Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, elegant and secure interface.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Building and Running
|
## Building and Running
|
||||||
|
|
||||||
@@ -59,7 +64,7 @@ To set default Homeserver on login and register page, place a customized [`confi
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2021 Ajay Bura (ajbura) and contributors
|
Copyright (c) 2021 Ajay Bura (ajbura)
|
||||||
|
|
||||||
Code licensed under the MIT License: <http://opensource.org/licenses/MIT>
|
Code licensed under the MIT License: <http://opensource.org/licenses/MIT>
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
"kde.org",
|
"kde.org",
|
||||||
"matrix.org",
|
"matrix.org",
|
||||||
"chat.mozilla.org"
|
"chat.mozilla.org"
|
||||||
]
|
],
|
||||||
|
"allowCustomHomeservers": true
|
||||||
}
|
}
|
||||||
2382
package-lock.json
generated
2382
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cinny",
|
"name": "cinny",
|
||||||
"version": "1.8.0",
|
"version": "2.0.1",
|
||||||
"description": "Yet another matrix client",
|
"description": "Yet another matrix client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
"author": "Ajay Bura",
|
"author": "Ajay Bura",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^4.5.4",
|
"@fontsource/inter": "^4.5.10",
|
||||||
"@fontsource/roboto": "^4.5.3",
|
"@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",
|
"@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",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
@@ -26,60 +26,62 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"flux": "^4.0.3",
|
"flux": "^4.0.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"html-react-parser": "^1.4.8",
|
"html-react-parser": "^1.4.12",
|
||||||
|
"katex": "^0.15.3",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"matrix-js-sdk": "^15.6.0",
|
"matrix-js-sdk": "^17.1.0",
|
||||||
"micromark": "^3.0.10",
|
"micromark": "^3.0.10",
|
||||||
"micromark-extension-gfm": "^2.0.1",
|
"micromark-extension-gfm": "^2.0.1",
|
||||||
|
"micromark-extension-math": "^2.0.2",
|
||||||
"micromark-util-chunked": "^1.0.0",
|
"micromark-util-chunked": "^1.0.0",
|
||||||
"micromark-util-resolve-all": "^1.0.0",
|
"micromark-util-resolve-all": "^1.0.0",
|
||||||
"micromark-util-symbol": "^1.0.1",
|
"micromark-util-symbol": "^1.0.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-autosize-textarea": "^7.1.0",
|
"react-autosize-textarea": "^7.1.0",
|
||||||
"react-dnd": "^15.1.1",
|
"react-dnd": "^15.1.2",
|
||||||
"react-dnd-html5-backend": "^15.1.2",
|
"react-dnd-html5-backend": "^15.1.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-google-recaptcha": "^2.1.0",
|
"react-google-recaptcha": "^2.1.0",
|
||||||
"react-modal": "^3.14.4",
|
"react-modal": "^3.15.1",
|
||||||
"sanitize-html": "^2.7.0",
|
"sanitize-html": "^2.7.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"twemoji": "^13.1.0"
|
"twemoji": "^14.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.5",
|
"@babel/core": "^7.17.10",
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@babel/preset-env": "^7.17.10",
|
||||||
"@babel/preset-react": "^7.16.7",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.5",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"copy-webpack-plugin": "^10.2.4",
|
"copy-webpack-plugin": "^10.2.4",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"css-loader": "^6.7.0",
|
"css-loader": "^6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"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-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-react": "^7.29.3",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.5.0",
|
||||||
"favicons": "^6.2.2",
|
"favicons": "^6.2.2",
|
||||||
"favicons-webpack-plugin": "^5.0.2",
|
"favicons-webpack-plugin": "^5.0.2",
|
||||||
"html-loader": "^3.1.0",
|
"html-loader": "^3.1.0",
|
||||||
"html-webpack-plugin": "^5.3.1",
|
"html-webpack-plugin": "^5.3.1",
|
||||||
"mini-css-extract-plugin": "^2.6.0",
|
"mini-css-extract-plugin": "^2.6.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.51.0",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"util": "^0.12.4",
|
"util": "^0.12.4",
|
||||||
"webpack": "^5.70.0",
|
"webpack": "^5.72.0",
|
||||||
"webpack-cli": "^4.9.2",
|
"webpack-cli": "^4.9.2",
|
||||||
"webpack-dev-server": "^4.7.4",
|
"webpack-dev-server": "^4.9.0",
|
||||||
"webpack-merge": "^5.7.3"
|
"webpack-merge": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,11 @@
|
|||||||
</head>
|
</head>
|
||||||
<body id="appBody">
|
<body id="appBody">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<audio id="notificationSound">
|
||||||
|
<source src="./sound/notification.ogg" type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
<audio id="inviteSound">
|
||||||
|
<source src="./sound/invite.ogg" type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
15
public/res/ic/outlined/message-unread.svg
Normal file
15
public/res/ic/outlined/message-unread.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
|
||||||
|
<g>
|
||||||
|
<circle fill="#010101" cx="19" cy="6" r="4"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#010101" d="M13.3,8H7v2h7.5C14,9.4,13.6,8.7,13.3,8z"/>
|
||||||
|
<path fill="#010101" d="M19,12v5.6l-2.4-1.3L16.1,16h-0.5H5V6h8c0-0.7,0.1-1.4,0.3-2H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2
|
||||||
|
h10.8l5.4,3v-9.3C20.4,11.9,19.7,12,19,12z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 811 B |
12
public/res/ic/outlined/message.svg
Normal file
12
public/res/ic/outlined/message.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#010101" d="M19,6v11.6l-2.4-1.3L16.1,16h-0.5H5V6H19 M19.2,4H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2h10.8l5.4,3V6
|
||||||
|
C21,4.9,20.2,4,19.2,4L19.2,4z"/>
|
||||||
|
<rect x="7" y="8" fill="#010101" width="10" height="2"/>
|
||||||
|
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 732 B |
17
public/res/ic/outlined/recent-clock.svg
Normal file
17
public/res/ic/outlined/recent-clock.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?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>
|
||||||
|
<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 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>
|
||||||
|
After Width: | Height: | Size: 985 B |
BIN
public/sound/invite.ogg
Executable file
BIN
public/sound/invite.ogg
Executable file
Binary file not shown.
BIN
public/sound/notification.ogg
Executable file
BIN
public/sound/notification.ogg
Executable file
Binary file not shown.
@@ -11,11 +11,12 @@ const IconButton = React.forwardRef(({
|
|||||||
variant, size, type,
|
variant, size, type,
|
||||||
tooltip, tooltipPlacement, src,
|
tooltip, tooltipPlacement, src,
|
||||||
onClick, tabIndex, disabled, isImage,
|
onClick, tabIndex, disabled, isImage,
|
||||||
|
className,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const btn = (
|
const btn = (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`ic-btn ic-btn-${variant}`}
|
className={`ic-btn ic-btn-${variant} ${className}`}
|
||||||
onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)}
|
onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
// eslint-disable-next-line react/button-has-type
|
// eslint-disable-next-line react/button-has-type
|
||||||
@@ -47,6 +48,7 @@ IconButton.defaultProps = {
|
|||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
isImage: false,
|
isImage: false,
|
||||||
|
className: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
IconButton.propTypes = {
|
IconButton.propTypes = {
|
||||||
@@ -60,6 +62,7 @@ IconButton.propTypes = {
|
|||||||
tabIndex: PropTypes.number,
|
tabIndex: PropTypes.number,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
isImage: PropTypes.bool,
|
isImage: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IconButton;
|
export default IconButton;
|
||||||
|
|||||||
59
src/app/atoms/card/InfoCard.jsx
Normal file
59
src/app/atoms/card/InfoCard.jsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './InfoCard.scss';
|
||||||
|
|
||||||
|
import Text from '../text/Text';
|
||||||
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
|
import IconButton from '../button/IconButton';
|
||||||
|
|
||||||
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
|
function InfoCard({
|
||||||
|
className, style,
|
||||||
|
variant, iconSrc,
|
||||||
|
title, content,
|
||||||
|
rounded, requestClose,
|
||||||
|
}) {
|
||||||
|
const classes = [`info-card info-card--${variant}`];
|
||||||
|
if (rounded) classes.push('info-card--rounded');
|
||||||
|
if (className) classes.push(className);
|
||||||
|
return (
|
||||||
|
<div className={classes.join(' ')} style={style}>
|
||||||
|
{iconSrc && (
|
||||||
|
<div className="info-card__icon">
|
||||||
|
<RawIcon color={`var(--ic-${variant}-high)`} src={iconSrc} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="info-card__content">
|
||||||
|
<Text>{title}</Text>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
{requestClose && (
|
||||||
|
<IconButton src={CrossIC} variant={variant} onClick={requestClose} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoCard.defaultProps = {
|
||||||
|
className: null,
|
||||||
|
style: null,
|
||||||
|
variant: 'surface',
|
||||||
|
iconSrc: null,
|
||||||
|
content: null,
|
||||||
|
rounded: false,
|
||||||
|
requestClose: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
InfoCard.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.shape({}),
|
||||||
|
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||||
|
iconSrc: PropTypes.string,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.node,
|
||||||
|
rounded: PropTypes.bool,
|
||||||
|
requestClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoCard;
|
||||||
79
src/app/atoms/card/InfoCard.scss
Normal file
79
src/app/atoms/card/InfoCard.scss
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
@use '.././../partials/flex';
|
||||||
|
@use '.././../partials/dir';
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
line-height: 0;
|
||||||
|
padding: var(--sp-tight);
|
||||||
|
@include dir.prop(border-left, 4px solid transparent, none);
|
||||||
|
@include dir.prop(border-right, none, 4px solid transparent);
|
||||||
|
|
||||||
|
& > .ic-btn {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
margin: 0 var(--sp-tight);
|
||||||
|
@extend .cp-fx__item-one;
|
||||||
|
|
||||||
|
& > *:nth-child(2) {
|
||||||
|
margin-top: var(--sp-ultra-tight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--rounded {
|
||||||
|
@include dir.prop(
|
||||||
|
border-radius,
|
||||||
|
0 var(--bo-radius) var(--bo-radius) 0,
|
||||||
|
var(--bo-radius) 0 0 var(--bo-radius)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--surface {
|
||||||
|
border-color: var(--bg-surface-border);
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
|
||||||
|
}
|
||||||
|
&--primary {
|
||||||
|
border-color: var(--bg-primary);
|
||||||
|
background-color: var(--bg-primary-hover);
|
||||||
|
& .text {
|
||||||
|
color: var(--tc-primary-high);
|
||||||
|
&-b3 {
|
||||||
|
color: var(--tc-primary-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--positive {
|
||||||
|
border-color: var(--bg-positive-border);
|
||||||
|
background-color: var(--bg-positive-hover);
|
||||||
|
& .text {
|
||||||
|
color: var(--tc-positive-high);
|
||||||
|
&-b3 {
|
||||||
|
color: var(--tc-positive-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--caution {
|
||||||
|
border-color: var(--bg-caution-border);
|
||||||
|
background-color: var(--bg-caution-hover);
|
||||||
|
& .text {
|
||||||
|
color: var(--tc-caution-high);
|
||||||
|
&-b3 {
|
||||||
|
color: var(--tc-caution-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--danger {
|
||||||
|
border-color: var(--bg-danger-border);
|
||||||
|
background-color: var(--bg-danger-hover);
|
||||||
|
& .text {
|
||||||
|
color: var(--tc-danger-high);
|
||||||
|
&-b3 {
|
||||||
|
color: var(--tc-danger-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/app/atoms/math/Math.jsx
Normal file
33
src/app/atoms/math/Math.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import katex from 'katex';
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
import 'katex/dist/contrib/copy-tex';
|
||||||
|
import 'katex/dist/contrib/copy-tex.css';
|
||||||
|
|
||||||
|
const Math = React.memo(({
|
||||||
|
content, throwOnError, errorColor, displayMode,
|
||||||
|
}) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
|
||||||
|
}, [content, throwOnError, errorColor, displayMode]);
|
||||||
|
|
||||||
|
return <span ref={ref} />;
|
||||||
|
});
|
||||||
|
Math.defaultProps = {
|
||||||
|
throwOnError: null,
|
||||||
|
errorColor: null,
|
||||||
|
displayMode: null,
|
||||||
|
};
|
||||||
|
Math.propTypes = {
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
throwOnError: PropTypes.bool,
|
||||||
|
errorColor: PropTypes.string,
|
||||||
|
displayMode: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Math;
|
||||||
@@ -74,7 +74,7 @@ Tabs.defaultProps = {
|
|||||||
|
|
||||||
Tabs.propTypes = {
|
Tabs.propTypes = {
|
||||||
items: PropTypes.arrayOf(
|
items: PropTypes.arrayOf(
|
||||||
PropTypes.exact({
|
PropTypes.shape({
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
@@ -84,4 +84,4 @@ Tabs.propTypes = {
|
|||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Tabs as default };
|
export default Tabs;
|
||||||
|
|||||||
25
src/app/hooks/useCrossSigningStatus.js
Normal file
25
src/app/hooks/useCrossSigningStatus.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import initMatrix from '../../client/initMatrix';
|
||||||
|
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
|
||||||
|
|
||||||
|
export function useCrossSigningStatus() {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCSEnabled) return null;
|
||||||
|
const handleAccountData = (event) => {
|
||||||
|
if (event.getType() === 'm.cross_signing.master') {
|
||||||
|
setIsCSEnabled(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mx.on('accountData', handleAccountData);
|
||||||
|
return () => {
|
||||||
|
mx.removeListener('accountData', handleAccountData);
|
||||||
|
};
|
||||||
|
}, [isCSEnabled === false]);
|
||||||
|
return isCSEnabled;
|
||||||
|
}
|
||||||
32
src/app/hooks/useDeviceList.js
Normal file
32
src/app/hooks/useDeviceList.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import initMatrix from '../../client/initMatrix';
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
58
src/app/molecules/confirm-dialog/ConfirmDialog.jsx
Normal file
58
src/app/molecules/confirm-dialog/ConfirmDialog.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './ConfirmDialog.scss';
|
||||||
|
|
||||||
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
|
|
||||||
|
function ConfirmDialog({
|
||||||
|
desc, actionTitle, actionType, onComplete,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="confirm-dialog">
|
||||||
|
<Text>{desc}</Text>
|
||||||
|
<div className="confirm-dialog__btn">
|
||||||
|
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
|
||||||
|
<Button onClick={() => onComplete(false)}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ConfirmDialog.propTypes = {
|
||||||
|
desc: PropTypes.string.isRequired,
|
||||||
|
actionTitle: PropTypes.string.isRequired,
|
||||||
|
actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired,
|
||||||
|
onComplete: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} title title of confirm dialog
|
||||||
|
* @param {string} desc description of confirm dialog
|
||||||
|
* @param {string} actionTitle title of main action to take
|
||||||
|
* @param {'primary' | 'positive' | 'danger' | 'caution'} actionType type of action. default=primary
|
||||||
|
* @return {Promise<boolean>} does it get's confirmed or not
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => {
|
||||||
|
let isCompleted = false;
|
||||||
|
openReusableDialog(
|
||||||
|
<Text variant="s1" weight="medium">{title}</Text>,
|
||||||
|
(requestClose) => (
|
||||||
|
<ConfirmDialog
|
||||||
|
desc={desc}
|
||||||
|
actionTitle={actionTitle}
|
||||||
|
actionType={actionType}
|
||||||
|
onComplete={(isConfirmed) => {
|
||||||
|
isCompleted = true;
|
||||||
|
resolve(isConfirmed);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
() => {
|
||||||
|
if (!isCompleted) resolve(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
11
src/app/molecules/confirm-dialog/ConfirmDialog.scss
Normal file
11
src/app/molecules/confirm-dialog/ConfirmDialog.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.confirm-dialog {
|
||||||
|
padding: var(--sp-normal);
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
padding-bottom: var(--sp-normal);
|
||||||
|
}
|
||||||
|
&__btn {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,11 @@ import RawModal from '../../atoms/modal/RawModal';
|
|||||||
function Dialog({
|
function Dialog({
|
||||||
className, isOpen, title, onAfterOpen, onAfterClose,
|
className, isOpen, title, onAfterOpen, onAfterClose,
|
||||||
contentOptions, onRequestClose, closeFromOutside, children,
|
contentOptions, onRequestClose, closeFromOutside, children,
|
||||||
|
invisibleScroll,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RawModal
|
<RawModal
|
||||||
className={`${className === null ? '' : `${className} `}dialog-model`}
|
className={`${className === null ? '' : `${className} `}dialog-modal`}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onAfterOpen={onAfterOpen}
|
onAfterOpen={onAfterOpen}
|
||||||
onAfterClose={onAfterClose}
|
onAfterClose={onAfterClose}
|
||||||
@@ -36,7 +37,7 @@ function Dialog({
|
|||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="dialog__content__wrapper">
|
<div className="dialog__content__wrapper">
|
||||||
<ScrollView autoHide>
|
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
||||||
<div className="dialog__content-container">
|
<div className="dialog__content-container">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +56,7 @@ Dialog.defaultProps = {
|
|||||||
onAfterClose: null,
|
onAfterClose: null,
|
||||||
onRequestClose: null,
|
onRequestClose: null,
|
||||||
closeFromOutside: true,
|
closeFromOutside: true,
|
||||||
|
invisibleScroll: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Dialog.propTypes = {
|
Dialog.propTypes = {
|
||||||
@@ -67,6 +69,7 @@ Dialog.propTypes = {
|
|||||||
onRequestClose: PropTypes.func,
|
onRequestClose: PropTypes.func,
|
||||||
closeFromOutside: PropTypes.bool,
|
closeFromOutside: PropTypes.bool,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
|
invisibleScroll: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dialog;
|
export default Dialog;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.dialog-model {
|
.dialog-modal {
|
||||||
--modal-height: 656px;
|
--modal-height: 656px;
|
||||||
max-height: min(100%, var(--modal-height));
|
max-height: min(100%, var(--modal-height));
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -21,8 +21,3 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog__content-container {
|
|
||||||
padding-top: var(--sp-extra-tight);
|
|
||||||
padding-bottom: var(--sp-extra-loose);
|
|
||||||
}
|
|
||||||
49
src/app/molecules/dialog/ReusableDialog.jsx
Normal file
49
src/app/molecules/dialog/ReusableDialog.jsx
Normal file
@@ -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;
|
||||||
@@ -36,6 +36,8 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
|||||||
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
|
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
|
||||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||||
|
|
||||||
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function PlaceholderMessage() {
|
function PlaceholderMessage() {
|
||||||
return (
|
return (
|
||||||
<div className="ph-msg">
|
<div className="ph-msg">
|
||||||
@@ -121,17 +123,26 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
|||||||
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
|
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
|
||||||
await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
|
await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
|
||||||
|
|
||||||
const mEvent = eTimeline.getTimelineSet().findEventById(eventId);
|
let mEvent = eTimeline.getTimelineSet().findEventById(eventId);
|
||||||
|
const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
|
||||||
|
if (editedList) {
|
||||||
|
mEvent = editedList[editedList.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
const rawBody = mEvent.getContent().body;
|
const rawBody = mEvent.getContent().body;
|
||||||
const username = getUsernameOfRoomMember(mEvent.sender);
|
const username = getUsernameOfRoomMember(mEvent.sender);
|
||||||
|
|
||||||
if (isMountedRef.current === false) return;
|
if (isMountedRef.current === false) return;
|
||||||
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
|
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
|
||||||
|
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
|
||||||
|
if (editedList && parsedBody.startsWith(' * ')) {
|
||||||
|
parsedBody = parsedBody.slice(3);
|
||||||
|
}
|
||||||
|
|
||||||
setReply({
|
setReply({
|
||||||
to: username,
|
to: username,
|
||||||
color: colorMXID(mEvent.getSender()),
|
color: colorMXID(mEvent.getSender()),
|
||||||
body: parseReply(rawBody)?.body ?? rawBody ?? fallbackBody,
|
body: parsedBody,
|
||||||
event: mEvent,
|
event: mEvent,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -150,10 +161,13 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const focusReply = () => {
|
const focusReply = (ev) => {
|
||||||
if (reply?.event === null) return;
|
if (!ev.keyCode || ev.keyCode === 32 || ev.keyCode === 13) {
|
||||||
if (reply?.event.isRedacted()) return;
|
if (ev.keyCode) ev.preventDefault();
|
||||||
roomTimeline.loadEventTimeline(eventId);
|
if (reply?.event === null) return;
|
||||||
|
if (reply?.event.isRedacted()) return;
|
||||||
|
roomTimeline.loadEventTimeline(eventId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -183,9 +197,17 @@ const MessageBody = React.memo(({
|
|||||||
// if body is not string it is a React element.
|
// if body is not string it is a React element.
|
||||||
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
|
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
|
||||||
|
|
||||||
let content = isCustomHTML
|
let content = null;
|
||||||
? twemojify(sanitizeCustomHtml(body), undefined, true, false)
|
if (isCustomHTML) {
|
||||||
: twemojify(body, undefined, true);
|
try {
|
||||||
|
content = twemojify(sanitizeCustomHtml(body), undefined, true, false, true);
|
||||||
|
} 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
|
// Determine if this message should render with large emojis
|
||||||
// Criteria:
|
// Criteria:
|
||||||
@@ -201,10 +223,10 @@ const MessageBody = React.memo(({
|
|||||||
// Count the number of emojis
|
// Count the number of emojis
|
||||||
const nEmojis = content.filter((e) => e.type === 'img').length;
|
const nEmojis = content.filter((e) => e.type === 'img').length;
|
||||||
|
|
||||||
// Make sure there's no text besides whitespace
|
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
||||||
if (nEmojis <= 10 && content.every((element) => (
|
if (nEmojis <= 10 && content.every((element) => (
|
||||||
(typeof element === 'object' && element.type === 'img')
|
(typeof element === 'object' && element.type === 'img')
|
||||||
|| (typeof element === 'string' && /^\s*$/g.test(element))
|
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
||||||
))) {
|
))) {
|
||||||
emojiOnly = true;
|
emojiOnly = true;
|
||||||
}
|
}
|
||||||
@@ -466,6 +488,18 @@ function isMedia(mE) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if editedTimeline has mEventId then pass editedMEvent else pass mEvent to openViewSource
|
||||||
|
function handleOpenViewSource(mEvent, roomTimeline) {
|
||||||
|
const eventId = mEvent.getId();
|
||||||
|
const { editedTimeline } = roomTimeline ?? {};
|
||||||
|
let editedMEvent;
|
||||||
|
if (editedTimeline?.has(eventId)) {
|
||||||
|
const editedList = editedTimeline.get(eventId);
|
||||||
|
editedMEvent = editedList[editedList.length - 1];
|
||||||
|
}
|
||||||
|
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
||||||
|
}
|
||||||
|
|
||||||
const MessageOptions = React.memo(({
|
const MessageOptions = React.memo(({
|
||||||
roomTimeline, mEvent, edit, reply,
|
roomTimeline, mEvent, edit, reply,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -513,7 +547,7 @@ const MessageOptions = React.memo(({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={CmdIC}
|
iconSrc={CmdIC}
|
||||||
onClick={() => openViewSource(mEvent)}
|
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
|
||||||
>
|
>
|
||||||
View source
|
View source
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -523,10 +557,15 @@ const MessageOptions = React.memo(({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
iconSrc={BinIC}
|
iconSrc={BinIC}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (window.confirm('Are you sure you want to delete this event')) {
|
const isConfirmed = await confirmDialog(
|
||||||
redactEvent(roomId, mEvent.getId());
|
'Delete message',
|
||||||
}
|
'Are you sure that you want to delete this message?',
|
||||||
|
'Delete',
|
||||||
|
'danger',
|
||||||
|
);
|
||||||
|
if (!isConfirmed) return;
|
||||||
|
redactEvent(roomId, mEvent.getId());
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@use '../../atoms/scroll/scrollbar';
|
@use '../../atoms/scroll/scrollbar';
|
||||||
@use '../../partials/text';
|
@use '../../partials/text';
|
||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/screen';
|
||||||
|
|
||||||
.message,
|
.message,
|
||||||
.ph-msg {
|
.ph-msg {
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
.message__reactions {
|
.message__reactions {
|
||||||
max-width: calc(100% - 88px);
|
max-width: calc(100% - 88px);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@media (max-width: 1124px) {
|
@include screen.smallerThan(tabletBreakpoint) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ function PopupWindow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RawModal
|
<RawModal
|
||||||
className={`${className === null ? '' : `${className} `}pw-model`}
|
className={`${className === null ? '' : `${className} `}pw-modal`}
|
||||||
|
overlayClassName="pw-modal__overlay"
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onAfterClose={onAfterClose}
|
onAfterClose={onAfterClose}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/screen';
|
||||||
|
|
||||||
.pw-model {
|
.pw-modal {
|
||||||
--modal-height: 656px;
|
--modal-height: 774px;
|
||||||
max-height: var(--modal-height) !important;
|
max-height: var(--modal-height) !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
@include screen.smallerThan(mobileBreakpoint) {
|
||||||
|
--modal-height: 100%;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
&__overlay {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pw {
|
.pw {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import Text from '../../atoms/text/Text';
|
|||||||
import Toggle from '../../atoms/button/Toggle';
|
import Toggle from '../../atoms/button/Toggle';
|
||||||
import SettingTile from '../setting-tile/SettingTile';
|
import SettingTile from '../setting-tile/SettingTile';
|
||||||
|
|
||||||
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function RoomEncryption({ roomId }) {
|
function RoomEncryption({ roomId }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
@@ -15,17 +17,20 @@ function RoomEncryption({ roomId }) {
|
|||||||
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
|
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
|
||||||
const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
|
const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
|
||||||
|
|
||||||
const handleEncryptionEnable = () => {
|
const handleEncryptionEnable = async () => {
|
||||||
const joinRule = room.getJoinRule();
|
const joinRule = room.getJoinRule();
|
||||||
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.';
|
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.';
|
||||||
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly';
|
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly';
|
||||||
if (joinRule === 'public' ? confirm(confirmMsg1) : true) {
|
|
||||||
if (confirm(confirmMsg2)) {
|
const isConfirmed1 = (joinRule === 'public')
|
||||||
setIsEncrypted(true);
|
? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution')
|
||||||
mx.sendStateEvent(roomId, 'm.room.encryption', {
|
: true;
|
||||||
algorithm: 'm.megolm.v1.aes-sha2',
|
if (!isConfirmed1) return;
|
||||||
});
|
if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) {
|
||||||
}
|
setIsEncrypted(true);
|
||||||
|
mx.sendStateEvent(roomId, 'm.room.encryption', {
|
||||||
|
algorithm: 'm.megolm.v1.aes-sha2',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ const visibility = {
|
|||||||
|
|
||||||
const items = [{
|
const items = [{
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
text: 'World readable (anyone can read)',
|
text: 'Anyone (including guests)',
|
||||||
type: visibility.WORLD_READABLE,
|
type: visibility.WORLD_READABLE,
|
||||||
}, {
|
}, {
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
text: 'Member shared (since the point in time of selecting this option)',
|
text: 'Members (all messages)',
|
||||||
type: visibility.SHARED,
|
type: visibility.SHARED,
|
||||||
}, {
|
}, {
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
text: 'Member invited (since they were invited)',
|
text: 'Members (messages after invite)',
|
||||||
type: visibility.INVITED,
|
type: visibility.INVITED,
|
||||||
}, {
|
}, {
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
text: 'Member joined (since they joined)',
|
text: 'Members (messages after join)',
|
||||||
type: visibility.JOINED,
|
type: visibility.JOINED,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ function RoomHistoryVisibility({ roomId }) {
|
|||||||
</MenuItem>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomIntro.scss';
|
import './RoomIntro.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
@@ -15,8 +14,8 @@ function RoomIntro({
|
|||||||
<div className="room-intro">
|
<div className="room-intro">
|
||||||
<Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" />
|
<Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" />
|
||||||
<div className="room-intro__content">
|
<div className="room-intro__content">
|
||||||
<Text className="room-intro__name" variant="h1" weight="medium" primary>{twemojify(heading)}</Text>
|
<Text className="room-intro__name" variant="h1" weight="medium" primary>{heading}</Text>
|
||||||
<Text className="room-intro__desc" variant="b1">{twemojify(desc, undefined, true)}</Text>
|
<Text className="room-intro__desc" variant="b1">{desc}</Text>
|
||||||
{ time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>}
|
{ time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,9 +34,9 @@ RoomIntro.propTypes = {
|
|||||||
PropTypes.bool,
|
PropTypes.bool,
|
||||||
]),
|
]),
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
heading: PropTypes.string.isRequired,
|
heading: PropTypes.node.isRequired,
|
||||||
desc: PropTypes.string.isRequired,
|
desc: PropTypes.node.isRequired,
|
||||||
time: PropTypes.string,
|
time: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RoomIntro;
|
export default RoomIntro;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import colorMXID from '../../../util/colorMXID';
|
|||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
||||||
import AsyncSearch from '../../../util/AsyncSearch';
|
import AsyncSearch from '../../../util/AsyncSearch';
|
||||||
|
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
@@ -19,26 +20,6 @@ import PeopleSelector from '../people-selector/PeopleSelector';
|
|||||||
|
|
||||||
const PER_PAGE_MEMBER = 50;
|
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) {
|
function normalizeMembers(members) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
return members.map((member) => ({
|
return members.map((member) => ({
|
||||||
@@ -65,7 +46,7 @@ function useMemberOfMembership(roomId, membership) {
|
|||||||
if (event && event?.getRoomId() !== roomId) return;
|
if (event && event?.getRoomId() !== roomId) return;
|
||||||
const memberOfMembership = normalizeMembers(
|
const memberOfMembership = normalizeMembers(
|
||||||
room.getMembersWithMembership(membership)
|
room.getMembersWithMembership(membership)
|
||||||
.sort(AtoZ).sort(sortByPowerLevel),
|
.sort(memberByAtoZ).sort(memberByPowerLevel),
|
||||||
);
|
);
|
||||||
setMembers(memberOfMembership);
|
setMembers(memberOfMembership);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,29 +32,15 @@ const items = [{
|
|||||||
type: cons.notifs.MUTE,
|
type: cons.notifs.MUTE,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
function getNotifType(roomId) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const pushRule = mx.getRoomPushRule('global', roomId);
|
|
||||||
|
|
||||||
if (typeof pushRule === 'undefined') {
|
|
||||||
const overridePushRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
|
|
||||||
if (typeof overridePushRules === 'undefined') return 0;
|
|
||||||
|
|
||||||
const isMuteOverride = overridePushRules.find((rule) => (
|
|
||||||
rule.rule_id === roomId
|
|
||||||
&& rule.actions[0] === 'dont_notify'
|
|
||||||
&& rule.conditions[0].kind === 'event_match'
|
|
||||||
));
|
|
||||||
|
|
||||||
return isMuteOverride ? cons.notifs.MUTE : cons.notifs.DEFAULT;
|
|
||||||
}
|
|
||||||
if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES;
|
|
||||||
return cons.notifs.MENTIONS_AND_KEYWORDS;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRoomNotifType(roomId, newType) {
|
function setRoomNotifType(roomId, newType) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const roomPushRule = mx.getRoomPushRule('global', roomId);
|
const { notifications } = initMatrix;
|
||||||
|
let roomPushRule;
|
||||||
|
try {
|
||||||
|
roomPushRule = mx.getRoomPushRule('global', roomId);
|
||||||
|
} catch {
|
||||||
|
roomPushRule = undefined;
|
||||||
|
}
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
if (newType === cons.notifs.MUTE) {
|
if (newType === cons.notifs.MUTE) {
|
||||||
@@ -76,7 +62,7 @@ function setRoomNotifType(roomId, newType) {
|
|||||||
return promises;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldState = getNotifType(roomId);
|
const oldState = notifications.getNotiType(roomId);
|
||||||
if (oldState === cons.notifs.MUTE) {
|
if (oldState === cons.notifs.MUTE) {
|
||||||
promises.push(mx.deletePushRule('global', 'override', roomId));
|
promises.push(mx.deletePushRule('global', 'override', roomId));
|
||||||
}
|
}
|
||||||
@@ -115,8 +101,9 @@ function setRoomNotifType(roomId, newType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useNotifications(roomId) {
|
function useNotifications(roomId) {
|
||||||
const [activeType, setActiveType] = useState(getNotifType(roomId));
|
const { notifications } = initMatrix;
|
||||||
useEffect(() => setActiveType(getNotifType(roomId)), [roomId]);
|
const [activeType, setActiveType] = useState(notifications.getNotiType(roomId));
|
||||||
|
useEffect(() => setActiveType(notifications.getNotiType(roomId)), [roomId]);
|
||||||
|
|
||||||
const setNotification = useCallback((item) => {
|
const setNotification = useCallback((item) => {
|
||||||
if (item.type === activeType.type) return;
|
if (item.type === activeType.type) return;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { twemojify } from '../../../util/twemojify';
|
|||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
import { openInviteUser } from '../../../client/action/navigation';
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
|
|
||||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
import RoomNotification from '../room-notification/RoomNotification';
|
import RoomNotification from '../room-notification/RoomNotification';
|
||||||
@@ -14,27 +15,32 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
|||||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||||
|
|
||||||
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function RoomOptions({ roomId, afterOptionSelect }) {
|
function RoomOptions({ roomId, afterOptionSelect }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const canInvite = room?.canInvite(mx.getUserId());
|
const canInvite = room?.canInvite(mx.getUserId());
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
|
markAsRead(roomId);
|
||||||
afterOptionSelect();
|
afterOptionSelect();
|
||||||
if (!room) return;
|
|
||||||
const events = room.getLiveTimeline().getEvents();
|
|
||||||
mx.sendReadReceipt(events[events.length - 1]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInviteClick = () => {
|
const handleInviteClick = () => {
|
||||||
openInviteUser(roomId);
|
openInviteUser(roomId);
|
||||||
afterOptionSelect();
|
afterOptionSelect();
|
||||||
};
|
};
|
||||||
const handleLeaveClick = () => {
|
const handleLeaveClick = async () => {
|
||||||
if (confirm('Are you really want to leave this room?')) {
|
afterOptionSelect();
|
||||||
roomActions.leave(roomId);
|
const isConfirmed = await confirmDialog(
|
||||||
afterOptionSelect();
|
'Leave room',
|
||||||
}
|
`Are you sure that you want to leave "${room.name}" room?`,
|
||||||
|
'Leave',
|
||||||
|
'danger',
|
||||||
|
);
|
||||||
|
if (!isConfirmed) return;
|
||||||
|
roomActions.leave(roomId);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ function RoomPermissions({ roomId }) {
|
|||||||
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
||||||
const permissions = pLEvent.getContent();
|
const permissions = pLEvent.getContent();
|
||||||
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
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 handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
|
||||||
const handlePowerLevelChange = (newPowerLevel) => {
|
const handlePowerLevelChange = (newPowerLevel) => {
|
||||||
@@ -208,7 +209,7 @@ function RoomPermissions({ roomId }) {
|
|||||||
(closeMenu) => (
|
(closeMenu) => (
|
||||||
<PowerLevelSelector
|
<PowerLevelSelector
|
||||||
value={powerLevel}
|
value={powerLevel}
|
||||||
max={100}
|
max={myPowerLevel}
|
||||||
onSelect={(pl) => {
|
onSelect={(pl) => {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
handlePowerLevelChange(pl);
|
handlePowerLevelChange(pl);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
|||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function RoomProfile({ roomId }) {
|
function RoomProfile({ roomId }) {
|
||||||
const isMountStore = useStore();
|
const isMountStore = useStore();
|
||||||
@@ -117,7 +118,13 @@ function RoomProfile({ roomId }) {
|
|||||||
|
|
||||||
const handleAvatarUpload = async (url) => {
|
const handleAvatarUpload = async (url) => {
|
||||||
if (url === null) {
|
if (url === null) {
|
||||||
if (confirm('Are you sure you want to remove avatar?')) {
|
const isConfirmed = await confirmDialog(
|
||||||
|
'Remove avatar',
|
||||||
|
'Are you sure that you want to remove room avatar?',
|
||||||
|
'Remove',
|
||||||
|
'caution',
|
||||||
|
);
|
||||||
|
if (isConfirmed) {
|
||||||
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||||
}
|
}
|
||||||
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
|||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
function RoomSelectorWrapper({
|
function RoomSelectorWrapper({
|
||||||
isSelected, isUnread, onClick,
|
isSelected, isMuted, isUnread, onClick,
|
||||||
content, options, onContextMenu,
|
content, options, onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
let myClass = isUnread ? ' room-selector--unread' : '';
|
const classes = ['room-selector'];
|
||||||
myClass += isSelected ? ' room-selector--selected' : '';
|
if (isMuted) classes.push('room-selector--muted');
|
||||||
|
if (isUnread) classes.push('room-selector--unread');
|
||||||
|
if (isSelected) classes.push('room-selector--selected');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`room-selector${myClass}`}>
|
<div className={classes.join(' ')}>
|
||||||
<button
|
<button
|
||||||
className="room-selector__content"
|
className="room-selector__content"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -32,11 +35,13 @@ function RoomSelectorWrapper({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
RoomSelectorWrapper.defaultProps = {
|
RoomSelectorWrapper.defaultProps = {
|
||||||
|
isMuted: false,
|
||||||
options: null,
|
options: null,
|
||||||
onContextMenu: null,
|
onContextMenu: null,
|
||||||
};
|
};
|
||||||
RoomSelectorWrapper.propTypes = {
|
RoomSelectorWrapper.propTypes = {
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
isMuted: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool.isRequired,
|
isUnread: PropTypes.bool.isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
content: PropTypes.node.isRequired,
|
content: PropTypes.node.isRequired,
|
||||||
@@ -46,12 +51,13 @@ RoomSelectorWrapper.propTypes = {
|
|||||||
|
|
||||||
function RoomSelector({
|
function RoomSelector({
|
||||||
name, parentName, roomId, imageSrc, iconSrc,
|
name, parentName, roomId, imageSrc, iconSrc,
|
||||||
isSelected, isUnread, notificationCount, isAlert,
|
isSelected, isMuted, isUnread, notificationCount, isAlert,
|
||||||
options, onClick, onContextMenu,
|
options, onClick, onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RoomSelectorWrapper
|
<RoomSelectorWrapper
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
isMuted={isMuted}
|
||||||
isUnread={isUnread}
|
isUnread={isUnread}
|
||||||
content={(
|
content={(
|
||||||
<>
|
<>
|
||||||
@@ -91,6 +97,7 @@ RoomSelector.defaultProps = {
|
|||||||
isSelected: false,
|
isSelected: false,
|
||||||
imageSrc: null,
|
imageSrc: null,
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
|
isMuted: false,
|
||||||
options: null,
|
options: null,
|
||||||
onContextMenu: null,
|
onContextMenu: null,
|
||||||
};
|
};
|
||||||
@@ -101,6 +108,7 @@ RoomSelector.propTypes = {
|
|||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
|
isMuted: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool.isRequired,
|
isUnread: PropTypes.bool.isRequired,
|
||||||
notificationCount: PropTypes.oneOfType([
|
notificationCount: PropTypes.oneOfType([
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
border-radius: var(--bo-radius);
|
border-radius: var(--bo-radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&--muted {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
&--unread {
|
&--unread {
|
||||||
.room-selector__content > .text {
|
.room-selector__content > .text {
|
||||||
color: var(--tc-surface-high);
|
color: var(--tc-surface-high);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import './RoomTile.scss';
|
import './RoomTile.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
import { twemojify } from '../../../util/twemojify';
|
||||||
import { sanitizeText } from '../../../util/sanitize';
|
|
||||||
|
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function RoomVisibility({ roomId }) {
|
|||||||
|
|
||||||
const noSpaceParent = currentState.getStateEvents('m.space.parent').length === 0;
|
const noSpaceParent = currentState.getStateEvents('m.space.parent').length === 0;
|
||||||
const mCreate = currentState.getStateEvents('m.room.create')[0]?.getContent();
|
const mCreate = currentState.getStateEvents('m.room.create')[0]?.getContent();
|
||||||
const roomVersion = Number(mCreate.room_version);
|
const roomVersion = Number(mCreate?.room_version ?? 0);
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||||
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ function SettingTile({ title, options, content }) {
|
|||||||
<div className="setting-tile">
|
<div className="setting-tile">
|
||||||
<div className="setting-tile__content">
|
<div className="setting-tile__content">
|
||||||
<div className="setting-tile__title">
|
<div className="setting-tile__title">
|
||||||
<Text variant="b1">{title}</Text>
|
{
|
||||||
|
typeof title === 'string'
|
||||||
|
? <Text variant="b1">{title}</Text>
|
||||||
|
: title
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
@@ -24,7 +28,7 @@ SettingTile.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SettingTile.propTypes = {
|
SettingTile.propTypes = {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.node.isRequired,
|
||||||
options: PropTypes.node,
|
options: PropTypes.node,
|
||||||
content: PropTypes.node,
|
content: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import Tooltip from '../../atoms/tooltip/Tooltip';
|
|||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
const SidebarAvatar = React.forwardRef(({
|
const SidebarAvatar = React.forwardRef(({
|
||||||
tooltip, active, onClick, onContextMenu,
|
className, tooltip, active, onClick,
|
||||||
avatar, notificationBadge,
|
onContextMenu, avatar, notificationBadge,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
let activeClass = '';
|
const classes = ['sidebar-avatar'];
|
||||||
if (active) activeClass = ' sidebar-avatar--active';
|
if (active) classes.push('sidebar-avatar--active');
|
||||||
|
if (className) classes.push(className);
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={<Text variant="b1">{twemojify(tooltip)}</Text>}
|
content={<Text variant="b1">{twemojify(tooltip)}</Text>}
|
||||||
@@ -21,7 +22,7 @@ const SidebarAvatar = React.forwardRef(({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`sidebar-avatar${activeClass}`}
|
className={classes.join(' ')}
|
||||||
type="button"
|
type="button"
|
||||||
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
|
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -34,6 +35,7 @@ const SidebarAvatar = React.forwardRef(({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
SidebarAvatar.defaultProps = {
|
SidebarAvatar.defaultProps = {
|
||||||
|
className: null,
|
||||||
active: false,
|
active: false,
|
||||||
onClick: null,
|
onClick: null,
|
||||||
onContextMenu: null,
|
onContextMenu: null,
|
||||||
@@ -41,6 +43,7 @@ SidebarAvatar.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SidebarAvatar.propTypes = {
|
SidebarAvatar.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
tooltip: PropTypes.string.isRequired,
|
tooltip: PropTypes.string.isRequired,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ function SpaceAddExistingContent({ roomId }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (ev) => {
|
const handleSearch = (ev) => {
|
||||||
const term = ev.target.value.toLocaleLowerCase().replaceAll(' ', '');
|
const term = ev.target.value.toLocaleLowerCase().replace(/\s/g, '');
|
||||||
if (term === '') {
|
if (term === '') {
|
||||||
setSearchIds(null);
|
setSearchIds(null);
|
||||||
return;
|
return;
|
||||||
@@ -100,7 +100,7 @@ function SpaceAddExistingContent({ roomId }) {
|
|||||||
if (!name) return false;
|
if (!name) return false;
|
||||||
name = name.normalize('NFKC')
|
name = name.normalize('NFKC')
|
||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.replaceAll(' ', '');
|
.replace(/\s/g, '');
|
||||||
return name.includes(term);
|
return name.includes(term);
|
||||||
});
|
});
|
||||||
setSearchIds(searchedIds);
|
setSearchIds(searchedIds);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
|||||||
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
|
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
|
||||||
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
|
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
|
||||||
|
|
||||||
|
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function SpaceOptions({ roomId, afterOptionSelect }) {
|
function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
@@ -54,11 +56,16 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
|||||||
afterOptionSelect();
|
afterOptionSelect();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLeaveClick = () => {
|
const handleLeaveClick = async () => {
|
||||||
if (confirm('Are you really want to leave this space?')) {
|
afterOptionSelect();
|
||||||
leave(roomId);
|
const isConfirmed = await confirmDialog(
|
||||||
afterOptionSelect();
|
'Leave space',
|
||||||
}
|
`Are you sure that you want to leave "${room.name}" space?`,
|
||||||
|
'Leave',
|
||||||
|
'danger',
|
||||||
|
);
|
||||||
|
if (!isConfirmed) return;
|
||||||
|
leave(roomId);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
content={(
|
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)" />
|
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.create-room {
|
.create-room {
|
||||||
|
margin: var(--sp-normal);
|
||||||
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
|
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
|
||||||
|
|
||||||
&__form > * {
|
&__form > * {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Text from '../../atoms/text/Text';
|
|||||||
function DragDrop({ isOpen }) {
|
function DragDrop({ isOpen }) {
|
||||||
return (
|
return (
|
||||||
<RawModal
|
<RawModal
|
||||||
className="drag-drop__model"
|
className="drag-drop__modal"
|
||||||
overlayClassName="drag-drop__overlay"
|
overlayClassName="drag-drop__overlay"
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.drag-drop__model {
|
.drag-drop__modal {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import initMatrix from '../../../client/initMatrix';
|
|||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import AsyncSearch from '../../../util/AsyncSearch';
|
import AsyncSearch from '../../../util/AsyncSearch';
|
||||||
|
import { addRecentEmoji, getRecentEmojis } from './recent';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
@@ -20,6 +21,7 @@ import Input from '../../atoms/input/Input';
|
|||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
|
|
||||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
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 EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||||
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
|
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
|
||||||
import CupIC from '../../../../public/res/ic/outlined/cup.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 PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
|
||||||
import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
|
import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
|
||||||
|
|
||||||
|
const ROW_EMOJIS_COUNT = 7;
|
||||||
|
|
||||||
const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
||||||
function getEmojiBoard() {
|
function getEmojiBoard() {
|
||||||
const emojiBoard = [];
|
const emojiBoard = [];
|
||||||
const ROW_EMOJIS_COUNT = 7;
|
|
||||||
const totalEmojis = groupEmojis.length;
|
const totalEmojis = groupEmojis.length;
|
||||||
|
|
||||||
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
|
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
|
||||||
@@ -147,8 +150,9 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||||||
function selectEmoji(e) {
|
function selectEmoji(e) {
|
||||||
if (isTargetNotEmoji(e.target)) return;
|
if (isTargetNotEmoji(e.target)) return;
|
||||||
|
|
||||||
const emoji = e.target;
|
const emoji = getEmojiDataFromTarget(e.target);
|
||||||
onSelect(getEmojiDataFromTarget(emoji));
|
onSelect(emoji);
|
||||||
|
if (emoji.hexcode) addRecentEmoji(emoji.unicode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEmojiInfo(emoji) {
|
function setEmojiInfo(emoji) {
|
||||||
@@ -188,6 +192,9 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [availableEmojis, setAvailableEmojis] = useState([]);
|
const [availableEmojis, setAvailableEmojis] = useState([]);
|
||||||
|
const [recentEmojis, setRecentEmojis] = useState([]);
|
||||||
|
|
||||||
|
const recentOffset = recentEmojis.length > 0 ? 1 : 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateAvailableEmoji = (selectedRoomId) => {
|
const updateAvailableEmoji = (selectedRoomId) => {
|
||||||
@@ -215,6 +222,9 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
searchRef.current.value = '';
|
searchRef.current.value = '';
|
||||||
handleSearchChange();
|
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);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
|
||||||
@@ -230,7 +240,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||||||
const $emojiContent = scrollEmojisRef.current.firstElementChild;
|
const $emojiContent = scrollEmojisRef.current.firstElementChild;
|
||||||
const groupCount = $emojiContent.childElementCount;
|
const groupCount = $emojiContent.childElementCount;
|
||||||
if (groupCount > emojiGroups.length) {
|
if (groupCount > emojiGroups.length) {
|
||||||
tabIndex += groupCount - emojiGroups.length - availableEmojis.length;
|
tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
|
||||||
}
|
}
|
||||||
$emojiContent.children[tabIndex].scrollIntoView();
|
$emojiContent.children[tabIndex].scrollIntoView();
|
||||||
}
|
}
|
||||||
@@ -246,6 +256,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||||||
<ScrollView ref={scrollEmojisRef} autoHide>
|
<ScrollView ref={scrollEmojisRef} autoHide>
|
||||||
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
||||||
<SearchedEmoji />
|
<SearchedEmoji />
|
||||||
|
{recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
|
||||||
{
|
{
|
||||||
availableEmojis.map((pack) => (
|
availableEmojis.map((pack) => (
|
||||||
<EmojiGroup
|
<EmojiGroup
|
||||||
@@ -271,13 +282,21 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||||||
</div>
|
</div>
|
||||||
<ScrollView invisible>
|
<ScrollView invisible>
|
||||||
<div className="emoji-board__nav">
|
<div className="emoji-board__nav">
|
||||||
|
{recentEmojis.length > 0 && (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => openGroup(0)}
|
||||||
|
src={RecentClockIC}
|
||||||
|
tooltip="Recent"
|
||||||
|
tooltipPlacement="right"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="emoji-board__nav-custom">
|
<div className="emoji-board__nav-custom">
|
||||||
{
|
{
|
||||||
availableEmojis.map((pack) => {
|
availableEmojis.map((pack) => {
|
||||||
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
|
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => openGroup(pack.packIndex)}
|
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||||
src={src}
|
src={src}
|
||||||
key={pack.packIndex}
|
key={pack.packIndex}
|
||||||
tooltip={pack.displayName}
|
tooltip={pack.displayName}
|
||||||
@@ -301,7 +320,7 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||||||
[7, FlagIC, 'Flags'],
|
[7, FlagIC, 'Flags'],
|
||||||
].map(([indx, ico, name]) => (
|
].map(([indx, ico, name]) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => openGroup(availableEmojis.length + indx)}
|
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
|
||||||
key={indx}
|
key={indx}
|
||||||
src={ico}
|
src={ico}
|
||||||
tooltip={name}
|
tooltip={name}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
--emoji-board-height: 390px;
|
--emoji-board-height: 390px;
|
||||||
--emoji-board-width: 286px;
|
--emoji-board-width: 286px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
@extend .cp-fx__item-one;
|
@extend .cp-fx__item-one;
|
||||||
@@ -91,6 +93,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.emoji-group {
|
.emoji-group {
|
||||||
--emoji-padding: 6px;
|
--emoji-padding: 6px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
36
src/app/organisms/emoji-board/recent.js
Normal file
36
src/app/organisms/emoji-board/recent.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import { emojis } from './emoji';
|
||||||
|
|
||||||
|
const eventType = 'io.element.recent_emoji';
|
||||||
|
|
||||||
|
function getRecentEmojisRaw() {
|
||||||
|
return initMatrix.matrixClient.getAccountData(eventType).getContent().recent_emoji ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecentEmojis(limit) {
|
||||||
|
const res = [];
|
||||||
|
getRecentEmojisRaw()
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.find(([unicode]) => {
|
||||||
|
const emoji = emojis.find((e) => e.unicode === unicode);
|
||||||
|
if (emoji) return res.push(emoji) >= limit;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addRecentEmoji(unicode) {
|
||||||
|
const recent = getRecentEmojisRaw();
|
||||||
|
const i = recent.findIndex(([u]) => u === unicode);
|
||||||
|
let entry;
|
||||||
|
if (i < 0) {
|
||||||
|
entry = [unicode, 1];
|
||||||
|
} else {
|
||||||
|
[entry] = recent.splice(i, 1);
|
||||||
|
entry[1] += 1;
|
||||||
|
}
|
||||||
|
recent.unshift(entry);
|
||||||
|
initMatrix.matrixClient.setAccountData(eventType, {
|
||||||
|
recent_emoji: recent.slice(0, 100),
|
||||||
|
});
|
||||||
|
}
|
||||||
200
src/app/organisms/emoji-verification/EmojiVerification.jsx
Normal file
200
src/app/organisms/emoji-verification/EmojiVerification.jsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './EmojiVerification.scss';
|
||||||
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import cons from '../../../client/state/cons';
|
||||||
|
import navigation from '../../../client/state/navigation';
|
||||||
|
import { hasPrivateKey } from '../../../client/state/secretStorageKeys';
|
||||||
|
import { getDefaultSSKey, isCrossVerified } from '../../../util/matrixUtil';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
|
import Dialog from '../../molecules/dialog/Dialog';
|
||||||
|
|
||||||
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
import { accessSecretStorage } from '../settings/SecretStorageAccess';
|
||||||
|
|
||||||
|
function EmojiVerificationContent({ data, requestClose }) {
|
||||||
|
const [sas, setSas] = useState(null);
|
||||||
|
const [process, setProcess] = useState(false);
|
||||||
|
const { request, targetDevice } = data;
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const mountStore = useStore();
|
||||||
|
const beginStore = useStore();
|
||||||
|
|
||||||
|
const beginVerification = async () => {
|
||||||
|
if (
|
||||||
|
isCrossVerified(mx.deviceId)
|
||||||
|
&& (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false)
|
||||||
|
) {
|
||||||
|
if (!hasPrivateKey(getDefaultSSKey())) {
|
||||||
|
const keyData = await accessSecretStorage('Emoji verification');
|
||||||
|
if (!keyData) {
|
||||||
|
request.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await mx.checkOwnCrossSigningTrust();
|
||||||
|
}
|
||||||
|
setProcess(true);
|
||||||
|
await request.accept();
|
||||||
|
|
||||||
|
const verifier = request.beginKeyVerification('m.sas.v1', targetDevice);
|
||||||
|
|
||||||
|
const handleVerifier = (sasData) => {
|
||||||
|
verifier.off('show_sas', handleVerifier);
|
||||||
|
if (!mountStore.getItem()) return;
|
||||||
|
setSas(sasData);
|
||||||
|
setProcess(false);
|
||||||
|
};
|
||||||
|
verifier.on('show_sas', handleVerifier);
|
||||||
|
await verifier.verify();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sasMismatch = () => {
|
||||||
|
sas.mismatch();
|
||||||
|
setProcess(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sasConfirm = () => {
|
||||||
|
sas.confirm();
|
||||||
|
setProcess(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mountStore.setItem(true);
|
||||||
|
const handleChange = () => {
|
||||||
|
if (request.done || request.cancelled) {
|
||||||
|
requestClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (targetDevice && !beginStore.getItem()) {
|
||||||
|
beginStore.setItem(true);
|
||||||
|
beginVerification();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request === null) return null;
|
||||||
|
const req = request;
|
||||||
|
req.on('change', handleChange);
|
||||||
|
return () => {
|
||||||
|
req.off('change', handleChange);
|
||||||
|
if (req.cancelled === false && req.done === false) {
|
||||||
|
req.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [request]);
|
||||||
|
|
||||||
|
const renderWait = () => (
|
||||||
|
<>
|
||||||
|
<Spinner size="small" />
|
||||||
|
<Text>Waiting for response from other device...</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sas !== null) {
|
||||||
|
return (
|
||||||
|
<div className="emoji-verification__content">
|
||||||
|
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
|
||||||
|
<div className="emoji-verification__emojis">
|
||||||
|
{sas.sas.emoji.map((emoji, i) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
|
||||||
|
<Text variant="h1">{twemojify(emoji[0])}</Text>
|
||||||
|
<Text>{emoji[1]}</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="emoji-verification__buttons">
|
||||||
|
{process ? renderWait() : (
|
||||||
|
<>
|
||||||
|
<Button variant="primary" onClick={sasConfirm}>They match</Button>
|
||||||
|
<Button onClick={sasMismatch}>{'They don\'t match'}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDevice) {
|
||||||
|
return (
|
||||||
|
<div className="emoji-verification__content">
|
||||||
|
<Text>Please accept the request from other device.</Text>
|
||||||
|
<div className="emoji-verification__buttons">
|
||||||
|
{renderWait()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="emoji-verification__content">
|
||||||
|
<Text>Click accept to start the verification process.</Text>
|
||||||
|
<div className="emoji-verification__buttons">
|
||||||
|
{
|
||||||
|
process
|
||||||
|
? renderWait()
|
||||||
|
: <Button variant="primary" onClick={beginVerification}>Accept</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EmojiVerificationContent.propTypes = {
|
||||||
|
data: PropTypes.shape({}).isRequired,
|
||||||
|
requestClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function useVisibilityToggle() {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOpen = (request, targetDevice) => {
|
||||||
|
setData({ request, targetDevice });
|
||||||
|
};
|
||||||
|
navigation.on(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
|
||||||
|
mx.on('crypto.verification.request', handleOpen);
|
||||||
|
return () => {
|
||||||
|
navigation.removeListener(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
|
||||||
|
mx.removeListener('crypto.verification.request', handleOpen);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestClose = () => setData(null);
|
||||||
|
|
||||||
|
return [data, requestClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiVerification() {
|
||||||
|
const [data, requestClose] = useVisibilityToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
isOpen={data !== null}
|
||||||
|
className="emoji-verification"
|
||||||
|
title={(
|
||||||
|
<Text variant="s1" weight="medium" primary>
|
||||||
|
Emoji verification
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||||
|
onRequestClose={requestClose}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
data !== null
|
||||||
|
? <EmojiVerificationContent data={data} requestClose={requestClose} />
|
||||||
|
: <div />
|
||||||
|
}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmojiVerification;
|
||||||
35
src/app/organisms/emoji-verification/EmojiVerification.scss
Normal file
35
src/app/organisms/emoji-verification/EmojiVerification.scss
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@use '../../partials/flex';
|
||||||
|
@use '../../partials/dir';
|
||||||
|
|
||||||
|
.emoji-verification {
|
||||||
|
&__content {
|
||||||
|
padding: var(--sp-normal);
|
||||||
|
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__emojis {
|
||||||
|
margin: var(--sp-loose) 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
gap: var(--sp-extra-tight);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__emoji-block {
|
||||||
|
@extend .cp-fx__column;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-extra-tight);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,17 +54,20 @@ function InviteList({ isOpen, onRequestClose }) {
|
|||||||
}, [procInvite]);
|
}, [procInvite]);
|
||||||
|
|
||||||
function renderRoomTile(roomId) {
|
function renderRoomTile(roomId) {
|
||||||
const myRoom = initMatrix.matrixClient.getRoom(roomId);
|
const mx = initMatrix.matrixClient;
|
||||||
|
const myRoom = mx.getRoom(roomId);
|
||||||
|
if (!myRoom) return null;
|
||||||
const roomName = myRoom.name;
|
const roomName = myRoom.name;
|
||||||
let roomAlias = myRoom.getCanonicalAlias();
|
let roomAlias = myRoom.getCanonicalAlias();
|
||||||
if (roomAlias === null) roomAlias = myRoom.roomId;
|
if (!roomAlias) roomAlias = myRoom.roomId;
|
||||||
|
const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? '';
|
||||||
return (
|
return (
|
||||||
<RoomTile
|
<RoomTile
|
||||||
key={myRoom.roomId}
|
key={myRoom.roomId}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
avatarSrc={initMatrix.matrixClient.getRoom(roomId).getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop')}
|
avatarSrc={initMatrix.matrixClient.getRoom(roomId).getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop')}
|
||||||
id={roomAlias}
|
id={roomAlias}
|
||||||
inviterName={myRoom.getJoinedMembers()[0].userId}
|
inviterName={inviterName}
|
||||||
options={
|
options={
|
||||||
procInvite.has(myRoom.roomId)
|
procInvite.has(myRoom.roomId)
|
||||||
? (<Spinner size="small" />)
|
? (<Spinner size="small" />)
|
||||||
@@ -95,12 +98,13 @@ function InviteList({ isOpen, onRequestClose }) {
|
|||||||
{
|
{
|
||||||
Array.from(initMatrix.roomList.inviteDirects).map((roomId) => {
|
Array.from(initMatrix.roomList.inviteDirects).map((roomId) => {
|
||||||
const myRoom = initMatrix.matrixClient.getRoom(roomId);
|
const myRoom = initMatrix.matrixClient.getRoom(roomId);
|
||||||
|
if (myRoom === null) return null;
|
||||||
const roomName = myRoom.name;
|
const roomName = myRoom.name;
|
||||||
return (
|
return (
|
||||||
<RoomTile
|
<RoomTile
|
||||||
key={myRoom.roomId}
|
key={myRoom.roomId}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
id={myRoom.getDMInviter()}
|
id={myRoom.getDMInviter() || roomId}
|
||||||
options={
|
options={
|
||||||
procInvite.has(myRoom.roomId)
|
procInvite.has(myRoom.roomId)
|
||||||
? (<Spinner size="small" />)
|
? (<Spinner size="small" />)
|
||||||
|
|||||||
@@ -103,6 +103,18 @@ function InviteUser({
|
|||||||
updateIsSearching(false);
|
updateIsSearching(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasDevices(userId) {
|
||||||
|
try {
|
||||||
|
const usersDeviceMap = await mx.downloadKeys([userId, mx.getUserId()]);
|
||||||
|
return Object.values(usersDeviceMap).every((userDevices) =>
|
||||||
|
Object.keys(userDevices).length > 0,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error determining if it's possible to encrypt to all users: ", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createDM(userId) {
|
async function createDM(userId) {
|
||||||
if (mx.getUserId() === userId) return;
|
if (mx.getUserId() === userId) return;
|
||||||
const dmRoomId = hasDMWith(userId);
|
const dmRoomId = hasDMWith(userId);
|
||||||
@@ -117,7 +129,7 @@ function InviteUser({
|
|||||||
procUserError.delete(userId);
|
procUserError.delete(userId);
|
||||||
updateUserProcError(getMapCopy(procUserError));
|
updateUserProcError(getMapCopy(procUserError));
|
||||||
|
|
||||||
const result = await roomActions.createDM(userId);
|
const result = await roomActions.createDM(userId, await hasDevices(userId));
|
||||||
roomIdToUserId.set(result.room_id, userId);
|
roomIdToUserId.set(result.room_id, userId);
|
||||||
updateRoomIdToUserId(getMapCopy(roomIdToUserId));
|
updateRoomIdToUserId(getMapCopy(roomIdToUserId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
155
src/app/organisms/join-alias/JoinAlias.jsx
Normal file
155
src/app/organisms/join-alias/JoinAlias.jsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './JoinAlias.scss';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import cons from '../../../client/state/cons';
|
||||||
|
import navigation from '../../../client/state/navigation';
|
||||||
|
import { join } from '../../../client/action/room';
|
||||||
|
import { selectRoom, selectSpace } from '../../../client/action/navigation';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
|
import Input from '../../atoms/input/Input';
|
||||||
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
|
import Dialog from '../../molecules/dialog/Dialog';
|
||||||
|
|
||||||
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
|
const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
|
||||||
|
|
||||||
|
function JoinAliasContent({ term, requestClose }) {
|
||||||
|
const [process, setProcess] = useState(false);
|
||||||
|
const [error, setError] = useState(undefined);
|
||||||
|
const [lastJoinId, setLastJoinId] = useState(undefined);
|
||||||
|
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const mountStore = useStore();
|
||||||
|
|
||||||
|
const openRoom = (roomId) => {
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
if (!room) return;
|
||||||
|
if (room.isSpaceRoom()) selectSpace(roomId);
|
||||||
|
else selectRoom(roomId);
|
||||||
|
requestClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleJoin = (roomId) => {
|
||||||
|
if (lastJoinId !== roomId) return;
|
||||||
|
openRoom(roomId);
|
||||||
|
};
|
||||||
|
initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleJoin);
|
||||||
|
return () => {
|
||||||
|
initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleJoin);
|
||||||
|
};
|
||||||
|
}, [lastJoinId]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
mountStore.setItem(true);
|
||||||
|
const alias = e.target.alias.value;
|
||||||
|
if (alias?.trim() === '') return;
|
||||||
|
if (alias.match(ALIAS_OR_ID_REG) === null) {
|
||||||
|
setError('Invalid address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProcess('Looking for address...');
|
||||||
|
setError(undefined);
|
||||||
|
let via;
|
||||||
|
if (alias.startsWith('#')) {
|
||||||
|
try {
|
||||||
|
const aliasData = await mx.resolveRoomAlias(alias);
|
||||||
|
via = aliasData?.servers.slice(0, 3) || [];
|
||||||
|
if (mountStore.getItem()) {
|
||||||
|
setProcess(`Joining ${alias}...`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!mountStore.getItem()) return;
|
||||||
|
setProcess(false);
|
||||||
|
setError(`Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const roomId = await join(alias, false, via);
|
||||||
|
if (!mountStore.getItem()) return;
|
||||||
|
setLastJoinId(roomId);
|
||||||
|
openRoom(roomId);
|
||||||
|
} catch {
|
||||||
|
if (!mountStore.getItem()) return;
|
||||||
|
setProcess(false);
|
||||||
|
setError(`Unable to join ${alias}. Either room/space is private or doesn't exist.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="join-alias" onSubmit={handleSubmit}>
|
||||||
|
<Input
|
||||||
|
label="Address"
|
||||||
|
value={term}
|
||||||
|
name="alias"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{error && <Text className="join-alias__error" variant="b3">{error}</Text>}
|
||||||
|
<div className="join-alias__btn">
|
||||||
|
{
|
||||||
|
process
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<Spinner size="small" />
|
||||||
|
<Text>{process}</Text>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: <Button variant="primary" type="submit">Join</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
JoinAliasContent.defaultProps = {
|
||||||
|
term: undefined,
|
||||||
|
};
|
||||||
|
JoinAliasContent.propTypes = {
|
||||||
|
term: PropTypes.string,
|
||||||
|
requestClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function useWindowToggle() {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOpen = (term) => {
|
||||||
|
setData({ term });
|
||||||
|
};
|
||||||
|
navigation.on(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
|
||||||
|
return () => {
|
||||||
|
navigation.removeListener(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onRequestClose = () => setData(null);
|
||||||
|
|
||||||
|
return [data, onRequestClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
function JoinAlias() {
|
||||||
|
const [data, requestClose] = useWindowToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
isOpen={data !== null}
|
||||||
|
title={(
|
||||||
|
<Text variant="s1" weight="medium" primary>Join with address</Text>
|
||||||
|
)}
|
||||||
|
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||||
|
onRequestClose={requestClose}
|
||||||
|
>
|
||||||
|
{ data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div /> }
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JoinAlias;
|
||||||
20
src/app/organisms/join-alias/JoinAlias.scss
Normal file
20
src/app/organisms/join-alias/JoinAlias.scss
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@use '../../partials/dir';
|
||||||
|
|
||||||
|
.join-alias {
|
||||||
|
padding: var(--sp-normal);
|
||||||
|
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||||
|
|
||||||
|
& > *:not(:first-child) {
|
||||||
|
margin-top: var(--sp-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
color: var(--tc-danger-high);
|
||||||
|
margin-top: var(--sp-extra-tight) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,39 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import Postie from '../../../util/Postie';
|
import Postie from '../../../util/Postie';
|
||||||
|
import { roomIdByActivity } from '../../../util/sort';
|
||||||
|
|
||||||
import RoomsCategory from './RoomsCategory';
|
import RoomsCategory from './RoomsCategory';
|
||||||
|
|
||||||
import { AtoZ } from './common';
|
|
||||||
|
|
||||||
const drawerPostie = new Postie();
|
const drawerPostie = new Postie();
|
||||||
function Directs() {
|
function Directs({ size }) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
const { roomList, notifications } = initMatrix;
|
const { roomList, notifications } = initMatrix;
|
||||||
const directIds = [...roomList.directs].sort(AtoZ);
|
const [directIds, setDirectIds] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), [size]);
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
|
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
|
||||||
@@ -33,13 +54,18 @@ function Directs() {
|
|||||||
|
|
||||||
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
|
return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
|
||||||
}
|
}
|
||||||
|
Directs.propTypes = {
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default Directs;
|
export default Directs;
|
||||||
|
|||||||
@@ -42,12 +42,15 @@ function Drawer() {
|
|||||||
const [spaceId] = useSelectedSpace();
|
const [spaceId] = useSelectedSpace();
|
||||||
const [, forceUpdate] = useForceUpdate();
|
const [, forceUpdate] = useForceUpdate();
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
|
const { roomList } = initMatrix;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { roomList } = initMatrix;
|
const handleUpdate = () => {
|
||||||
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate);
|
forceUpdate();
|
||||||
|
};
|
||||||
|
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
|
||||||
return () => {
|
return () => {
|
||||||
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate);
|
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -61,14 +64,16 @@ function Drawer() {
|
|||||||
<div className="drawer">
|
<div className="drawer">
|
||||||
<DrawerHeader selectedTab={selectedTab} spaceId={spaceId} />
|
<DrawerHeader selectedTab={selectedTab} spaceId={spaceId} />
|
||||||
<div className="drawer__content-wrapper">
|
<div className="drawer__content-wrapper">
|
||||||
{navigation.selectedSpacePath.length > 1 && <DrawerBreadcrumb spaceId={spaceId} />}
|
{navigation.selectedSpacePath.length > 1 && selectedTab !== cons.tabs.DIRECTS && (
|
||||||
|
<DrawerBreadcrumb spaceId={spaceId} />
|
||||||
|
)}
|
||||||
<div className="rooms__wrapper">
|
<div className="rooms__wrapper">
|
||||||
<ScrollView ref={scrollRef} autoHide>
|
<ScrollView ref={scrollRef} autoHide>
|
||||||
<div className="rooms-container">
|
<div className="rooms-container">
|
||||||
{
|
{
|
||||||
selectedTab !== cons.tabs.DIRECTS
|
selectedTab !== cons.tabs.DIRECTS
|
||||||
? <Home spaceId={spaceId} />
|
? <Home spaceId={spaceId} />
|
||||||
: <Directs />
|
: <Directs size={roomList.directs.size} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -72,14 +72,22 @@ function DrawerBreadcrumb({ spaceId }) {
|
|||||||
const noti = notifications.getNoti(roomId);
|
const noti = notifications.getNoti(roomId);
|
||||||
if (!notifications.hasNoti(childId)) return noti;
|
if (!notifications.hasNoti(childId)) return noti;
|
||||||
if (noti.from === null) return noti;
|
if (noti.from === null) return noti;
|
||||||
if (noti.from.has(childId) && noti.from.size === 1) return null;
|
|
||||||
|
|
||||||
const childNoti = notifications.getNoti(childId);
|
const childNoti = notifications.getNoti(childId);
|
||||||
|
|
||||||
return {
|
let noOther = true;
|
||||||
total: noti.total - childNoti.total,
|
let total = 0;
|
||||||
highlight: noti.highlight - childNoti.highlight,
|
let highlight = 0;
|
||||||
};
|
noti.from.forEach((fromId) => {
|
||||||
|
if (childNoti.from.has(fromId)) return;
|
||||||
|
noOther = false;
|
||||||
|
const fromNoti = notifications.getNoti(fromId);
|
||||||
|
total += fromNoti.total;
|
||||||
|
highlight += fromNoti.highlight;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (noOther) return null;
|
||||||
|
return { total, highlight };
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { twemojify } from '../../../util/twemojify';
|
|||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import {
|
import {
|
||||||
openPublicRooms, openCreateRoom, openSpaceManage,
|
openPublicRooms, openCreateRoom, openSpaceManage, openJoinAlias,
|
||||||
openSpaceAddExisting, openInviteUser, openReusableContextMenu,
|
openSpaceAddExisting, openInviteUser, openReusableContextMenu,
|
||||||
} from '../../../client/action/navigation';
|
} from '../../../client/action/navigation';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
@@ -60,6 +60,14 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
|
|||||||
Join public room
|
Join public room
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{ !spaceId && (
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={PlusIC}
|
||||||
|
onClick={() => { afterOptionSelect(); openJoinAlias(); }}
|
||||||
|
>
|
||||||
|
Join with address
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{ spaceId && (
|
{ spaceId && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
iconSrc={PlusIC}
|
iconSrc={PlusIC}
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import initMatrix from '../../../client/initMatrix';
|
|||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import Postie from '../../../util/Postie';
|
import Postie from '../../../util/Postie';
|
||||||
|
import { roomIdByActivity, roomIdByAtoZ } from '../../../util/sort';
|
||||||
|
|
||||||
import RoomsCategory from './RoomsCategory';
|
import RoomsCategory from './RoomsCategory';
|
||||||
|
|
||||||
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
|
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
|
||||||
import { AtoZ, RoomToDM } from './common';
|
|
||||||
|
|
||||||
const drawerPostie = new Postie();
|
const drawerPostie = new Postie();
|
||||||
function Home({ spaceId }) {
|
function Home({ spaceId }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const { roomList, notifications, accountData } = initMatrix;
|
const { roomList, notifications, accountData } = initMatrix;
|
||||||
const {
|
const { spaces, rooms, directs } = roomList;
|
||||||
spaces, rooms, directs, roomIdToParents,
|
useCategorizedSpaces();
|
||||||
} = roomList;
|
|
||||||
const categorizedSpaces = useCategorizedSpaces();
|
|
||||||
const isCategorized = accountData.categorizedSpaces.has(spaceId);
|
const isCategorized = accountData.categorizedSpaces.has(spaceId);
|
||||||
|
|
||||||
let categories = null;
|
let categories = null;
|
||||||
@@ -26,22 +24,19 @@ function Home({ spaceId }) {
|
|||||||
let roomIds = [];
|
let roomIds = [];
|
||||||
let directIds = [];
|
let directIds = [];
|
||||||
|
|
||||||
const spaceChildIds = roomList.getSpaceChildren(spaceId);
|
if (spaceId) {
|
||||||
if (spaceChildIds) {
|
const spaceChildIds = roomList.getSpaceChildren(spaceId);
|
||||||
spaceIds = spaceChildIds.filter((roomId) => spaces.has(roomId));
|
spaceIds = spaceChildIds.filter((roomId) => spaces.has(roomId));
|
||||||
roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId));
|
roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId));
|
||||||
directIds = spaceChildIds.filter((roomId) => directs.has(roomId));
|
directIds = spaceChildIds.filter((roomId) => directs.has(roomId));
|
||||||
} else {
|
} else {
|
||||||
spaceIds = [...spaces].filter((roomId) => !roomIdToParents.has(roomId));
|
spaceIds = roomList.getOrphanSpaces();
|
||||||
roomIds = [...rooms].filter((roomId) => !roomIdToParents.has(roomId));
|
roomIds = roomList.getOrphanRooms();
|
||||||
}
|
}
|
||||||
|
|
||||||
spaceIds.sort(AtoZ);
|
|
||||||
roomIds.sort(AtoZ);
|
|
||||||
directIds.sort(AtoZ);
|
|
||||||
|
|
||||||
if (isCategorized) {
|
if (isCategorized) {
|
||||||
categories = roomList.getCategorizedSpaces(spaceIds);
|
categories = roomList.getCategorizedSpaces(spaceIds);
|
||||||
|
categories.delete(spaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,35 +58,47 @@ function Home({ spaceId }) {
|
|||||||
|
|
||||||
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ !isCategorized && spaceIds.length !== 0 && (
|
{ !isCategorized && spaceIds.length !== 0 && (
|
||||||
<RoomsCategory name="Spaces" roomIds={spaceIds} drawerPostie={drawerPostie} />
|
<RoomsCategory name="Spaces" roomIds={spaceIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ roomIds.length !== 0 && (
|
{ roomIds.length !== 0 && (
|
||||||
<RoomsCategory name="Rooms" roomIds={roomIds} drawerPostie={drawerPostie} />
|
<RoomsCategory name="Rooms" roomIds={roomIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ directIds.length !== 0 && (
|
{ directIds.length !== 0 && (
|
||||||
<RoomsCategory name="People" roomIds={directIds} drawerPostie={drawerPostie} />
|
<RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ isCategorized && [...categories].map(([catId, childIds]) => (
|
{ isCategorized && [...categories].map(([catId, childIds]) => {
|
||||||
<RoomsCategory
|
const rms = [];
|
||||||
key={catId}
|
const dms = [];
|
||||||
spaceId={catId}
|
childIds.forEach((id) => {
|
||||||
name={mx.getRoom(catId).name}
|
if (directs.has(id)) dms.push(id);
|
||||||
roomIds={[...childIds].sort(AtoZ).sort(RoomToDM)}
|
else rms.push(id);
|
||||||
drawerPostie={drawerPostie}
|
});
|
||||||
/>
|
rms.sort(roomIdByAtoZ);
|
||||||
))}
|
dms.sort(roomIdByActivity);
|
||||||
|
return (
|
||||||
|
<RoomsCategory
|
||||||
|
key={catId}
|
||||||
|
spaceId={catId}
|
||||||
|
name={mx.getRoom(catId).name}
|
||||||
|
roomIds={rms.concat(dms)}
|
||||||
|
drawerPostie={drawerPostie}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import './RoomsCategory.scss';
|
import './RoomsCategory.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { selectSpace, selectRoom,openReusableContextMenu } from '../../../client/action/navigation';
|
import { selectSpace, selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useEffect } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords, abbreviateNumber } from '../../../util/common';
|
import { getEventCords, abbreviateNumber } from '../../../util/common';
|
||||||
@@ -23,9 +24,12 @@ function Selector({
|
|||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const noti = initMatrix.notifications;
|
const noti = initMatrix.notifications;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
|
|
||||||
|
const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
|
||||||
|
|
||||||
const [, forceUpdate] = useForceUpdate();
|
const [, forceUpdate] = useForceUpdate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -56,7 +60,8 @@ function Selector({
|
|||||||
imageSrc={isDM ? imageSrc : null}
|
imageSrc={isDM ? imageSrc : null}
|
||||||
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
|
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
|
||||||
isSelected={navigation.selectedRoomId === roomId}
|
isSelected={navigation.selectedRoomId === roomId}
|
||||||
isUnread={noti.hasNoti(roomId)}
|
isMuted={isMuted}
|
||||||
|
isUnread={!isMuted && noti.hasNoti(roomId)}
|
||||||
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
|
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
|
||||||
isAlert={noti.getHighlightNoti(roomId) !== 0}
|
isAlert={noti.getHighlightNoti(roomId) !== 0}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '../../../client/action/navigation';
|
} from '../../../client/action/navigation';
|
||||||
import { moveSpaceShortcut } from '../../../client/action/accountData';
|
import { moveSpaceShortcut } from '../../../client/action/accountData';
|
||||||
import { abbreviateNumber, getEventCords } from '../../../util/common';
|
import { abbreviateNumber, getEventCords } from '../../../util/common';
|
||||||
|
import { isCrossVerified } from '../../../util/matrixUtil';
|
||||||
|
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
||||||
@@ -26,8 +27,12 @@ import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
|||||||
import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
|
import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
|
||||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
|
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
|
||||||
|
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||||
|
|
||||||
import { useSelectedTab } from '../../hooks/useSelectedTab';
|
import { useSelectedTab } from '../../hooks/useSelectedTab';
|
||||||
|
import { useDeviceList } from '../../hooks/useDeviceList';
|
||||||
|
|
||||||
|
import { tabText as settingTabText } from '../settings/Settings';
|
||||||
|
|
||||||
function useNotificationUpdate() {
|
function useNotificationUpdate() {
|
||||||
const { notifications } = initMatrix;
|
const { notifications } = initMatrix;
|
||||||
@@ -72,7 +77,7 @@ function ProfileAvatarMenu() {
|
|||||||
return (
|
return (
|
||||||
<SidebarAvatar
|
<SidebarAvatar
|
||||||
onClick={openSettings}
|
onClick={openSettings}
|
||||||
tooltip={profile.displayName}
|
tooltip="Settings"
|
||||||
avatar={(
|
avatar={(
|
||||||
<Avatar
|
<Avatar
|
||||||
text={profile.displayName}
|
text={profile.displayName}
|
||||||
@@ -85,6 +90,22 @@ function ProfileAvatarMenu() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CrossSigninAlert() {
|
||||||
|
const deviceList = useDeviceList();
|
||||||
|
const unverified = deviceList?.filter((device) => isCrossVerified(device.device_id) === false);
|
||||||
|
|
||||||
|
if (!unverified?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarAvatar
|
||||||
|
className="sidebar__cross-signin-alert"
|
||||||
|
tooltip={`${unverified.length} unverified sessions`}
|
||||||
|
onClick={() => openSettings(settingTabText.SECURITY)}
|
||||||
|
avatar={<Avatar iconSrc={ShieldUserIC} iconColor="var(--ic-danger-normal)" size="normal" />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function FeaturedTab() {
|
function FeaturedTab() {
|
||||||
const { roomList, accountData, notifications } = initMatrix;
|
const { roomList, accountData, notifications } = initMatrix;
|
||||||
const [selectedTab] = useSelectedTab();
|
const [selectedTab] = useSelectedTab();
|
||||||
@@ -358,6 +379,7 @@ function SideBar() {
|
|||||||
notificationBadge={<NotificationBadge alert content={totalInvites} />}
|
notificationBadge={<NotificationBadge alert content={totalInvites} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<CrossSigninAlert />
|
||||||
<ProfileAvatarMenu />
|
<ProfileAvatarMenu />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,3 +58,20 @@
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: var(--bg-surface-border);
|
background-color: var(--bg-surface-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar__cross-signin-alert .avatar-container {
|
||||||
|
box-shadow: var(--bs-danger-border);
|
||||||
|
animation-name: pushRight;
|
||||||
|
animation-duration: 400ms;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pushRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(4px) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,53 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
import ImageUpload from '../../molecules/image-upload/ImageUpload';
|
import ImageUpload from '../../molecules/image-upload/ImageUpload';
|
||||||
import Input from '../../atoms/input/Input';
|
import Input from '../../atoms/input/Input';
|
||||||
|
|
||||||
|
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||||
|
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
import './ProfileEditor.scss';
|
import './ProfileEditor.scss';
|
||||||
|
|
||||||
// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
|
// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
|
||||||
function ProfileEditor({
|
function ProfileEditor({ userId }) {
|
||||||
userId,
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
}) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
const user = mx.getUser(mx.getUserId());
|
||||||
|
|
||||||
const displayNameRef = useRef(null);
|
const displayNameRef = useRef(null);
|
||||||
const bgColor = colorMXID(userId);
|
const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
|
||||||
const [avatarSrc, setAvatarSrc] = useState(null);
|
const [username, setUsername] = useState(user.displayName);
|
||||||
const [disabled, setDisabled] = useState(true);
|
const [disabled, setDisabled] = useState(true);
|
||||||
|
|
||||||
let username = mx.getUser(mx.getUserId()).displayName;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
mx.getProfileInfo(mx.getUserId()).then((info) => {
|
mx.getProfileInfo(mx.getUserId()).then((info) => {
|
||||||
|
if (!isMounted) return;
|
||||||
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
|
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
|
||||||
|
setUsername(info.displayname);
|
||||||
});
|
});
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
// Sets avatar URL and updates the avatar component in profile editor to reflect new upload
|
const handleAvatarUpload = async (url) => {
|
||||||
function handleAvatarUpload(url) {
|
|
||||||
if (url === null) {
|
if (url === null) {
|
||||||
if (confirm('Are you sure you want to remove avatar?')) {
|
const isConfirmed = await confirmDialog(
|
||||||
|
'Remove avatar',
|
||||||
|
'Are you sure that you want to remove avatar?',
|
||||||
|
'Remove',
|
||||||
|
'caution',
|
||||||
|
);
|
||||||
|
if (isConfirmed) {
|
||||||
mx.setAvatarUrl('');
|
mx.setAvatarUrl('');
|
||||||
setAvatarSrc(null);
|
setAvatarSrc(null);
|
||||||
}
|
}
|
||||||
@@ -39,48 +55,72 @@ function ProfileEditor({
|
|||||||
}
|
}
|
||||||
mx.setAvatarUrl(url);
|
mx.setAvatarUrl(url);
|
||||||
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
|
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
|
||||||
}
|
};
|
||||||
|
|
||||||
function saveDisplayName() {
|
const saveDisplayName = () => {
|
||||||
const newDisplayName = displayNameRef.current.value;
|
const newDisplayName = displayNameRef.current.value;
|
||||||
if (newDisplayName !== null && newDisplayName !== username) {
|
if (newDisplayName !== null && newDisplayName !== username) {
|
||||||
mx.setDisplayName(newDisplayName);
|
mx.setDisplayName(newDisplayName);
|
||||||
username = newDisplayName;
|
setUsername(newDisplayName);
|
||||||
setDisabled(true);
|
setDisabled(true);
|
||||||
|
setIsEditing(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function onDisplayNameInputChange() {
|
const onDisplayNameInputChange = () => {
|
||||||
setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
|
setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
|
||||||
}
|
};
|
||||||
function cancelDisplayNameChanges() {
|
const cancelDisplayNameChanges = () => {
|
||||||
displayNameRef.current.value = username;
|
displayNameRef.current.value = username;
|
||||||
onDisplayNameInputChange();
|
onDisplayNameInputChange();
|
||||||
}
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const renderForm = () => (
|
||||||
<form
|
<form
|
||||||
className="profile-editor"
|
className="profile-editor__form"
|
||||||
|
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
|
||||||
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
|
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
|
<ImageUpload
|
||||||
text={username}
|
text={username}
|
||||||
bgColor={bgColor}
|
bgColor={colorMXID(userId)}
|
||||||
imageSrc={avatarSrc}
|
imageSrc={avatarSrc}
|
||||||
onUpload={handleAvatarUpload}
|
onUpload={handleAvatarUpload}
|
||||||
onRequestRemove={() => handleAvatarUpload(null)}
|
onRequestRemove={() => handleAvatarUpload(null)}
|
||||||
/>
|
/>
|
||||||
<div className="profile-editor__input-wrapper">
|
{
|
||||||
<Input
|
isEditing ? renderForm() : renderInfo()
|
||||||
label={`Display name of ${mx.getUserId()}`}
|
}
|
||||||
onChange={onDisplayNameInputChange}
|
</div>
|
||||||
value={mx.getUser(mx.getUserId()).displayName}
|
|
||||||
forwardRef={displayNameRef}
|
|
||||||
/>
|
|
||||||
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
|
|
||||||
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/flex';
|
||||||
|
|
||||||
.profile-editor {
|
.profile-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-editor__input-wrapper {
|
.profile-editor__info,
|
||||||
flex: 1;
|
.profile-editor__form {
|
||||||
min-width: 0;
|
@extend .cp-fx__item-one;
|
||||||
margin-top: 10px;
|
@include dir.side(margin, var(--sp-loose), 0);
|
||||||
|
|
||||||
display: flex;
|
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;
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
& > .input-container {
|
& > .input-container {
|
||||||
flex: 1;
|
@extend .cp-fx__item-one;
|
||||||
}
|
}
|
||||||
& > button {
|
& > button {
|
||||||
height: 46px;
|
height: 46px;
|
||||||
margin-top: var(--sp-normal);
|
margin-top: var(--sp-normal);
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
@include dir.side(margin, var(--sp-normal), 0);
|
@include dir.side(margin, var(--sp-normal), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
|
|||||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
function ModerationTools({
|
function ModerationTools({
|
||||||
roomId, userId,
|
roomId, userId,
|
||||||
@@ -362,7 +363,7 @@ function ProfileViewer() {
|
|||||||
&& (powerLevel < myPowerLevel || userId === mx.getUserId())
|
&& (powerLevel < myPowerLevel || userId === mx.getUserId())
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangePowerLevel = (newPowerLevel) => {
|
const handleChangePowerLevel = async (newPowerLevel) => {
|
||||||
if (newPowerLevel === powerLevel) return;
|
if (newPowerLevel === powerLevel) return;
|
||||||
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
|
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
|
||||||
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
|
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
|
||||||
@@ -370,9 +371,14 @@ function ProfileViewer() {
|
|||||||
const isSharedPower = newPowerLevel === myPowerLevel;
|
const isSharedPower = newPowerLevel === myPowerLevel;
|
||||||
const isDemotingMyself = userId === mx.getUserId();
|
const isDemotingMyself = userId === mx.getUserId();
|
||||||
if (isSharedPower || isDemotingMyself) {
|
if (isSharedPower || isDemotingMyself) {
|
||||||
if (confirm(isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG)) {
|
const isConfirmed = await confirmDialog(
|
||||||
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
'Change power level',
|
||||||
}
|
isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
|
||||||
|
'Change',
|
||||||
|
'caution',
|
||||||
|
);
|
||||||
|
if (!isConfirmed) return;
|
||||||
|
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||||
} else {
|
} else {
|
||||||
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import './PublicRooms.scss';
|
|||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
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 * as roomActions from '../../../client/action/room';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
@@ -179,7 +179,9 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
|
|||||||
}, [joiningRooms]);
|
}, [joiningRooms]);
|
||||||
|
|
||||||
function handleViewRoom(roomId) {
|
function handleViewRoom(roomId) {
|
||||||
selectRoom(roomId);
|
const room = initMatrix.matrixClient.getRoom(roomId);
|
||||||
|
if (room.isSpaceRoom()) selectTab(roomId);
|
||||||
|
else selectRoom(roomId);
|
||||||
onRequestClose();
|
onRequestClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +195,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
|
|||||||
return rooms.map((room) => {
|
return rooms.map((room) => {
|
||||||
const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
|
const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
|
||||||
const name = typeof room.name === 'string' ? room.name : alias;
|
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)?.getMyMembership() === 'join';
|
||||||
return (
|
return (
|
||||||
<RoomTile
|
<RoomTile
|
||||||
key={room.room_id}
|
key={room.room_id}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExistin
|
|||||||
import Search from '../search/Search';
|
import Search from '../search/Search';
|
||||||
import ViewSource from '../view-source/ViewSource';
|
import ViewSource from '../view-source/ViewSource';
|
||||||
import CreateRoom from '../create-room/CreateRoom';
|
import CreateRoom from '../create-room/CreateRoom';
|
||||||
|
import JoinAlias from '../join-alias/JoinAlias';
|
||||||
|
import EmojiVerification from '../emoji-verification/EmojiVerification';
|
||||||
|
|
||||||
|
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
|
||||||
|
|
||||||
function Dialogs() {
|
function Dialogs() {
|
||||||
return (
|
return (
|
||||||
@@ -16,8 +20,12 @@ function Dialogs() {
|
|||||||
<ProfileViewer />
|
<ProfileViewer />
|
||||||
<ShortcutSpaces />
|
<ShortcutSpaces />
|
||||||
<CreateRoom />
|
<CreateRoom />
|
||||||
|
<JoinAlias />
|
||||||
<SpaceAddExisting />
|
<SpaceAddExisting />
|
||||||
<Search />
|
<Search />
|
||||||
|
<EmojiVerification />
|
||||||
|
|
||||||
|
<ReusableDialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ function Windows() {
|
|||||||
const [inviteUser, changeInviteUser] = useState({
|
const [inviteUser, changeInviteUser] = useState({
|
||||||
isOpen: false, roomId: undefined, term: undefined,
|
isOpen: false, roomId: undefined, term: undefined,
|
||||||
});
|
});
|
||||||
const [settings, changeSettings] = useState(false);
|
|
||||||
|
|
||||||
function openInviteList() {
|
function openInviteList() {
|
||||||
changeInviteList(true);
|
changeInviteList(true);
|
||||||
@@ -36,20 +35,15 @@ function Windows() {
|
|||||||
searchTerm,
|
searchTerm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function openSettings() {
|
|
||||||
changeSettings(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
||||||
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
||||||
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
|
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
|
||||||
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
|
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
||||||
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
||||||
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
|
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}
|
searchTerm={inviteUser.searchTerm}
|
||||||
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
|
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
|
||||||
/>
|
/>
|
||||||
<Settings
|
<Settings />
|
||||||
isOpen={settings}
|
|
||||||
onRequestClose={() => changeSettings(false)}
|
|
||||||
/>
|
|
||||||
<SpaceSettings />
|
<SpaceSettings />
|
||||||
<SpaceManage />
|
<SpaceManage />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -64,9 +64,11 @@ function ReadReceipts() {
|
|||||||
onRequestClose={() => setIsOpen(false)}
|
onRequestClose={() => setIsOpen(false)}
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
|
||||||
>
|
>
|
||||||
{
|
<div style={{ marginTop: 'var(--sp-tight)', marginBottom: 'var(--sp-extra-loose)' }}>
|
||||||
readers.map(renderPeople)
|
{
|
||||||
}
|
readers.map(renderPeople)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/app/organisms/room/EventLimit.js
Normal file
35
src/app/organisms/room/EventLimit.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
class EventLimit {
|
||||||
|
constructor() {
|
||||||
|
this._from = 0;
|
||||||
|
|
||||||
|
this.SMALLEST_EVT_HEIGHT = 32;
|
||||||
|
this.PAGES_COUNT = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxEvents() {
|
||||||
|
return Math.round(document.body.clientHeight / this.SMALLEST_EVT_HEIGHT) * this.PAGES_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
get from() {
|
||||||
|
return this._from;
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this._from + this.maxEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFrom(from) {
|
||||||
|
this._from = from < 0 ? 0 : from;
|
||||||
|
}
|
||||||
|
|
||||||
|
paginate(backwards, limit, timelineLength) {
|
||||||
|
this._from = backwards ? this._from - limit : this._from + limit;
|
||||||
|
|
||||||
|
if (!backwards && this.length > timelineLength) {
|
||||||
|
this._from = timelineLength - this.maxEvents;
|
||||||
|
}
|
||||||
|
if (this._from < 0) this._from = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventLimit;
|
||||||
@@ -9,6 +9,7 @@ import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil
|
|||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
|
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
|
||||||
import AsyncSearch from '../../../util/AsyncSearch';
|
import AsyncSearch from '../../../util/AsyncSearch';
|
||||||
|
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
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 SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
import CrossIC from '../../../../public/res/ic/outlined/cross.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) {
|
function simplyfiMembers(members) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
return members.map((member) => ({
|
return members.map((member) => ({
|
||||||
@@ -111,7 +92,7 @@ function PeopleDrawer({ roomId }) {
|
|||||||
setMemberList(
|
setMemberList(
|
||||||
simplyfiMembers(
|
simplyfiMembers(
|
||||||
getMembersWithMembership(membership)
|
getMembersWithMembership(membership)
|
||||||
.sort(AtoZ).sort(sortByPowerLevel),
|
.sort(memberByAtoZ).sort(memberByPowerLevel),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import './Room.scss';
|
|||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
|
||||||
import settings from '../../../client/state/settings';
|
import settings from '../../../client/state/settings';
|
||||||
import RoomTimeline from '../../../client/state/RoomTimeline';
|
import RoomTimeline from '../../../client/state/RoomTimeline';
|
||||||
|
import navigation from '../../../client/state/navigation';
|
||||||
|
import { openNavigation } from '../../../client/action/navigation';
|
||||||
|
|
||||||
import Welcome from '../welcome/Welcome';
|
import Welcome from '../welcome/Welcome';
|
||||||
import RoomView from './RoomView';
|
import RoomView from './RoomView';
|
||||||
@@ -13,34 +14,50 @@ import RoomSettings from './RoomSettings';
|
|||||||
import PeopleDrawer from './PeopleDrawer';
|
import PeopleDrawer from './PeopleDrawer';
|
||||||
|
|
||||||
function Room() {
|
function Room() {
|
||||||
const [roomTimeline, setRoomTimeline] = useState(null);
|
const [roomInfo, setRoomInfo] = useState({
|
||||||
const [eventId, setEventId] = useState(null);
|
roomTimeline: null,
|
||||||
|
eventId: null,
|
||||||
|
});
|
||||||
const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
|
const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
|
||||||
|
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const handleRoomSelected = (rId, pRoomId, eId) => {
|
|
||||||
if (mx.getRoom(rId)) {
|
|
||||||
setRoomTimeline(new RoomTimeline(rId, initMatrix.notifications));
|
|
||||||
setEventId(eId);
|
|
||||||
} else {
|
|
||||||
// TODO: add ability to join room if roomId is invalid
|
|
||||||
setRoomTimeline(null);
|
|
||||||
setEventId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const handleRoomSelected = (rId, pRoomId, eId) => {
|
||||||
|
roomInfo.roomTimeline?.removeInternalListeners();
|
||||||
|
if (mx.getRoom(rId)) {
|
||||||
|
setRoomInfo({
|
||||||
|
roomTimeline: new RoomTimeline(rId),
|
||||||
|
eventId: eId ?? null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// TODO: add ability to join room if roomId is invalid
|
||||||
|
setRoomInfo({
|
||||||
|
roomTimeline: null,
|
||||||
|
eventId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
||||||
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
|
||||||
|
};
|
||||||
|
}, [roomInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
|
||||||
|
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
||||||
|
return () => {
|
||||||
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
|
||||||
roomTimeline?.removeInternalListeners();
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (roomTimeline === null) return <Welcome />;
|
const { roomTimeline, eventId } = roomInfo;
|
||||||
|
if (roomTimeline === null) {
|
||||||
|
setTimeout(() => openNavigation());
|
||||||
|
return <Welcome />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="room">
|
<div className="room">
|
||||||
@@ -48,7 +65,7 @@ function Room() {
|
|||||||
<RoomSettings roomId={roomTimeline.roomId} />
|
<RoomSettings roomId={roomTimeline.roomId} />
|
||||||
<RoomView roomTimeline={roomTimeline} eventId={eventId} />
|
<RoomView roomTimeline={roomTimeline} eventId={eventId} />
|
||||||
</div>
|
</div>
|
||||||
{ isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
|
{isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@use '../../partials/flex';
|
@use '../../partials/flex';
|
||||||
|
@use '../../partials/screen';
|
||||||
|
|
||||||
.room {
|
.room {
|
||||||
@extend .cp-fx__row;
|
@extend .cp-fx__row;
|
||||||
@@ -10,3 +11,9 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.room .people-drawer {
|
||||||
|
@include screen.smallerThan(tabletBreakpoint) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
|||||||
import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg';
|
import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg';
|
||||||
|
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||||
|
|
||||||
const tabText = {
|
const tabText = {
|
||||||
GENERAL: 'General',
|
GENERAL: 'General',
|
||||||
@@ -85,10 +86,15 @@ function GeneralSettings({ roomId }) {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (confirm('Are you really want to leave this room?')) {
|
const isConfirmed = await confirmDialog(
|
||||||
roomActions.leave(roomId);
|
'Leave room',
|
||||||
}
|
`Are you sure that you want to leave "${room.name}" room?`,
|
||||||
|
'Leave',
|
||||||
|
'danger',
|
||||||
|
);
|
||||||
|
if (!isConfirmed) return;
|
||||||
|
roomActions.leave(roomId);
|
||||||
}}
|
}}
|
||||||
iconSrc={LeaveArrowIC}
|
iconSrc={LeaveArrowIC}
|
||||||
>
|
>
|
||||||
@@ -123,7 +129,7 @@ function SecuritySettings({ roomId }) {
|
|||||||
<RoomEncryption roomId={roomId} />
|
<RoomEncryption roomId={roomId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="room-settings__card">
|
<div className="room-settings__card">
|
||||||
<MenuHeader>Message history visibility (Who can read history)</MenuHeader>
|
<MenuHeader>Message history visibility</MenuHeader>
|
||||||
<RoomHistoryVisibility roomId={roomId} />
|
<RoomHistoryVisibility roomId={roomId} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
@use '../../partials/flex';
|
@use '../../partials/flex';
|
||||||
|
@use '../../partials/screen';
|
||||||
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.room-view {
|
.room-view {
|
||||||
@extend .cp-fx__column;
|
@extend .cp-fx__column;
|
||||||
@@ -18,6 +20,12 @@
|
|||||||
box-shadow: var(--bs-popup);
|
box-shadow: var(--bs-popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .header {
|
||||||
|
@include screen.smallerThan(mobileBreakpoint) {
|
||||||
|
padding: 0 var(--sp-tight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__content-wrapper {
|
&__content-wrapper {
|
||||||
@extend .cp-fx__item-one;
|
@extend .cp-fx__item-one;
|
||||||
@extend .cp-fx__column;
|
@extend .cp-fx__column;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import AsyncSearch from '../../../util/AsyncSearch';
|
|||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
|
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
|
||||||
|
import { addRecentEmoji } from '../emoji-board/recent';
|
||||||
|
|
||||||
const commands = [{
|
const commands = [{
|
||||||
name: 'markdown',
|
name: 'markdown',
|
||||||
@@ -43,7 +44,9 @@ const commands = [{
|
|||||||
}, {
|
}, {
|
||||||
name: 'leave',
|
name: 'leave',
|
||||||
description: 'Leave current room',
|
description: 'Leave current room',
|
||||||
exe: (roomId) => roomActions.leave(roomId),
|
exe: (roomId) => {
|
||||||
|
roomActions.leave(roomId);
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
name: 'invite',
|
name: 'invite',
|
||||||
isOptions: true,
|
isOptions: true,
|
||||||
@@ -237,6 +240,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
|||||||
viewEvent.emit('cmd_fired');
|
viewEvent.emit('cmd_fired');
|
||||||
}
|
}
|
||||||
if (myCmd.prefix === ':') {
|
if (myCmd.prefix === ':') {
|
||||||
|
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
|
||||||
viewEvent.emit('cmd_fired', {
|
viewEvent.emit('cmd_fired', {
|
||||||
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
|
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
&__info {
|
&__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@include dir.side(margin, 10px, 14px);
|
@include dir.side(margin, 14px, 10px);
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ import React, {
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewContent.scss';
|
import './RoomViewContent.scss';
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
import {
|
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
|
||||||
diffMinutes, isInSameDay, Throttle, getScrollInfo,
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
} from '../../../util/common';
|
|
||||||
|
|
||||||
import Divider from '../../atoms/divider/Divider';
|
import Divider from '../../atoms/divider/Divider';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
@@ -27,17 +26,15 @@ import TimelineChange from '../../molecules/message/TimelineChange';
|
|||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
import { parseTimelineChange } from './common';
|
import { parseTimelineChange } from './common';
|
||||||
|
import TimelineScroll from './TimelineScroll';
|
||||||
|
import EventLimit from './EventLimit';
|
||||||
|
|
||||||
const DEFAULT_MAX_EVENTS = 50;
|
|
||||||
const PAG_LIMIT = 30;
|
const PAG_LIMIT = 30;
|
||||||
const MAX_MSG_DIFF_MINUTES = 5;
|
const MAX_MSG_DIFF_MINUTES = 5;
|
||||||
const PLACEHOLDER_COUNT = 2;
|
const PLACEHOLDER_COUNT = 2;
|
||||||
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
||||||
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
||||||
|
|
||||||
const SMALLEST_MSG_HEIGHT = 32;
|
|
||||||
const PAGES_COUNT = 4;
|
|
||||||
|
|
||||||
function loadingMsgPlaceholders(key, count = 2) {
|
function loadingMsgPlaceholders(key, count = 2) {
|
||||||
const pl = [];
|
const pl = [];
|
||||||
const genPlaceholders = () => {
|
const genPlaceholders = () => {
|
||||||
@@ -54,21 +51,54 @@ function loadingMsgPlaceholders(key, count = 2) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function genRoomIntro(mEvent, roomTimeline) {
|
function RoomIntroContainer({ event, timeline }) {
|
||||||
|
const [, nameForceUpdate] = useForceUpdate();
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
const { roomList } = initMatrix;
|
||||||
const isDM = initMatrix.roomList.directs.has(roomTimeline.roomId);
|
const { room } = timeline;
|
||||||
let avatarSrc = roomTimeline.room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
|
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||||
avatarSrc = isDM ? roomTimeline.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
|
const isDM = roomList.directs.has(timeline.roomId);
|
||||||
|
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
|
||||||
|
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
|
||||||
|
|
||||||
|
const heading = isDM ? room.name : `Welcome to ${room.name}`;
|
||||||
|
const topic = twemojify(roomTopic || '', undefined, true);
|
||||||
|
const nameJsx = twemojify(room.name);
|
||||||
|
const desc = isDM
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
This is the beginning of your direct message history with @
|
||||||
|
<b>{nameJsx}</b>
|
||||||
|
{'. '}
|
||||||
|
{topic}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
{'This is the beginning of the '}
|
||||||
|
<b>{nameJsx}</b>
|
||||||
|
{' room. '}
|
||||||
|
{topic}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleUpdate = () => nameForceUpdate();
|
||||||
|
|
||||||
|
roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
|
||||||
|
return () => {
|
||||||
|
roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomIntro
|
<RoomIntro
|
||||||
key={mEvent ? mEvent.getId() : 'room-intro'}
|
roomId={timeline.roomId}
|
||||||
roomId={roomTimeline.roomId}
|
|
||||||
avatarSrc={avatarSrc}
|
avatarSrc={avatarSrc}
|
||||||
name={roomTimeline.room.name}
|
name={room.name}
|
||||||
heading={`Welcome to ${roomTimeline.room.name}`}
|
heading={twemojify(heading)}
|
||||||
desc={`This is the beginning of ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
|
desc={desc}
|
||||||
time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
|
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -124,178 +154,7 @@ function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus = false) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineScroll extends EventEmitter {
|
function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
|
||||||
constructor(target) {
|
|
||||||
super();
|
|
||||||
if (target === null) {
|
|
||||||
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
|
|
||||||
}
|
|
||||||
this.scroll = target;
|
|
||||||
|
|
||||||
this.backwards = false;
|
|
||||||
this.inTopHalf = false;
|
|
||||||
this.maxEvents = DEFAULT_MAX_EVENTS;
|
|
||||||
|
|
||||||
this.isScrollable = false;
|
|
||||||
this.top = 0;
|
|
||||||
this.bottom = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.viewHeight = 0;
|
|
||||||
|
|
||||||
this.topMsg = null;
|
|
||||||
this.bottomMsg = null;
|
|
||||||
this.diff = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, maxScrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// restore scroll using previous calc by this._updateTopBottomMsg() and this._calcDiff.
|
|
||||||
tryRestoringScroll() {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
|
|
||||||
let scrollTop = 0;
|
|
||||||
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
|
|
||||||
if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
|
|
||||||
else scrollTop = ot - this.diff;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, scrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToIndex(index, offset = 0) {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
|
||||||
const offsetTop = msgs[index]?.offsetTop;
|
|
||||||
|
|
||||||
if (offsetTop === undefined) return;
|
|
||||||
// if msg is already in visible are we don't need to scroll to that
|
|
||||||
if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
|
|
||||||
const to = offsetTop - offset;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
_scrollTo(scrollInfo, scrollTop) {
|
|
||||||
this.scroll.scrollTop = scrollTop;
|
|
||||||
|
|
||||||
// browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
|
|
||||||
// so here we flag that the upcoming 'onscroll' event is
|
|
||||||
// emitted as side effect of assigning 'this.scroll.scrollTop' above
|
|
||||||
// only if it's changes.
|
|
||||||
// by doing so we prevent this._updateCalc() from calc again.
|
|
||||||
if (scrollTop !== this.top) {
|
|
||||||
this.scrolledByCode = true;
|
|
||||||
}
|
|
||||||
const sInfo = { ...scrollInfo };
|
|
||||||
|
|
||||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
|
||||||
|
|
||||||
sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
|
|
||||||
this._updateCalc(sInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we maintain reference of top and bottom messages
|
|
||||||
// to restore the scroll position when
|
|
||||||
// messages gets removed from either end and added to other.
|
|
||||||
_updateTopBottomMsg() {
|
|
||||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
|
||||||
const lMsgIndex = msgs.length - 1;
|
|
||||||
|
|
||||||
this.topMsg = msgs[0]?.className === 'ph-msg'
|
|
||||||
? msgs[PLACEHOLDER_COUNT]
|
|
||||||
: msgs[0];
|
|
||||||
this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
|
|
||||||
? msgs[lMsgIndex - PLACEHOLDER_COUNT]
|
|
||||||
: msgs[lMsgIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
// we calculate the difference between first/last message and current scrollTop.
|
|
||||||
// if we are going above we calc diff between first and scrollTop
|
|
||||||
// else otherwise.
|
|
||||||
// NOTE: This will help to restore the scroll when msgs get's removed
|
|
||||||
// from one end and added to other end
|
|
||||||
_calcDiff(scrollInfo) {
|
|
||||||
if (!this.topMsg || !this.bottomMsg) return 0;
|
|
||||||
if (this.inTopHalf) {
|
|
||||||
return this.topMsg.offsetTop - scrollInfo.top;
|
|
||||||
}
|
|
||||||
return this.bottomMsg.offsetTop - scrollInfo.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_calcMaxEvents(scrollInfo) {
|
|
||||||
return Math.round(scrollInfo.viewHeight / SMALLEST_MSG_HEIGHT) * PAGES_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateCalc(scrollInfo) {
|
|
||||||
const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
|
|
||||||
const scrollMiddle = scrollInfo.top + halfViewHeight;
|
|
||||||
const lastMiddle = this.top + halfViewHeight;
|
|
||||||
|
|
||||||
this.backwards = scrollMiddle < lastMiddle;
|
|
||||||
this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
|
|
||||||
|
|
||||||
this.isScrollable = scrollInfo.isScrollable;
|
|
||||||
this.top = scrollInfo.top;
|
|
||||||
this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
|
|
||||||
this.height = scrollInfo.height;
|
|
||||||
|
|
||||||
// only calculate maxEvents if viewHeight change
|
|
||||||
if (this.viewHeight !== scrollInfo.viewHeight) {
|
|
||||||
this.maxEvents = this._calcMaxEvents(scrollInfo);
|
|
||||||
this.viewHeight = scrollInfo.viewHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateTopBottomMsg();
|
|
||||||
this.diff = this._calcDiff(scrollInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
calcScroll() {
|
|
||||||
if (this.scrolledByCode) {
|
|
||||||
this.scrolledByCode = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
this._updateCalc(scrollInfo);
|
|
||||||
|
|
||||||
this.emit('scroll', this.backwards);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let timelineScroll = null;
|
|
||||||
let jumpToItemIndex = -1;
|
|
||||||
const throttle = new Throttle();
|
|
||||||
const limit = {
|
|
||||||
from: 0,
|
|
||||||
getMaxEvents() {
|
|
||||||
return timelineScroll?.maxEvents ?? DEFAULT_MAX_EVENTS;
|
|
||||||
},
|
|
||||||
getEndIndex() {
|
|
||||||
return this.from + this.getMaxEvents();
|
|
||||||
},
|
|
||||||
calcNextFrom(backwards, tLength) {
|
|
||||||
let newFrom = backwards ? this.from - PAG_LIMIT : this.from + PAG_LIMIT;
|
|
||||||
if (!backwards && newFrom + this.getMaxEvents() > tLength) {
|
|
||||||
newFrom = tLength - this.getMaxEvents();
|
|
||||||
}
|
|
||||||
if (newFrom < 0) newFrom = 0;
|
|
||||||
return newFrom;
|
|
||||||
},
|
|
||||||
setFrom(from) {
|
|
||||||
if (from < 0) {
|
|
||||||
this.from = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.from = from;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function useTimeline(roomTimeline, eventId, readEventStore) {
|
|
||||||
const [timelineInfo, setTimelineInfo] = useState(null);
|
const [timelineInfo, setTimelineInfo] = useState(null);
|
||||||
|
|
||||||
const setEventTimeline = async (eId) => {
|
const setEventTimeline = async (eId) => {
|
||||||
@@ -309,6 +168,7 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
const initTimeline = (eId) => {
|
const initTimeline = (eId) => {
|
||||||
// NOTICE: eId can be id of readUpto, reply or specific event.
|
// NOTICE: eId can be id of readUpto, reply or specific event.
|
||||||
// readUpTo: when user click jump to unread message button.
|
// readUpTo: when user click jump to unread message button.
|
||||||
@@ -320,20 +180,19 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
|||||||
|
|
||||||
if (isSpecificEvent) {
|
if (isSpecificEvent) {
|
||||||
focusEventIndex = roomTimeline.getEventIndex(eId);
|
focusEventIndex = roomTimeline.getEventIndex(eId);
|
||||||
} else if (!readEventStore.getItem()) {
|
}
|
||||||
|
if (!readUptoEvtStore.getItem() && roomTimeline.hasEventInTimeline(readUpToId)) {
|
||||||
// either opening live timeline or jump to unread.
|
// either opening live timeline or jump to unread.
|
||||||
focusEventIndex = roomTimeline.getUnreadEventIndex(readUpToId);
|
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
if (roomTimeline.hasEventInTimeline(readUpToId)) {
|
}
|
||||||
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
if (readUptoEvtStore.getItem() && !isSpecificEvent) {
|
||||||
}
|
focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId());
|
||||||
} else {
|
|
||||||
focusEventIndex = roomTimeline.getUnreadEventIndex(readEventStore.getItem().getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focusEventIndex > -1) {
|
if (focusEventIndex > -1) {
|
||||||
limit.setFrom(focusEventIndex - Math.round(limit.getMaxEvents() / 2));
|
limit.setFrom(focusEventIndex - Math.round(limit.maxEvents / 2));
|
||||||
} else {
|
} else {
|
||||||
limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents());
|
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||||
}
|
}
|
||||||
setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
|
setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
|
||||||
};
|
};
|
||||||
@@ -342,7 +201,6 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
|||||||
setEventTimeline(eventId);
|
setEventTimeline(eventId);
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
|
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
|
||||||
roomTimeline.removeInternalListeners();
|
|
||||||
limit.setFrom(0);
|
limit.setFrom(0);
|
||||||
};
|
};
|
||||||
}, [roomTimeline, eventId]);
|
}, [roomTimeline, eventId]);
|
||||||
@@ -350,36 +208,45 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
|||||||
return timelineInfo;
|
return timelineInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
function usePaginate(
|
||||||
|
roomTimeline,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
) {
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOnPagination = (backwards, loaded) => {
|
const handlePaginatedFromServer = (backwards, loaded) => {
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
if (loaded === 0) return;
|
if (loaded === 0) return;
|
||||||
if (!readEventStore.getItem()) {
|
if (!readUptoEvtStore.getItem()) {
|
||||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||||
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
}
|
}
|
||||||
limit.setFrom(limit.calcNextFrom(backwards, roomTimeline.timeline.length));
|
limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
|
||||||
setTimeout(() => setInfo({
|
setTimeout(() => setInfo({
|
||||||
backwards,
|
backwards,
|
||||||
loaded,
|
loaded,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handleOnPagination);
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handleOnPagination);
|
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const autoPaginate = useCallback(async () => {
|
const autoPaginate = useCallback(async () => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
if (roomTimeline.isOngoingPagination) return;
|
if (roomTimeline.isOngoingPagination) return;
|
||||||
const tLength = roomTimeline.timeline.length;
|
const tLength = roomTimeline.timeline.length;
|
||||||
|
|
||||||
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
||||||
if (limit.getEndIndex() < tLength) {
|
if (limit.length < tLength) {
|
||||||
// paginate from memory
|
// paginate from memory
|
||||||
limit.setFrom(limit.calcNextFrom(false, tLength));
|
limit.paginate(false, PAG_LIMIT, tLength);
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
} else if (roomTimeline.canPaginateForward()) {
|
} else if (roomTimeline.canPaginateForward()) {
|
||||||
// paginate from server.
|
// paginate from server.
|
||||||
@@ -390,7 +257,7 @@ function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
|||||||
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
||||||
if (limit.from > 0) {
|
if (limit.from > 0) {
|
||||||
// paginate from memory
|
// paginate from memory
|
||||||
limit.setFrom(limit.calcNextFrom(true, tLength));
|
limit.paginate(true, PAG_LIMIT, tLength);
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
} else if (roomTimeline.canPaginateBackward()) {
|
} else if (roomTimeline.canPaginateBackward()) {
|
||||||
// paginate from server.
|
// paginate from server.
|
||||||
@@ -402,28 +269,39 @@ function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
|||||||
return [info, autoPaginate];
|
return [info, autoPaginate];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdateLimit) {
|
function useHandleScroll(
|
||||||
|
roomTimeline,
|
||||||
|
autoPaginate,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
) {
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// emit event to toggle scrollToBottom button visibility
|
// emit event to toggle scrollToBottom button visibility
|
||||||
const isAtBottom = (
|
const isAtBottom = (
|
||||||
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
||||||
&& limit.getEndIndex() >= roomTimeline.timeline.length
|
&& limit.length >= roomTimeline.timeline.length
|
||||||
);
|
);
|
||||||
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
||||||
if (isAtBottom && readEventStore.getItem()) {
|
if (isAtBottom && readUptoEvtStore.getItem()) {
|
||||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const handleScrollToLive = useCallback(() => {
|
const handleScrollToLive = useCallback(() => {
|
||||||
if (readEventStore.getItem()) {
|
const timelineScroll = timelineScrollRef.current;
|
||||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
const limit = eventLimitRef.current;
|
||||||
|
if (readUptoEvtStore.getItem()) {
|
||||||
|
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||||
}
|
}
|
||||||
if (roomTimeline.isServingLiveTimeline()) {
|
if (roomTimeline.isServingLiveTimeline()) {
|
||||||
limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents());
|
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||||
timelineScroll.scrollToBottom();
|
timelineScroll.scrollToBottom();
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
return;
|
return;
|
||||||
@@ -434,48 +312,46 @@ function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdate
|
|||||||
return [handleScroll, handleScrollToLive];
|
return [handleScroll, handleScrollToLive];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEventArrive(roomTimeline, readEventStore) {
|
function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef) {
|
||||||
const myUserId = initMatrix.matrixClient.getUserId();
|
const myUserId = initMatrix.matrixClient.getUserId();
|
||||||
const [newEvent, setEvent] = useState(null);
|
const [newEvent, setEvent] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sendReadReceipt = (event) => {
|
const timelineScroll = timelineScrollRef.current;
|
||||||
if (event.isSending()) return;
|
const limit = eventLimitRef.current;
|
||||||
|
const trySendReadReceipt = (event) => {
|
||||||
if (myUserId === event.getSender()) {
|
if (myUserId === event.getSender()) {
|
||||||
roomTimeline.markAllAsRead();
|
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const readUpToEvent = readEventStore.getItem();
|
const readUpToEvent = readUptoEvtStore.getItem();
|
||||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||||
|
const isUnread = readUpToEvent ? readUpToEvent?.getId() === readUpToId : true;
|
||||||
|
|
||||||
// if user doesn't have focus on app don't mark messages as read.
|
if (isUnread === false) {
|
||||||
if (document.visibilityState === 'hidden' || timelineScroll.bottom >= 16) {
|
if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
|
||||||
if (readUpToEvent === readUpToId) return;
|
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||||
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
} else {
|
||||||
|
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// user has not mark room as read
|
|
||||||
const isUnreadMsg = readUpToEvent?.getId() === readUpToId;
|
|
||||||
if (!isUnreadMsg) {
|
|
||||||
roomTimeline.markAllAsRead();
|
|
||||||
}
|
|
||||||
const { timeline } = roomTimeline;
|
const { timeline } = roomTimeline;
|
||||||
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToEvent?.getId();
|
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
|
||||||
if (unreadMsgIsLast) {
|
if (unreadMsgIsLast) {
|
||||||
roomTimeline.markAllAsRead();
|
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEvent = (event) => {
|
const handleEvent = (event) => {
|
||||||
const tLength = roomTimeline.timeline.length;
|
const tLength = roomTimeline.timeline.length;
|
||||||
const isUserViewingLive = (
|
const isViewingLive = roomTimeline.isServingLiveTimeline() && limit.length >= tLength - 1;
|
||||||
roomTimeline.isServingLiveTimeline()
|
const isAttached = timelineScroll.bottom < SCROLL_TRIGGER_POS;
|
||||||
&& limit.getEndIndex() >= tLength - 1
|
|
||||||
&& timelineScroll.bottom < SCROLL_TRIGGER_POS
|
if (isViewingLive && isAttached) {
|
||||||
);
|
limit.setFrom(tLength - limit.maxEvents);
|
||||||
if (isUserViewingLive) {
|
trySendReadReceipt(event);
|
||||||
limit.setFrom(tLength - limit.getMaxEvents());
|
|
||||||
sendReadReceipt(event);
|
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -484,11 +360,8 @@ function useEventArrive(roomTimeline, readEventStore) {
|
|||||||
setEvent(event);
|
setEvent(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isUserDitchedLive = (
|
|
||||||
roomTimeline.isServingLiveTimeline()
|
if (isViewingLive) {
|
||||||
&& limit.getEndIndex() >= tLength - 1
|
|
||||||
);
|
|
||||||
if (isUserDitchedLive) {
|
|
||||||
// This stateUpdate will help to put the
|
// This stateUpdate will help to put the
|
||||||
// loading msg placeholder at bottom
|
// loading msg placeholder at bottom
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
@@ -505,39 +378,52 @@ function useEventArrive(roomTimeline, readEventStore) {
|
|||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
useEffect(() => {
|
return newEvent;
|
||||||
if (!roomTimeline.initialized) return;
|
|
||||||
if (timelineScroll.bottom < 16
|
|
||||||
&& !roomTimeline.canPaginateForward()
|
|
||||||
&& document.visibilityState === 'visible') {
|
|
||||||
timelineScroll.scrollToBottom();
|
|
||||||
} else {
|
|
||||||
timelineScroll.tryRestoringScroll();
|
|
||||||
}
|
|
||||||
}, [newEvent, roomTimeline]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let jumpToItemIndex = -1;
|
||||||
|
|
||||||
function RoomViewContent({ eventId, roomTimeline }) {
|
function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
|
const [throttle] = useState(new Throttle());
|
||||||
|
|
||||||
const timelineSVRef = useRef(null);
|
const timelineSVRef = useRef(null);
|
||||||
const readEventStore = useStore(roomTimeline);
|
const timelineScrollRef = useRef(null);
|
||||||
const timelineInfo = useTimeline(roomTimeline, eventId, readEventStore);
|
const eventLimitRef = useRef(null);
|
||||||
|
|
||||||
|
const readUptoEvtStore = useStore(roomTimeline);
|
||||||
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
||||||
const [paginateInfo, autoPaginate] = usePaginate(roomTimeline, readEventStore, forceUpdateLimit);
|
|
||||||
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
const timelineInfo = useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef);
|
||||||
roomTimeline, autoPaginate, readEventStore, forceUpdateLimit,
|
const [paginateInfo, autoPaginate] = usePaginate(
|
||||||
|
roomTimeline,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
);
|
);
|
||||||
useEventArrive(roomTimeline, readEventStore);
|
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
||||||
|
roomTimeline,
|
||||||
|
autoPaginate,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
);
|
||||||
|
const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
|
||||||
|
|
||||||
const { timeline } = roomTimeline;
|
const { timeline } = roomTimeline;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!roomTimeline.initialized) {
|
if (!roomTimeline.initialized) {
|
||||||
timelineScroll = new TimelineScroll(timelineSVRef.current);
|
timelineScrollRef.current = new TimelineScroll(timelineSVRef.current);
|
||||||
|
eventLimitRef.current = new EventLimit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// when active timeline changes
|
// when active timeline changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return undefined;
|
if (!roomTimeline.initialized) return undefined;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
|
||||||
if (timeline.length > 0) {
|
if (timeline.length > 0) {
|
||||||
if (jumpToItemIndex === -1) {
|
if (jumpToItemIndex === -1) {
|
||||||
@@ -547,19 +433,17 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
}
|
}
|
||||||
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
|
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
|
||||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||||
if (readEventStore.getItem()?.getId() === readUpToId || readUpToId === null) {
|
if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
|
||||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jumpToItemIndex = -1;
|
jumpToItemIndex = -1;
|
||||||
}
|
}
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
|
|
||||||
timelineScroll.on('scroll', handleScroll);
|
|
||||||
roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||||
return () => {
|
return () => {
|
||||||
if (timelineSVRef.current === null) return;
|
if (timelineSVRef.current === null) return;
|
||||||
timelineScroll.removeListener('scroll', handleScroll);
|
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||||
};
|
};
|
||||||
}, [timelineInfo]);
|
}, [timelineInfo]);
|
||||||
@@ -567,6 +451,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
// when paginating from server
|
// when paginating from server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
}, [paginateInfo]);
|
}, [paginateInfo]);
|
||||||
@@ -574,49 +459,68 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
// when paginating locally
|
// when paginating locally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
}, [onLimitUpdate]);
|
}, [onLimitUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
if (!roomTimeline.initialized) return;
|
||||||
|
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
|
||||||
|
timelineScroll.scrollToBottom();
|
||||||
|
} else {
|
||||||
|
timelineScroll.tryRestoringScroll();
|
||||||
|
}
|
||||||
|
}, [newEvent]);
|
||||||
|
|
||||||
const handleTimelineScroll = (event) => {
|
const handleTimelineScroll = (event) => {
|
||||||
const { target } = event;
|
const timelineScroll = timelineScrollRef.current;
|
||||||
if (!target) return;
|
if (!event.target) return;
|
||||||
throttle._(() => timelineScroll?.calcScroll(), 400)(target);
|
|
||||||
|
throttle._(() => {
|
||||||
|
const backwards = timelineScroll?.calcScroll();
|
||||||
|
if (typeof backwards !== 'boolean') return;
|
||||||
|
handleScroll(backwards);
|
||||||
|
}, 200)();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTimeline = () => {
|
const renderTimeline = () => {
|
||||||
const tl = [];
|
const tl = [];
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
|
|
||||||
let itemCountIndex = 0;
|
let itemCountIndex = 0;
|
||||||
jumpToItemIndex = -1;
|
jumpToItemIndex = -1;
|
||||||
const readEvent = readEventStore.getItem();
|
const readUptoEvent = readUptoEvtStore.getItem();
|
||||||
let unreadDivider = false;
|
let unreadDivider = false;
|
||||||
|
|
||||||
if (roomTimeline.canPaginateBackward() || limit.from > 0) {
|
if (roomTimeline.canPaginateBackward() || limit.from > 0) {
|
||||||
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
||||||
itemCountIndex += PLACEHOLDER_COUNT;
|
itemCountIndex += PLACEHOLDER_COUNT;
|
||||||
}
|
}
|
||||||
for (let i = limit.from; i < limit.getEndIndex(); i += 1) {
|
for (let i = limit.from; i < limit.length; i += 1) {
|
||||||
if (i >= timeline.length) break;
|
if (i >= timeline.length) break;
|
||||||
const mEvent = timeline[i];
|
const mEvent = timeline[i];
|
||||||
const prevMEvent = timeline[i - 1] ?? null;
|
const prevMEvent = timeline[i - 1] ?? null;
|
||||||
|
|
||||||
if (i === 0 && !roomTimeline.canPaginateBackward()) {
|
if (i === 0 && !roomTimeline.canPaginateBackward()) {
|
||||||
if (mEvent.getType() === 'm.room.create') {
|
if (mEvent.getType() === 'm.room.create') {
|
||||||
tl.push(genRoomIntro(mEvent, roomTimeline));
|
tl.push(
|
||||||
|
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
|
||||||
|
);
|
||||||
itemCountIndex += 1;
|
itemCountIndex += 1;
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
tl.push(genRoomIntro(undefined, roomTimeline));
|
tl.push(<RoomIntroContainer key="room-intro" event={null} timeline={roomTimeline} />);
|
||||||
itemCountIndex += 1;
|
itemCountIndex += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isNewEvent = false;
|
let isNewEvent = false;
|
||||||
if (!unreadDivider) {
|
if (!unreadDivider) {
|
||||||
unreadDivider = (readEvent
|
unreadDivider = (readUptoEvent
|
||||||
&& prevMEvent?.getTs() <= readEvent.getTs()
|
&& prevMEvent?.getTs() <= readUptoEvent.getTs()
|
||||||
&& readEvent.getTs() < mEvent.getTs());
|
&& readUptoEvent.getTs() < mEvent.getTs());
|
||||||
if (unreadDivider) {
|
if (unreadDivider) {
|
||||||
isNewEvent = true;
|
isNewEvent = true;
|
||||||
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
|
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
|
||||||
@@ -637,7 +541,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
|
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
|
||||||
itemCountIndex += 1;
|
itemCountIndex += 1;
|
||||||
}
|
}
|
||||||
if (roomTimeline.canPaginateForward() || limit.getEndIndex() < timeline.length) {
|
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
|
||||||
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import './RoomViewFloating.scss';
|
|||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
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 TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||||
|
|
||||||
import { getUsersActionJsx } from './common';
|
import { getUsersActionJsx } from './common';
|
||||||
@@ -23,7 +24,7 @@ function useJumpToEvent(roomTimeline) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelJumpToEvent = () => {
|
const cancelJumpToEvent = () => {
|
||||||
roomTimeline.markAllAsRead();
|
markAsRead(roomTimeline.roomId);
|
||||||
setEventId(null);
|
setEventId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,11 +37,12 @@ function useJumpToEvent(roomTimeline) {
|
|||||||
setEventId(readEventId);
|
setEventId(readEventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { notifications } = initMatrix;
|
||||||
const handleMarkAsRead = () => setEventId(null);
|
const handleMarkAsRead = () => setEventId(null);
|
||||||
roomTimeline.on(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
|
notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
|
notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead);
|
||||||
setEventId(null);
|
setEventId(null);
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
@@ -96,28 +98,21 @@ function RoomViewFloating({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
|
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
|
||||||
<Button onClick={jumpToEvent} variant="primary">
|
<Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
|
||||||
<Text variant="b2">Jump to unread</Text>
|
<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>
|
</Button>
|
||||||
<IconButton
|
|
||||||
onClick={cancelJumpToEvent}
|
|
||||||
variant="primary"
|
|
||||||
size="extra-small"
|
|
||||||
src={TickMarkIC}
|
|
||||||
tooltipPlacement="bottom"
|
|
||||||
tooltip="Mark as read"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
|
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
|
||||||
<div className="bouncing-loader"><div /></div>
|
<div className="bouncing-loader"><div /></div>
|
||||||
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
|
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
|
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
|
||||||
<IconButton
|
<Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
|
||||||
onClick={handleScrollToBottom}
|
<Text variant="b3" weight="medium">Jump to latest</Text>
|
||||||
src={ChevronBottomIC}
|
</Button>
|
||||||
tooltip="Scroll to Bottom"
|
|
||||||
/>
|
|
||||||
</div>
|
</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 {
|
&__STB {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@include dir.prop(right, var(--sp-normal), unset);
|
@include dir.prop(left, 50%, unset);
|
||||||
@include dir.prop(left, unset, var(--sp-normal));
|
@include dir.prop(right, unset, 50%);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border-radius: var(--bo-radius);
|
|
||||||
box-shadow: var(--bs-surface-border);
|
box-shadow: var(--bs-surface-border);
|
||||||
background-color: var(--bg-surface-low);
|
|
||||||
transition: transform 200ms ease-in-out;
|
transition: transform 200ms ease-in-out;
|
||||||
transform: translateY(100%) scale(0);
|
transform: translate(-50%, 100%);
|
||||||
|
|
||||||
&--open {
|
&--open {
|
||||||
transform: translateY(-28px) scale(1);
|
transform: translate(-50%, -28px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__unread {
|
&__unread {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--sp-extra-tight);
|
top: var(--sp-extra-tight);
|
||||||
@include dir.prop(right, var(--sp-extra-tight), unset);
|
@include dir.prop(left, var(--sp-normal), unset);
|
||||||
@include dir.prop(left, unset, var(--sp-extra-tight));
|
@include dir.prop(right, unset, var(--sp-normal));
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
background-color: var(--bg-surface);
|
width: calc(100% - var(--sp-extra-loose));
|
||||||
border-radius: var(--bo-radius);
|
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
|
||||||
box-shadow: var(--bs-primary-border);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&--open {
|
&--open {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
& button:first-child {
|
||||||
& .ic-btn {
|
|
||||||
padding: 6px var(--sp-extra-tight);
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
& .btn-primary {
|
|
||||||
@extend .cp-fx__item-one;
|
@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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import { blurOnBubbling } from '../../atoms/button/script';
|
|||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { toggleRoomSettings, openReusableContextMenu } from '../../../client/action/navigation';
|
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
|
||||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
@@ -25,6 +25,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
|
|||||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||||
|
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
||||||
|
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
|
||||||
@@ -73,6 +74,12 @@ function RoomViewHeader({ roomId }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
|
<IconButton
|
||||||
|
src={BackArrowIC}
|
||||||
|
className="room-header__back-btn"
|
||||||
|
tooltip="Return to navigation"
|
||||||
|
onClick={() => openNavigation()}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
ref={roomHeaderBtnRef}
|
ref={roomHeaderBtnRef}
|
||||||
className="room-header__btn"
|
className="room-header__btn"
|
||||||
@@ -87,7 +94,8 @@ function RoomViewHeader({ roomId }) {
|
|||||||
<RawIcon src={ChevronBottomIC} />
|
<RawIcon src={ChevronBottomIC} />
|
||||||
</button>
|
</button>
|
||||||
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />
|
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />
|
||||||
<IconButton onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
||||||
|
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={openRoomOptions}
|
onClick={openRoomOptions}
|
||||||
tooltip="Options"
|
tooltip="Options"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user