Compare commits
77 Commits
416fd02069
...
v2.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f05037c7d6 | ||
|
|
d0fd654bf7 | ||
|
|
7165bd91cd | ||
|
|
d3431a5d53 | ||
|
|
fa6c865000 | ||
|
|
fd680a93e0 | ||
|
|
38b604ad41 | ||
|
|
2ca67bb61a | ||
|
|
95b814b751 | ||
|
|
9963f3f988 | ||
|
|
fde7d4a25a | ||
|
|
895b2c4f19 | ||
|
|
427ea9baab | ||
|
|
df718e4498 | ||
|
|
00956f5bba | ||
|
|
e48d216d79 | ||
|
|
489f178c7c | ||
|
|
3bd4eda789 | ||
|
|
fc6c7b8dc6 | ||
|
|
deef1f2c8a | ||
|
|
38bd38a487 | ||
|
|
40de64078a | ||
|
|
780bd5e65a | ||
|
|
2cd74b4ea9 | ||
|
|
0cd3df391e | ||
|
|
854d2b4c27 | ||
|
|
7227fc7c43 | ||
|
|
73dcb44121 | ||
|
|
54fd394ef5 | ||
|
|
fda71166df | ||
|
|
69b6055353 | ||
|
|
1bdd0449e0 | ||
|
|
a6fdf9010b | ||
|
|
941dae0625 | ||
|
|
4a715bfd17 | ||
|
|
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 |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
|||||||
|
github: ajbura
|
||||||
|
liberapay: ajbura
|
||||||
open_collective: cinny
|
open_collective: cinny
|
||||||
liberapay: ajbura
|
|
||||||
6
.github/workflows/build-pull-request.yml
vendored
6
.github/workflows/build-pull-request.yml
vendored
@@ -15,19 +15,19 @@ jobs:
|
|||||||
- name: Build app
|
- name: Build app
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3.0.0
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: previewbuild
|
name: previewbuild
|
||||||
path: dist
|
path: dist
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
- name: Get PR info
|
- name: Get PR info
|
||||||
uses: actions/github-script@v6.0.0
|
uses: actions/github-script@v6.1.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
||||||
- name: Upload PR Info
|
- name: Upload PR Info
|
||||||
uses: actions/upload-artifact@v3.0.0
|
uses: actions/upload-artifact@v3.1.0
|
||||||
with:
|
with:
|
||||||
name: pr.json
|
name: pr.json
|
||||||
path: pr.json
|
path: pr.json
|
||||||
|
|||||||
11
.github/workflows/deploy-pull-request.yml
vendored
11
.github/workflows/deploy-pull-request.yml
vendored
@@ -6,6 +6,9 @@ on:
|
|||||||
- completed
|
- completed
|
||||||
jobs:
|
jobs:
|
||||||
get-build-and-deploy:
|
get-build-and-deploy:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: >
|
||||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
@@ -14,7 +17,7 @@ jobs:
|
|||||||
# 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.1.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
@@ -48,7 +51,7 @@ jobs:
|
|||||||
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.1.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
@@ -56,7 +59,7 @@ jobs:
|
|||||||
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"
|
||||||
@@ -68,7 +71,7 @@ jobs:
|
|||||||
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: Beakyn/gha-comment-pull-request@v1.0.2
|
uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.0.2
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v2.10.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
|
|||||||
5
.github/workflows/netlify-dev.yml
vendored
5
.github/workflows/netlify-dev.yml
vendored
@@ -9,12 +9,13 @@ jobs:
|
|||||||
deploy-to-netlify:
|
deploy-to-netlify:
|
||||||
name: 'Deploy'
|
name: 'Deploy'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.0.2
|
||||||
- name: Build and deploy to Netlify
|
- name: Build and deploy to Netlify
|
||||||
uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
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 }}
|
||||||
|
|||||||
65
.github/workflows/prod-deploy.yml
vendored
65
.github/workflows/prod-deploy.yml
vendored
@@ -5,14 +5,48 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-to-netlify:
|
create-release-tar:
|
||||||
name: 'Deploy to Netlify'
|
name: 'Create release tar'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3.0.2
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
- 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
|
||||||
|
run: |
|
||||||
|
echo '${{ secrets.GNUPG_KEY }}' | gpg --batch --import
|
||||||
|
# Sadly a few lines in the private key match a few lines in the public key,
|
||||||
|
# As a result just --export --armor gives us a few lines replaced with ***
|
||||||
|
# making it useless for importing the signing key. Instead, we dump it as
|
||||||
|
# non-armored and hex-encode it so that its printable.
|
||||||
|
echo "PGP Signing key, in raw PGP format in hex. Import with cat ... | xxd -r -p - | gpg --import"
|
||||||
|
gpg --export | xxd -p
|
||||||
|
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign 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
|
||||||
|
|
||||||
|
deploy-to-netlify:
|
||||||
|
name: 'Deploy to Netlify'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.0.2
|
||||||
- name: Build and deploy to Netlify
|
- name: Build and deploy to Netlify
|
||||||
uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
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 }}
|
||||||
@@ -20,37 +54,34 @@ jobs:
|
|||||||
BUILD_DIRECTORY: "dist"
|
BUILD_DIRECTORY: "dist"
|
||||||
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
|
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
|
||||||
NETLIFY_DEPLOY_TO_PROD: true
|
NETLIFY_DEPLOY_TO_PROD: true
|
||||||
- name: Get version from tag
|
|
||||||
id: vars
|
|
||||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
|
||||||
- name: Create tar.gz
|
|
||||||
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
|
||||||
- name: Upload tagged release
|
|
||||||
uses: softprops/action-gh-release@v0.1.14
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
|
||||||
|
|
||||||
push_to_dockerhub:
|
push-to-dockerhub:
|
||||||
name: Push Docker image to Docker Hub
|
name: Push Docker image to Docker Hub
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.0.2
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1.2.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1.6.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1.14.1
|
uses: docker/login-action@v2.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3.7.0
|
uses: docker/metadata-action@v4.0.1
|
||||||
with:
|
with:
|
||||||
images: ajbura/cinny
|
images: ajbura/cinny
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v2.10.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Ajay Bura (ajbura) and 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
|
||||||
|
|||||||
83
README.md
83
README.md
@@ -1,18 +1,27 @@
|
|||||||
# Cinny
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/ajbura/cinny/dev/public/res/svg/cinny.svg?sanitize=true"
|
||||||
|
height="16">
|
||||||
|
<span><b>Cinny</b></span>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/ajbura/cinny/releases">
|
||||||
|
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
|
||||||
|
<a href="https://hub.docker.com/r/ajbura/cinny">
|
||||||
|
<img alt="DockerHub downloads" src="https://img.shields.io/docker/pulls/ajbura/cinny?logo=docker&style=social"></a>
|
||||||
|
<a href="https://fosstodon.org/@cinnyapp">
|
||||||
|
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
|
||||||
|
<a href="https://twitter.com/intent/follow?screen_name=cinnyapp">
|
||||||
|
<img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/cinnyapp?logo=twitter&style=social"></a>
|
||||||
|
<a href="https://cinny.in/#sponsor">
|
||||||
|
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Table of Contents
|
**Cinny** is a Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have a client that is easy on end user
|
||||||
|
and feels a modern chat application.
|
||||||
|
|
||||||
- [About](#about)
|
|
||||||
- [Getting Started](https://cinny.in)
|
|
||||||
- [Contributing](./CONTRIBUTING.md)
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
||||||
|
|
||||||
## About <a name = "about"></a>
|
|
||||||
|
|
||||||
Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, elegant and secure interface.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Building and Running
|
## Building and Running
|
||||||
|
|
||||||
### Running pre-compiled
|
### Running pre-compiled
|
||||||
@@ -20,7 +29,57 @@ Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, ele
|
|||||||
A tarball of pre-compiled version of the app is provided with each [release](https://github.com/ajbura/cinny/releases).
|
A tarball of pre-compiled version of the app is provided with each [release](https://github.com/ajbura/cinny/releases).
|
||||||
You can serve the application with a webserver of your choosing by simply copying `dist/` directory to the webroot.
|
You can serve the application with a webserver of your choosing by simply copying `dist/` directory to the webroot.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>PGP Public Key to verify pre-compiled tarball</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQGNBGJw/g0BDAC8qQeLqDMzYzfPyOmRlHVEoguVTo+eo1aVdQH2X7OELdjjBlyj
|
||||||
|
6d6c1adv/uF2g83NNMoQY7GEeHjRnXE4m8kYSaarb840pxrYUagDc0dAbJOGaCBY
|
||||||
|
FKTo7U1Kvg0vdiaRuus0pvc1NVdXSxRNQbFXBSwduD+zn66TI3HfcEHNN62FG1cE
|
||||||
|
K1jWDwLAU0P3kKmj8+CAc3h9ZklPu0k/+t5bf/LJkvdBJAUzGZpehbPL5f3u3BZ0
|
||||||
|
leZLIrR8uV7PiV5jKFahxlKR5KQHld8qQm+qVhYbUzpuMBGmh419I6UvTzxuRcvU
|
||||||
|
Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
|
||||||
|
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
|
||||||
|
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
|
||||||
|
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
|
||||||
|
AdQEEwEIAD4WIQSRri2MHidaaZv+vvuUMwx6UK/M8wUCYnD+DQIbAwUJA8JnAAUL
|
||||||
|
CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCUMwx6UK/M88ApC/9HAdbum1lYBC0s
|
||||||
|
1k7GwP2A7B4sQtBWjy771BzybWlHeaeG+BGJwg4YiuowXZMm5dubFJFoI/CfeY07
|
||||||
|
B5aK40/bmT6Xcfkp0VA74c1wUpubBUEJN7tH5HG/OGd9BKeq9E/HHtVaJLVT1k3w
|
||||||
|
Rhv9VuHO6nR30EEp7IDthftotl5S4lio3+W0pKk4TAKV8vjaCNp3y/lAHzoP1BU9
|
||||||
|
bUSao+7GXVeArKBjuqxN+t1uuiaxPH4L0oe2pMVjTig04zGJM5fTVoly859MEcC/
|
||||||
|
R7Taq9RWGfXFmgCXy8Dviz3eOD90vqpCzhX4+ypK0cp2X0UwhMH4dpKUzExmdbhl
|
||||||
|
eBO5GcHB4VxvloRBNf9/Lr7YOTgWejMUw+MlhZE2RE8unfW1LnM/cjL4dhXzO/XB
|
||||||
|
FUHHNq8d6d4e02rfWqw7mZo2/NVJgFRcvzw2rgx7w7CKtCNwF4lNjUetB2waZzDb
|
||||||
|
fAE0kwhK4Iuwvy12JOBzL0Yy9MxANtwUryr/LQz9AmdT4Rwnp0S5AY0EYnD+DQEM
|
||||||
|
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
|
||||||
|
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
|
||||||
|
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
|
||||||
|
QPqfGDpowBwRkkOsGz/XVcesJ1Pzg4bKivTS9kZjZSyT9RRSY8As0sVUN57AwYul
|
||||||
|
s1+eh00n/tVpi2Jj9pCm7S0csSXvXj8v2OTdK1jt4YjpzR0/rwh4+/xlOjDjZEqH
|
||||||
|
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
|
||||||
|
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
|
||||||
|
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
|
||||||
|
NwARAQABiQG8BBgBCAAmFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmJw/g0CGwwF
|
||||||
|
CQPCZwAACgkQlDMMelCvzPPT7Qv8CjXUEhphZFLwpBfaNOzRNfIXJST9aDit8zHW
|
||||||
|
IMmfSpORVfpU71IyIB3o/DtTUPwCeb8nvNJs7aj1QT1ZUSsqFa3yY2S16V/g8+WN
|
||||||
|
sHca6oDSc1J+A0eEpEL1HbG1b5OPBC0AeGvvMOoqrbqThBZVKg1Jc/0SD3cvKElv
|
||||||
|
aHeCZCNNmfcZ2Ib4HYhhc8//ZtC9TeI+5J/YesctY1M12EoWMxMrc27Y3P5Pa0BI
|
||||||
|
Uc3qxWggPq1vOFYsEshL0w99HyJvREJmQA7Fa0crV+rICxyrBxJeNnEvjH/0KCBU
|
||||||
|
LCkEonLY1QwrxyeeV3VpxGE3zHHE3azOdAjTIoAdzX5f/qhbgYlM68GL2f8xdDkp
|
||||||
|
O0igSGHWhO4F8BfmE7IOTx1Bi7daczp8nCFxh73cKpKB0RUsd9xxrqYpovjmEAlo
|
||||||
|
w7aHpdzt64NQcsrbK10OSVDF3gFa9Vz20/NQvdUrp8jGmAb/8+nYqI94Jsc28H36
|
||||||
|
UeGsouhyuITLwEhScounZDqop+Dx
|
||||||
|
=Zg+6
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### Building from source
|
### Building from source
|
||||||
|
> We recommend using a version manager as versions change very quickly. You will likely need to switch
|
||||||
|
between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Also recommended nodejs version is 16.15.0 LTS.
|
||||||
|
|
||||||
Execute the following commands to compile the app from its source code:
|
Execute the following commands to compile the app from its source code:
|
||||||
|
|
||||||
@@ -31,7 +90,7 @@ npm run build # Compiles the app into the dist/ directory
|
|||||||
|
|
||||||
You can then copy the files to a webserver's webroot of your choice.
|
You can then copy the files to a webserver's webroot of your choice.
|
||||||
|
|
||||||
To serve a development version of the app locally for testing, you may also use the command `npm start`.
|
To serve a development version of the app locally for testing, you need to use the command `npm start`.
|
||||||
|
|
||||||
### Running with Docker
|
### Running with Docker
|
||||||
|
|
||||||
@@ -59,7 +118,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
|
||||||
}
|
}
|
||||||
2994
package-lock.json
generated
2994
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "cinny",
|
"name": "cinny",
|
||||||
"version": "1.8.2",
|
"version": "2.0.4",
|
||||||
"description": "Yet another matrix client",
|
"description": "Yet another matrix client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=6.14.11",
|
"npm": ">=6.14.8 <=8.5.5",
|
||||||
"node": ">=14.6.0"
|
"node": ">=14.15.0 <=17.9.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack serve --config ./webpack.dev.js --open",
|
"start": "webpack serve --config ./webpack.dev.js --open",
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
"author": "Ajay Bura",
|
"author": "Ajay Bura",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^4.5.7",
|
"@fontsource/inter": "^4.5.10",
|
||||||
"@fontsource/roboto": "^4.5.5",
|
"@fontsource/roboto": "^4.5.7",
|
||||||
"@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",
|
||||||
@@ -27,9 +27,9 @@
|
|||||||
"flux": "^4.0.3",
|
"flux": "^4.0.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"html-react-parser": "^1.4.12",
|
"html-react-parser": "^1.4.12",
|
||||||
"katex": "^0.15.3",
|
"katex": "^0.15.6",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"matrix-js-sdk": "^17.0.0",
|
"matrix-js-sdk": "^17.2.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-extension-math": "^2.0.2",
|
||||||
@@ -43,45 +43,45 @@
|
|||||||
"react-dnd-html5-backend": "^15.1.3",
|
"react-dnd-html5-backend": "^15.1.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-google-recaptcha": "^2.1.0",
|
"react-google-recaptcha": "^2.1.0",
|
||||||
"react-modal": "^3.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": "^14.0.2"
|
"twemoji": "^14.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.9",
|
"@babel/core": "^7.18.0",
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@babel/preset-env": "^7.18.0",
|
||||||
"@babel/preset-react": "^7.16.7",
|
"@babel/preset-react": "^7.17.12",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"copy-webpack-plugin": "^10.2.4",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||||
"eslint": "^8.14.0",
|
"eslint": "^8.16.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.30.0",
|
||||||
"eslint-plugin-react-hooks": "^4.4.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.50.1",
|
"sass": "^1.52.1",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^13.0.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.72.0",
|
"webpack": "^5.72.1",
|
||||||
"webpack-cli": "^4.9.2",
|
"webpack-cli": "^4.9.2",
|
||||||
"webpack-dev-server": "^4.8.1",
|
"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>
|
||||||
@@ -123,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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const items = [{
|
|||||||
type: cons.notifs.DEFAULT,
|
type: cons.notifs.DEFAULT,
|
||||||
}, {
|
}, {
|
||||||
iconSrc: BellRingIC,
|
iconSrc: BellRingIC,
|
||||||
text: 'All message',
|
text: 'All messages',
|
||||||
type: cons.notifs.ALL_MESSAGES,
|
type: cons.notifs.ALL_MESSAGES,
|
||||||
}, {
|
}, {
|
||||||
iconSrc: BellPingIC,
|
iconSrc: BellPingIC,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { emojis } from './emoji';
|
|||||||
const eventType = 'io.element.recent_emoji';
|
const eventType = 'io.element.recent_emoji';
|
||||||
|
|
||||||
function getRecentEmojisRaw() {
|
function getRecentEmojisRaw() {
|
||||||
return initMatrix.matrixClient.getAccountData(eventType).getContent().recent_emoji ?? [];
|
return initMatrix.matrixClient.getAccountData(eventType)?.getContent().recent_emoji ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRecentEmojis(limit) {
|
export function getRecentEmojis(limit) {
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,9 +56,10 @@ function InviteList({ isOpen, onRequestClose }) {
|
|||||||
function renderRoomTile(roomId) {
|
function renderRoomTile(roomId) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const myRoom = mx.getRoom(roomId);
|
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?.() ?? '';
|
const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? '';
|
||||||
return (
|
return (
|
||||||
<RoomTile
|
<RoomTile
|
||||||
@@ -97,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,4 +1,5 @@
|
|||||||
import React, { useState, 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';
|
||||||
@@ -9,12 +10,12 @@ import { roomIdByActivity } from '../../../util/sort';
|
|||||||
import RoomsCategory from './RoomsCategory';
|
import RoomsCategory from './RoomsCategory';
|
||||||
|
|
||||||
const drawerPostie = new Postie();
|
const drawerPostie = new Postie();
|
||||||
function Directs() {
|
function Directs({ size }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const { roomList, notifications } = initMatrix;
|
const { roomList, notifications } = initMatrix;
|
||||||
const [directIds, setDirectIds] = useState([]);
|
const [directIds, setDirectIds] = useState([]);
|
||||||
|
|
||||||
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), []);
|
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), [size]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
|
const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
|
||||||
@@ -63,5 +64,8 @@ function Directs() {
|
|||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -195,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.matrixClient.getRoom(room.room_id) !== null;
|
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,8 @@ 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';
|
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
|
||||||
|
|
||||||
@@ -18,8 +20,10 @@ function Dialogs() {
|
|||||||
<ProfileViewer />
|
<ProfileViewer />
|
||||||
<ShortcutSpaces />
|
<ShortcutSpaces />
|
||||||
<CreateRoom />
|
<CreateRoom />
|
||||||
|
<JoinAlias />
|
||||||
<SpaceAddExisting />
|
<SpaceAddExisting />
|
||||||
<Search />
|
<Search />
|
||||||
|
<EmojiVerification />
|
||||||
|
|
||||||
<ReusableDialog />
|
<ReusableDialog />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
|
|||||||
import './RoomViewContent.scss';
|
import './RoomViewContent.scss';
|
||||||
|
|
||||||
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';
|
||||||
@@ -50,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 the ${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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -199,7 +233,7 @@ function usePaginate(
|
|||||||
};
|
};
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
@@ -470,12 +504,14 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import dateFormat from 'dateformat';
|
|||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { isCrossVerified } from '../../../util/matrixUtil';
|
import { isCrossVerified } from '../../../util/matrixUtil';
|
||||||
import { openReusableDialog } from '../../../client/action/navigation';
|
import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
@@ -25,6 +25,7 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
|||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { useDeviceList } from '../../hooks/useDeviceList';
|
import { useDeviceList } from '../../hooks/useDeviceList';
|
||||||
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
|
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
|
||||||
|
import { accessSecretStorage } from './SecretStorageAccess';
|
||||||
|
|
||||||
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
|
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
|
||||||
let isCompleted = false;
|
let isCompleted = false;
|
||||||
@@ -69,6 +70,7 @@ function DeviceManage() {
|
|||||||
const [truncated, setTruncated] = useState(true);
|
const [truncated, setTruncated] = useState(true);
|
||||||
const mountStore = useStore();
|
const mountStore = useStore();
|
||||||
mountStore.setItem(true);
|
mountStore.setItem(true);
|
||||||
|
const isMeVerified = isCrossVerified(mx.deviceId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProcessing([]);
|
setProcessing([]);
|
||||||
@@ -127,18 +129,42 @@ function DeviceManage() {
|
|||||||
removeFromProcessing(device);
|
removeFromProcessing(device);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const verifyWithKey = async (device) => {
|
||||||
|
const keyData = await accessSecretStorage('Session verification');
|
||||||
|
if (!keyData) return;
|
||||||
|
addToProcessing(device);
|
||||||
|
await mx.checkOwnCrossSigningTrust();
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyWithEmojis = async (deviceId) => {
|
||||||
|
const req = await mx.requestVerification(mx.getUserId(), [deviceId]);
|
||||||
|
openEmojiVerification(req, { userId: mx.getUserId(), deviceId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const verify = (deviceId, isCurrentDevice) => {
|
||||||
|
if (isCurrentDevice) {
|
||||||
|
verifyWithKey(deviceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verifyWithEmojis(deviceId);
|
||||||
|
};
|
||||||
|
|
||||||
const renderDevice = (device, isVerified) => {
|
const renderDevice = (device, isVerified) => {
|
||||||
const deviceId = device.device_id;
|
const deviceId = device.device_id;
|
||||||
const displayName = device.display_name;
|
const displayName = device.display_name;
|
||||||
const lastIP = device.last_seen_ip;
|
const lastIP = device.last_seen_ip;
|
||||||
const lastTS = device.last_seen_ts;
|
const lastTS = device.last_seen_ts;
|
||||||
|
const isCurrentDevice = mx.deviceId === deviceId;
|
||||||
|
const canVerify = isVerified === false && (isMeVerified || isCurrentDevice);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
key={deviceId}
|
key={deviceId}
|
||||||
title={(
|
title={(
|
||||||
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
|
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
|
||||||
{displayName}
|
{displayName}
|
||||||
<Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
|
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
|
||||||
|
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
options={
|
options={
|
||||||
@@ -146,19 +172,27 @@ function DeviceManage() {
|
|||||||
? <Spinner size="small" />
|
? <Spinner size="small" />
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
|
{(isCSEnabled && canVerify) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>}
|
||||||
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
|
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
|
||||||
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
|
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
content={(
|
content={(
|
||||||
<Text variant="b3">
|
<>
|
||||||
Last activity
|
<Text variant="b3">
|
||||||
<span style={{ color: 'var(--tc-surface-normal)' }}>
|
Last activity
|
||||||
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
|
<span style={{ color: 'var(--tc-surface-normal)' }}>
|
||||||
</span>
|
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
|
||||||
{lastIP ? ` at ${lastIP}` : ''}
|
</span>
|
||||||
</Text>
|
{lastIP ? ` at ${lastIP}` : ''}
|
||||||
|
</Text>
|
||||||
|
{isCurrentDevice && (
|
||||||
|
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
|
||||||
|
{`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -200,7 +234,7 @@ function DeviceManage() {
|
|||||||
{noEncryption.length > 0 && (
|
{noEncryption.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<MenuHeader>Sessions without encryption support</MenuHeader>
|
<MenuHeader>Sessions without encryption support</MenuHeader>
|
||||||
{noEncryption.map((device) => renderDevice(device, true))}
|
{noEncryption.map((device) => renderDevice(device, null))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
@@ -211,7 +245,7 @@ function DeviceManage() {
|
|||||||
if (truncated && index >= TRUNCATED_COUNT) return null;
|
if (truncated && index >= TRUNCATED_COUNT) return null;
|
||||||
return renderDevice(device, true);
|
return renderDevice(device, true);
|
||||||
})
|
})
|
||||||
: <Text className="device-manage__info">No verified session</Text>
|
: <Text className="device-manage__info">No verified sessions</Text>
|
||||||
}
|
}
|
||||||
{ verified.length > TRUNCATED_COUNT && (
|
{ verified.length > TRUNCATED_COUNT && (
|
||||||
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
|
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
|
||||||
|
|||||||
@@ -15,6 +15,23 @@
|
|||||||
& .setting-tile:last-of-type {
|
& .setting-tile:last-of-type {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
& .setting-tile__options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-ultra-tight);
|
||||||
|
& .btn-positive {
|
||||||
|
padding: 6px var(--sp-tight);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__current-label {
|
||||||
|
margin: 0 var(--sp-extra-tight);
|
||||||
|
padding: 2px var(--sp-ultra-tight);
|
||||||
|
color: var(--bg-surface);
|
||||||
|
background-color: var(--tc-surface-low);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&__rename {
|
&__rename {
|
||||||
padding: var(--sp-normal);
|
padding: var(--sp-normal);
|
||||||
|
|||||||
@@ -159,9 +159,9 @@ function DeleteKeyBackupDialog({ requestClose }) {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const mountStore = useStore();
|
const mountStore = useStore();
|
||||||
mountStore.setItem(true);
|
|
||||||
|
|
||||||
const deleteBackup = async () => {
|
const deleteBackup = async () => {
|
||||||
|
mountStore.setItem(true);
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
const backupInfo = await mx.getKeyBackupVersion();
|
const backupInfo = await mx.getKeyBackupVersion();
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ function SecretStorageAccess({ onComplete }) {
|
|||||||
const [process, setProcess] = useState(false);
|
const [process, setProcess] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const mountStore = useStore();
|
const mountStore = useStore();
|
||||||
mountStore.setItem(true);
|
|
||||||
|
|
||||||
const toggleWithPhrase = () => setWithPhrase(!withPhrase);
|
const toggleWithPhrase = () => setWithPhrase(!withPhrase);
|
||||||
|
|
||||||
const processInput = async ({ key, phrase }) => {
|
const processInput = async ({ key, phrase }) => {
|
||||||
|
mountStore.setItem(true);
|
||||||
setProcess(true);
|
setProcess(true);
|
||||||
try {
|
try {
|
||||||
const { salt, iterations } = sSKeyInfo.passphrase;
|
const { salt, iterations } = sSKeyInfo.passphrase || {};
|
||||||
const privateKey = key
|
const privateKey = key
|
||||||
? mx.keyBackupKeyFromRecoveryKey(key)
|
? mx.keyBackupKeyFromRecoveryKey(key)
|
||||||
: await deriveKey(phrase, salt, iterations);
|
: await deriveKey(phrase, salt, iterations);
|
||||||
|
|||||||
@@ -93,12 +93,13 @@ function Homeserver({ onChange }) {
|
|||||||
const result = await (await fetch(configFileUrl, { method: 'GET' })).json();
|
const result = await (await fetch(configFileUrl, { method: 'GET' })).json();
|
||||||
const selectedHs = result?.defaultHomeserver;
|
const selectedHs = result?.defaultHomeserver;
|
||||||
const hsList = result?.homeserverList;
|
const hsList = result?.homeserverList;
|
||||||
|
const allowCustom = result?.allowCustomHomeservers ?? true;
|
||||||
if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) {
|
if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
setHs({ selected: hsList[selectedHs], list: hsList });
|
setHs({ selected: hsList[selectedHs], list: hsList, allowCustom: allowCustom });
|
||||||
} catch {
|
} catch {
|
||||||
setHs({ selected: 'matrix.org', list: ['matrix.org'] });
|
setHs({ selected: 'matrix.org', list: ['matrix.org'], allowCustom: true });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -106,14 +107,15 @@ function Homeserver({ onChange }) {
|
|||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
setProcess({ isLoading: false });
|
setProcess({ isLoading: false });
|
||||||
debounce._(async () => {
|
debounce._(async () => {
|
||||||
setHs({ selected: value.trim(), list: hs.list });
|
setHs({ ...hs, selected: value.trim() });
|
||||||
}, 700)();
|
}, 700)();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="homeserver-form">
|
<div className="homeserver-form">
|
||||||
<Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver" />
|
<Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver"
|
||||||
|
disabled={hs === null || !hs.allowCustom} />
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
placement="right"
|
placement="right"
|
||||||
content={(hideMenu) => (
|
content={(hideMenu) => (
|
||||||
@@ -126,7 +128,7 @@ function Homeserver({ onChange }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
hideMenu();
|
hideMenu();
|
||||||
hsRef.current.value = hsName;
|
hsRef.current.value = hsName;
|
||||||
setHs({ selected: hsName, list: hs.list });
|
setHs({ ...hs, selected: hsName });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hsName}
|
{hsName}
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ export function openCreateRoom(isSpace = false, parentId = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openJoinAlias(term) {
|
||||||
|
appDispatcher.dispatch({
|
||||||
|
type: cons.actions.navigation.OPEN_JOIN_ALIAS,
|
||||||
|
term,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function openInviteUser(roomId, searchTerm) {
|
export function openInviteUser(roomId, searchTerm) {
|
||||||
appDispatcher.dispatch({
|
appDispatcher.dispatch({
|
||||||
type: cons.actions.navigation.OPEN_INVITE_USER,
|
type: cons.actions.navigation.OPEN_INVITE_USER,
|
||||||
@@ -166,3 +173,11 @@ export function openReusableDialog(title, render, afterClose) {
|
|||||||
afterClose,
|
afterClose,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openEmojiVerification(request, targetDevice) {
|
||||||
|
appDispatcher.dispatch({
|
||||||
|
type: cons.actions.navigation.OPEN_EMOJI_VERIFICATION,
|
||||||
|
request,
|
||||||
|
targetDevice,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,17 +113,19 @@ async function join(roomIdOrAlias, isDM, via) {
|
|||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
* @param {boolean} isDM
|
* @param {boolean} isDM
|
||||||
*/
|
*/
|
||||||
function leave(roomId) {
|
async function leave(roomId) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||||
mx.leave(roomId)
|
try {
|
||||||
.then(() => {
|
await mx.leave(roomId);
|
||||||
appDispatcher.dispatch({
|
appDispatcher.dispatch({
|
||||||
type: cons.actions.room.LEAVE,
|
type: cons.actions.room.LEAVE,
|
||||||
roomId,
|
roomId,
|
||||||
isDM,
|
isDM,
|
||||||
});
|
});
|
||||||
}).catch();
|
} catch {
|
||||||
|
console.error('Unable to leave room.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(options, isDM = false) {
|
async function create(options, isDM = false) {
|
||||||
|
|||||||
@@ -2,25 +2,60 @@ import { openSearch, toggleRoomSettings } from '../action/navigation';
|
|||||||
import navigation from '../state/navigation';
|
import navigation from '../state/navigation';
|
||||||
import { markAsRead } from '../action/notifications';
|
import { markAsRead } from '../action/notifications';
|
||||||
|
|
||||||
|
function shouldFocusMessageField(code) {
|
||||||
|
// do not focus on F keys
|
||||||
|
if (/^F\d+$/.test(code)) return false;
|
||||||
|
|
||||||
|
// do not focus on numlock/scroll lock
|
||||||
|
if (
|
||||||
|
code.metaKey
|
||||||
|
|| code.startsWith('OS')
|
||||||
|
|| code.startsWith('Meta')
|
||||||
|
|| code.startsWith('Shift')
|
||||||
|
|| code.startsWith('Alt')
|
||||||
|
|| code.startsWith('Control')
|
||||||
|
|| code.startsWith('Arrow')
|
||||||
|
|| code === 'Tab'
|
||||||
|
|| code === 'Space'
|
||||||
|
|| code === 'Enter'
|
||||||
|
|| code === 'NumLock'
|
||||||
|
|| code === 'ScrollLock'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function listenKeyboard(event) {
|
function listenKeyboard(event) {
|
||||||
// Ctrl/Cmd +
|
// Ctrl/Cmd +
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
// k - for search Modal
|
// open search modal
|
||||||
if (event.keyCode === 75) {
|
if (event.code === 'KeyK') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (navigation.isRawModalVisible) return;
|
if (navigation.isRawModalVisible) return;
|
||||||
openSearch();
|
openSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// focus message field on paste
|
||||||
|
if (event.code === 'KeyV') {
|
||||||
|
if (navigation.isRawModalVisible) return;
|
||||||
|
const msgTextarea = document.getElementById('message-textarea');
|
||||||
|
const { activeElement } = document;
|
||||||
|
if (activeElement !== msgTextarea
|
||||||
|
&& ['input', 'textarea'].includes(activeElement.tagName.toLowerCase())
|
||||||
|
) return;
|
||||||
|
msgTextarea?.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event.ctrlKey && !event.altKey) {
|
if (!event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||||
if (navigation.isRawModalVisible) return;
|
if (navigation.isRawModalVisible) return;
|
||||||
if (['text', 'textarea'].includes(document.activeElement.type)) {
|
if (['input', 'textarea'].includes(document.activeElement.tagName.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// esc
|
if (event.code === 'Escape') {
|
||||||
if (event.keyCode === 27) {
|
|
||||||
if (navigation.isRoomSettings) {
|
if (navigation.isRoomSettings) {
|
||||||
toggleRoomSettings();
|
toggleRoomSettings();
|
||||||
return;
|
return;
|
||||||
@@ -31,16 +66,12 @@ function listenKeyboard(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow these keys to type/focus message field
|
// focus the text field on most keypresses
|
||||||
if ((event.keyCode !== 8 && event.keyCode < 48)
|
if (shouldFocusMessageField(event.code)) {
|
||||||
|| (event.keyCode >= 91 && event.keyCode <= 93)
|
// press any key to focus and type in message field
|
||||||
|| (event.keyCode >= 112 && event.keyCode <= 183)) {
|
const msgTextarea = document.getElementById('message-textarea');
|
||||||
return;
|
msgTextarea?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// press any key to focus and type in message field
|
|
||||||
const msgTextarea = document.getElementById('message-textarea');
|
|
||||||
msgTextarea?.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class InitMatrix extends EventEmitter {
|
|||||||
deviceId: secret.deviceId,
|
deviceId: secret.deviceId,
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
cryptoCallbacks,
|
cryptoCallbacks,
|
||||||
|
verificationMethods: [
|
||||||
|
'm.sas.v1',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.matrixClient.initCrypto();
|
await this.matrixClient.initCrypto();
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import cons from './cons';
|
|||||||
import navigation from './navigation';
|
import navigation from './navigation';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
|
|
||||||
import NotificationSound from '../../../public/sound/notification.ogg';
|
|
||||||
import InviteSound from '../../../public/sound/invite.ogg';
|
|
||||||
|
|
||||||
function isNotifEvent(mEvent) {
|
function isNotifEvent(mEvent) {
|
||||||
const eType = mEvent.getType();
|
const eType = mEvent.getType();
|
||||||
if (!cons.supportEventTypes.includes(eType)) return false;
|
if (!cons.supportEventTypes.includes(eType)) return false;
|
||||||
@@ -238,14 +235,14 @@ class Notifications extends EventEmitter {
|
|||||||
|
|
||||||
_playNotiSound() {
|
_playNotiSound() {
|
||||||
if (!this._notiAudio) {
|
if (!this._notiAudio) {
|
||||||
this._notiAudio = new Audio(NotificationSound);
|
this._notiAudio = document.getElementById('notificationSound');
|
||||||
}
|
}
|
||||||
this._notiAudio.play();
|
this._notiAudio.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
_playInviteSound() {
|
_playInviteSound() {
|
||||||
if (!this._inviteAudio) {
|
if (!this._inviteAudio) {
|
||||||
this._inviteAudio = new Audio(InviteSound);
|
this._inviteAudio = document.getElementById('inviteSound');
|
||||||
}
|
}
|
||||||
this._inviteAudio.play();
|
this._inviteAudio.play();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ function isMEventSpaceChild(mEvent) {
|
|||||||
return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0;
|
return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {() => boolean} callback if return true wait will over else callback will be called again.
|
||||||
|
* @param {number} timeout timeout to callback
|
||||||
|
* @param {number} maxTry maximum callback try > 0. -1 means no limit
|
||||||
|
*/
|
||||||
|
async function waitFor(callback, timeout = 400, maxTry = -1) {
|
||||||
|
if (maxTry === 0) return false;
|
||||||
|
const isOver = async () => new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(callback()), timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await isOver()) return true;
|
||||||
|
return waitFor(callback, timeout, maxTry - 1);
|
||||||
|
}
|
||||||
|
|
||||||
class RoomList extends EventEmitter {
|
class RoomList extends EventEmitter {
|
||||||
constructor(matrixClient) {
|
constructor(matrixClient) {
|
||||||
super();
|
super();
|
||||||
@@ -228,6 +243,7 @@ class RoomList extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isDMInvite(room) {
|
_isDMInvite(room) {
|
||||||
|
if (this.mDirects.has(room.roomId)) return true;
|
||||||
const me = room.getMember(this.matrixClient.getUserId());
|
const me = room.getMember(this.matrixClient.getUserId());
|
||||||
const myEventContent = me.events.member.getContent();
|
const myEventContent = me.events.member.getContent();
|
||||||
return myEventContent.membership === 'invite' && myEventContent.is_direct;
|
return myEventContent.membership === 'invite' && myEventContent.is_direct;
|
||||||
@@ -243,22 +259,11 @@ class RoomList extends EventEmitter {
|
|||||||
latestMDirects.forEach((directId) => {
|
latestMDirects.forEach((directId) => {
|
||||||
const myRoom = this.matrixClient.getRoom(directId);
|
const myRoom = this.matrixClient.getRoom(directId);
|
||||||
if (this.mDirects.has(directId)) return;
|
if (this.mDirects.has(directId)) return;
|
||||||
|
|
||||||
// Update mDirects
|
|
||||||
this.mDirects.add(directId);
|
this.mDirects.add(directId);
|
||||||
|
|
||||||
if (myRoom === null) return;
|
if (myRoom === null) return;
|
||||||
|
if (myRoom.getMyMembership() === 'join') {
|
||||||
if (this._isDMInvite(myRoom)) return;
|
|
||||||
|
|
||||||
if (myRoom.getMyMembership === 'join' && !this.directs.has(directId)) {
|
|
||||||
this.directs.add(directId);
|
this.directs.add(directId);
|
||||||
}
|
|
||||||
|
|
||||||
// Newly added room.
|
|
||||||
// at this time my membership can be invite | join
|
|
||||||
if (myRoom.getMyMembership() === 'join' && this.rooms.has(directId)) {
|
|
||||||
// found a DM which accidentally gets added to this.rooms
|
|
||||||
this.rooms.delete(directId);
|
this.rooms.delete(directId);
|
||||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||||
}
|
}
|
||||||
@@ -298,23 +303,17 @@ class RoomList extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.matrixClient.on('Room.myMembership', (room, membership, prevMembership) => {
|
this.matrixClient.on('Room.myMembership', async (room, membership, prevMembership) => {
|
||||||
// room => prevMembership = null | invite | join | leave | kick | ban | unban
|
// room => prevMembership = null | invite | join | leave | kick | ban | unban
|
||||||
// room => membership = invite | join | leave | kick | ban | unban
|
// room => membership = invite | join | leave | kick | ban | unban
|
||||||
const { roomId } = room;
|
const { roomId } = room;
|
||||||
|
const isRoomReady = () => this.matrixClient.getRoom(roomId) !== null;
|
||||||
|
if (['join', 'invite'].includes(membership) && isRoomReady() === false) {
|
||||||
|
if (await waitFor(isRoomReady, 200, 100) === false) return;
|
||||||
|
}
|
||||||
|
|
||||||
if (membership === 'unban') return;
|
if (membership === 'unban') return;
|
||||||
|
|
||||||
// When user_reject/sender_undo room invite
|
|
||||||
if (prevMembership === 'invite') {
|
|
||||||
if (this.inviteDirects.has(roomId)) this.inviteDirects.delete(roomId);
|
|
||||||
else if (this.inviteSpaces.has(roomId)) this.inviteSpaces.delete(roomId);
|
|
||||||
else this.inviteRooms.delete(roomId);
|
|
||||||
|
|
||||||
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When user get invited
|
|
||||||
if (membership === 'invite') {
|
if (membership === 'invite') {
|
||||||
if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
|
if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
|
||||||
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
|
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
|
||||||
@@ -324,88 +323,53 @@ class RoomList extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When user join room (first time) or start DM.
|
if (prevMembership === 'invite') {
|
||||||
if ((prevMembership === null || prevMembership === 'invite') && membership === 'join') {
|
if (this.inviteDirects.has(roomId)) this.inviteDirects.delete(roomId);
|
||||||
// when user create room/DM OR accept room/dm invite from this client.
|
else if (this.inviteSpaces.has(roomId)) this.inviteSpaces.delete(roomId);
|
||||||
// we will update this.rooms/this.directs with user action
|
else this.inviteRooms.delete(roomId);
|
||||||
if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return;
|
|
||||||
|
|
||||||
if (this.processingRooms.has(roomId)) {
|
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
|
||||||
const procRoomInfo = this.processingRooms.get(roomId);
|
|
||||||
|
|
||||||
if (procRoomInfo.isDM) this.directs.add(roomId);
|
|
||||||
else if (room.isSpaceRoom()) this.addToSpaces(roomId);
|
|
||||||
else this.rooms.add(roomId);
|
|
||||||
|
|
||||||
if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId);
|
|
||||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
|
||||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
|
||||||
|
|
||||||
this.processingRooms.delete(roomId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (room.isSpaceRoom()) {
|
|
||||||
this.addToSpaces(roomId);
|
|
||||||
|
|
||||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
|
||||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// below code intented to work when user create room/DM
|
|
||||||
// OR accept room/dm invite from other client.
|
|
||||||
// and we have to update our client. (it's ok to have 10sec delay)
|
|
||||||
|
|
||||||
// create a buffer of 10sec and HOPE client.accoundData get updated
|
|
||||||
// then accoundData event listener will update this.mDirects.
|
|
||||||
// and we will be able to know if it's a DM.
|
|
||||||
// ----------
|
|
||||||
// less likely situation:
|
|
||||||
// if we don't get accountData with 10sec then:
|
|
||||||
// we will temporary add it to this.rooms.
|
|
||||||
// and in future when accountData get updated
|
|
||||||
// accountData listener will automatically goona REMOVE it from this.rooms
|
|
||||||
// and will ADD it to this.directs
|
|
||||||
// and emit the cons.events.roomList.ROOMLIST_UPDATED to update the UI.
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return;
|
|
||||||
if (this.mDirects.has(roomId)) this.directs.add(roomId);
|
|
||||||
else this.rooms.add(roomId);
|
|
||||||
|
|
||||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
|
||||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
|
||||||
}, 10000);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// when room is a DM add/remove it from DM's and return.
|
if (['leave', 'kick', 'ban'].includes(membership)) {
|
||||||
if (this.directs.has(roomId)) {
|
if (this.directs.has(roomId)) this.directs.delete(roomId);
|
||||||
if (membership === 'leave' || membership === 'kick' || membership === 'ban') {
|
else if (this.spaces.has(roomId)) this.deleteFromSpaces(roomId);
|
||||||
this.directs.delete(roomId);
|
else this.rooms.delete(roomId);
|
||||||
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
|
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.mDirects.has(roomId)) {
|
|
||||||
if (membership === 'join') {
|
|
||||||
this.directs.add(roomId);
|
|
||||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
|
||||||
}
|
|
||||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// when room is not a DM add/remove it from rooms.
|
|
||||||
if (membership === 'leave' || membership === 'kick' || membership === 'ban') {
|
// when user create room/DM OR accept room/dm invite from this client.
|
||||||
if (room.isSpaceRoom()) this.deleteFromSpaces(roomId);
|
// we will update this.rooms/this.directs with user action
|
||||||
else this.rooms.delete(roomId);
|
if (membership === 'join' && this.processingRooms.has(roomId)) {
|
||||||
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
|
const procRoomInfo = this.processingRooms.get(roomId);
|
||||||
|
|
||||||
|
if (procRoomInfo.isDM) this.directs.add(roomId);
|
||||||
|
else if (room.isSpaceRoom()) this.addToSpaces(roomId);
|
||||||
|
else this.rooms.add(roomId);
|
||||||
|
|
||||||
|
if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId);
|
||||||
|
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||||
|
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||||
|
|
||||||
|
this.processingRooms.delete(roomId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mDirects.has(roomId) && membership === 'join') {
|
||||||
|
this.directs.add(roomId);
|
||||||
|
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||||
|
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (membership === 'join') {
|
if (membership === 'join') {
|
||||||
if (room.isSpaceRoom()) this.addToSpaces(roomId);
|
if (room.isSpaceRoom()) this.addToSpaces(roomId);
|
||||||
else this.rooms.add(roomId);
|
else this.rooms.add(roomId);
|
||||||
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
|
||||||
|
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||||
}
|
}
|
||||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const cons = {
|
const cons = {
|
||||||
version: '1.8.2',
|
version: '2.0.4',
|
||||||
secretKey: {
|
secretKey: {
|
||||||
ACCESS_TOKEN: 'cinny_access_token',
|
ACCESS_TOKEN: 'cinny_access_token',
|
||||||
DEVICE_ID: 'cinny_device_id',
|
DEVICE_ID: 'cinny_device_id',
|
||||||
@@ -38,6 +38,7 @@ const cons = {
|
|||||||
OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
|
OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
|
||||||
OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
|
OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
|
||||||
OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
|
OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
|
||||||
|
OPEN_JOIN_ALIAS: 'OPEN_JOIN_ALIAS',
|
||||||
OPEN_INVITE_USER: 'OPEN_INVITE_USER',
|
OPEN_INVITE_USER: 'OPEN_INVITE_USER',
|
||||||
OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER',
|
OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER',
|
||||||
OPEN_SETTINGS: 'OPEN_SETTINGS',
|
OPEN_SETTINGS: 'OPEN_SETTINGS',
|
||||||
@@ -49,6 +50,7 @@ const cons = {
|
|||||||
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
|
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
|
||||||
OPEN_NAVIGATION: 'OPEN_NAVIGATION',
|
OPEN_NAVIGATION: 'OPEN_NAVIGATION',
|
||||||
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
|
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
|
||||||
|
OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION',
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
JOIN: 'JOIN',
|
JOIN: 'JOIN',
|
||||||
@@ -85,6 +87,7 @@ const cons = {
|
|||||||
INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
|
INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
|
||||||
PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED',
|
PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED',
|
||||||
CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
|
CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
|
||||||
|
JOIN_ALIAS_OPENED: 'JOIN_ALIAS_OPENED',
|
||||||
INVITE_USER_OPENED: 'INVITE_USER_OPENED',
|
INVITE_USER_OPENED: 'INVITE_USER_OPENED',
|
||||||
SETTINGS_OPENED: 'SETTINGS_OPENED',
|
SETTINGS_OPENED: 'SETTINGS_OPENED',
|
||||||
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
|
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
|
||||||
@@ -96,6 +99,7 @@ const cons = {
|
|||||||
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
|
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
|
||||||
NAVIGATION_OPENED: 'NAVIGATION_OPENED',
|
NAVIGATION_OPENED: 'NAVIGATION_OPENED',
|
||||||
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
|
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
|
||||||
|
EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED',
|
||||||
},
|
},
|
||||||
roomList: {
|
roomList: {
|
||||||
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
|
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Navigation extends EventEmitter {
|
|||||||
this.isRoomSettings = false;
|
this.isRoomSettings = false;
|
||||||
this.recentRooms = [];
|
this.recentRooms = [];
|
||||||
|
|
||||||
this.isRawModalVisible = false;
|
this.rawModelStack = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_setSpacePath(roomId) {
|
_setSpacePath(roomId) {
|
||||||
@@ -47,8 +47,13 @@ class Navigation extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isRawModalVisible() {
|
||||||
|
return this.rawModelStack.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
setIsRawModalVisible(visible) {
|
setIsRawModalVisible(visible) {
|
||||||
this.isRawModalVisible = visible;
|
if (visible) this.rawModelStack.push(true);
|
||||||
|
else this.rawModelStack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(action) {
|
navigate(action) {
|
||||||
@@ -122,6 +127,12 @@ class Navigation extends EventEmitter {
|
|||||||
action.parentId,
|
action.parentId,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
[cons.actions.navigation.OPEN_JOIN_ALIAS]: () => {
|
||||||
|
this.emit(
|
||||||
|
cons.events.navigation.JOIN_ALIAS_OPENED,
|
||||||
|
action.term,
|
||||||
|
);
|
||||||
|
},
|
||||||
[cons.actions.navigation.OPEN_INVITE_USER]: () => {
|
[cons.actions.navigation.OPEN_INVITE_USER]: () => {
|
||||||
this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm);
|
this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm);
|
||||||
},
|
},
|
||||||
@@ -185,6 +196,13 @@ class Navigation extends EventEmitter {
|
|||||||
action.afterClose,
|
action.afterClose,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
[cons.actions.navigation.OPEN_EMOJI_VERIFICATION]: () => {
|
||||||
|
this.emit(
|
||||||
|
cons.events.navigation.EMOJI_VERIFICATION_OPENED,
|
||||||
|
action.request,
|
||||||
|
action.targetDevice,
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
actions[action.type]?.();
|
actions[action.type]?.();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -475,6 +475,10 @@ textarea {
|
|||||||
supported by Chrome, Edge, Opera and Firefox */
|
supported by Chrome, Edge, Opera and Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.flex--center {
|
.flex--center {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user