Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d59a4de48 | ||
|
|
73c74d0477 | ||
|
|
70ffd7ded8 | ||
|
|
92a3a8d6fa | ||
|
|
6e9cd02b2b | ||
|
|
d0f90af251 | ||
|
|
c53eb27c6f | ||
|
|
38773e89ff | ||
|
|
19cb30d360 | ||
|
|
50a734b977 | ||
|
|
b988758ac2 | ||
|
|
2567328e8b | ||
|
|
a7568fcbbf | ||
|
|
27a06ae90c | ||
|
|
211fd19031 | ||
|
|
fe18611b4b | ||
|
|
5e9b45ad5f | ||
|
|
af833daee4 | ||
|
|
1ad5317d6e | ||
|
|
7cf5df80ce | ||
|
|
d6b880d110 | ||
|
|
cf58a4376e | ||
|
|
a76dcb289a | ||
|
|
48ec6224e7 | ||
|
|
127dd8baf4 | ||
|
|
d25c3ff4fc | ||
|
|
f0e9de4cf9 | ||
|
|
92607a788e | ||
|
|
82948c1f55 | ||
|
|
714929c72f | ||
|
|
98f0d384cb | ||
|
|
e3c87ecf65 | ||
|
|
1fb9e98eeb | ||
|
|
01d7c8ec36 | ||
|
|
22d8d5a0b8 | ||
|
|
631ed997ba | ||
|
|
01930ab0cf | ||
|
|
b8fe4c937e | ||
|
|
a7a5b08ad8 | ||
|
|
faaac72b81 | ||
|
|
8f41139076 | ||
|
|
e1a67acde1 | ||
|
|
4ab2af51a5 | ||
|
|
9ffc4eaa40 | ||
|
|
5eda2581f4 | ||
|
|
21ceb4fdc4 | ||
|
|
edcf43efba | ||
|
|
eb1ef16b5e | ||
|
|
6b9c8b7a87 | ||
|
|
a05b96e9a0 | ||
|
|
699f67aa75 | ||
|
|
76e469e444 | ||
|
|
8df630ee0c | ||
|
|
b0c4c53880 | ||
|
|
116a9c9de0 | ||
|
|
e63f8ab8b8 | ||
|
|
424245df18 | ||
|
|
094a11ec64 | ||
|
|
6b03c9c99f | ||
|
|
3d116af011 | ||
|
|
cbd965d8e5 | ||
|
|
c9d972fc12 | ||
|
|
da9979494e | ||
|
|
f5c907af33 | ||
|
|
906fc2dd3d | ||
|
|
a62df536dd | ||
|
|
7db674b65d | ||
|
|
784deaa6ea | ||
|
|
8bc41c2c32 | ||
|
|
79afc7649d | ||
|
|
2eee3736df | ||
|
|
a8ba38ef1e | ||
|
|
d3ddbc0c72 | ||
|
|
6c4085398e | ||
|
|
2308578622 | ||
|
|
47cd87e653 | ||
|
|
87c9684c71 | ||
|
|
0b66d73178 | ||
|
|
24cd238eaf | ||
|
|
72bb9b80ad | ||
|
|
3d42ac8e44 | ||
|
|
205ab3b16d | ||
|
|
bb25da274d | ||
|
|
b56de35664 | ||
|
|
0655ac82f2 | ||
|
|
166dbfa491 | ||
|
|
9348977548 | ||
|
|
769c3eea9b | ||
|
|
e7095af5b8 | ||
|
|
fd42d72423 | ||
|
|
1382c455a2 | ||
|
|
1d7fbc841e | ||
|
|
a8a168b0a7 | ||
|
|
dbd1d02485 | ||
|
|
1c307daa03 | ||
|
|
7a31f84d34 | ||
|
|
a2a4dd9862 | ||
|
|
3d8e0f6c23 | ||
|
|
146da2d072 | ||
|
|
6a9964e889 | ||
|
|
318e7c7458 | ||
|
|
f8e2d27bb0 | ||
|
|
44544f3289 | ||
|
|
76a5c99b08 | ||
|
|
70f0f91a17 | ||
|
|
f1394239a3 | ||
|
|
9e810502c2 | ||
|
|
c67a1752c5 | ||
|
|
d87e40ada5 | ||
|
|
86bbaa437e | ||
|
|
96b6b56d95 | ||
|
|
1f6e5e71ef | ||
|
|
a55f1df17f | ||
|
|
e654226e60 | ||
|
|
189dc93a6e | ||
|
|
76cb52878c | ||
|
|
8d3f0a9f4d | ||
|
|
fe674ef2ea | ||
|
|
33b165518e | ||
|
|
ff9d509137 | ||
|
|
278fd5bd59 | ||
|
|
8ddbc24dd4 | ||
|
|
e93f429906 | ||
|
|
1dccb1bb64 | ||
|
|
6d9e67b9f2 | ||
|
|
4803d48ec7 | ||
|
|
7c6a12ea8e | ||
|
|
a17d5d01a7 | ||
|
|
cdd909f2dd | ||
|
|
d0e9728c26 | ||
|
|
c8ae428df8 | ||
|
|
a9692f7db4 | ||
|
|
d05a426d00 | ||
|
|
a842b4c4d9 | ||
|
|
101abaeda1 | ||
|
|
bb99167433 | ||
|
|
e5b57e5ff9 | ||
|
|
2bd598f7f8 | ||
|
|
b008c5f07f | ||
|
|
a59aa2c30f | ||
|
|
93de0d4bf3 | ||
|
|
7a0ac2077c | ||
|
|
952f459e70 | ||
|
|
4702196c6e | ||
|
|
f77e064f4e | ||
|
|
c74a9a30bf | ||
|
|
1d43b36380 | ||
|
|
736f4edfbd | ||
|
|
c7e2540f6f | ||
|
|
f796296286 | ||
|
|
eb206d2abb | ||
|
|
bff62fb105 | ||
|
|
5fc83d7615 | ||
|
|
552a324e08 | ||
|
|
b155d7d1ba | ||
|
|
e4571bf668 | ||
|
|
53cc929a72 | ||
|
|
20f5a4cc91 | ||
|
|
2aabe559bf | ||
|
|
dcf0902c5b | ||
|
|
0e8219b200 | ||
|
|
5f7fa0557f | ||
|
|
63fb4d57e2 | ||
|
|
b5d6f44f4c | ||
|
|
b21c8f8c3a | ||
|
|
fe7c7660d3 | ||
|
|
56de33c821 | ||
|
|
1d442cd0e2 | ||
|
|
c79d7957f6 | ||
|
|
1b9216b341 | ||
|
|
6d97e5fa5d | ||
|
|
162af35254 | ||
|
|
abe03811f1 | ||
|
|
e85a869733 | ||
|
|
0c32d5302c | ||
|
|
f310196937 | ||
|
|
c828dfd596 | ||
|
|
d0b4e092b3 | ||
|
|
91d7d78621 | ||
|
|
2bc21f13d4 | ||
|
|
833edc9568 | ||
|
|
9d8efce26d | ||
|
|
6d778ca223 | ||
|
|
1e739e94e2 | ||
|
|
8c013aa2a9 | ||
|
|
fb0a0b0dc2 | ||
|
|
ab3d9ddb13 | ||
|
|
a67b32b3ce | ||
|
|
e15e690c5d |
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.git/
|
||||||
@@ -20,5 +20,9 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'linebreak-style': 0,
|
'linebreak-style': 0,
|
||||||
'no-underscore-dangle': 0,
|
'no-underscore-dangle': 0,
|
||||||
|
'react/no-unstable-nested-components': [
|
||||||
|
'error',
|
||||||
|
{ allowAsProps: true },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
28
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: "tuesday"
|
||||||
|
time: "01:00"
|
||||||
|
timezone: "Asia/Kolkata"
|
||||||
|
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: "tuesday"
|
||||||
|
time: "01:00"
|
||||||
|
timezone: "Asia/Kolkata"
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: "tuesday"
|
||||||
|
time: "01:00"
|
||||||
|
timezone: "Asia/Kolkata"
|
||||||
59
.github/workflows/build-pull-request.yml
vendored
@@ -1,32 +1,39 @@
|
|||||||
name: 'Build PR'
|
name: 'Build pull request'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: ['opened', 'synchronize']
|
types: ['opened', 'synchronize']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-pull-request:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{github.event.number}}
|
PR_NUMBER: ${{github.event.number}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Check out the repo
|
||||||
- name: Build
|
uses: actions/checkout@v3.0.0
|
||||||
run: npm install && npm run build
|
- name: Build app
|
||||||
- name: Upload Artifact
|
run: npm ci && npm run build
|
||||||
uses: actions/upload-artifact@v2
|
- name: Upload artifact
|
||||||
with:
|
uses: actions/upload-artifact@v3.0.0
|
||||||
name: previewbuild
|
with:
|
||||||
path: dist
|
name: previewbuild
|
||||||
retention-days: 1
|
path: dist
|
||||||
- uses: actions/github-script@v3.1.0
|
retention-days: 1
|
||||||
with:
|
- name: Get PR info
|
||||||
script: |
|
uses: actions/github-script@v6.0.0
|
||||||
var fs = require('fs');
|
with:
|
||||||
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
script: |
|
||||||
- name: Upload PR Info
|
var fs = require('fs');
|
||||||
uses: actions/upload-artifact@v2
|
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
||||||
with:
|
- name: Upload PR Info
|
||||||
name: pr.json
|
uses: actions/upload-artifact@v3.0.0
|
||||||
path: pr.json
|
with:
|
||||||
retention-days: 1
|
name: pr.json
|
||||||
|
path: pr.json
|
||||||
|
retention-days: 1
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v2.10.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
|||||||
150
.github/workflows/deploy-pull-request.yml
vendored
@@ -1,78 +1,78 @@
|
|||||||
name: Upload Preview Build to Netlify
|
name: Upload Preview Build to Netlify
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Build PR"]
|
workflows: ["Build pull request"]
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
get-build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >
|
if: >
|
||||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
# There's a 'download artifact' action but it hasn't been updated for the
|
# There's a 'download artifact' action but it hasn't been updated for the
|
||||||
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
|
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
|
||||||
# so instead we get this mess:
|
# so instead we get this mess:
|
||||||
- name: 'Download artifact'
|
- name: 'Download artifact'
|
||||||
uses: actions/github-script@v3.1.0
|
uses: actions/github-script@v6.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
var artifacts = await github.actions.listWorkflowRunArtifacts({
|
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
run_id: ${{github.event.workflow_run.id }},
|
run_id: ${{github.event.workflow_run.id }},
|
||||||
});
|
});
|
||||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
return artifact.name == "previewbuild"
|
return artifact.name == "previewbuild"
|
||||||
})[0];
|
})[0];
|
||||||
var download = await github.actions.downloadArtifact({
|
var download = await github.rest.actions.downloadArtifact({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
artifact_id: matchArtifact.id,
|
artifact_id: matchArtifact.id,
|
||||||
archive_format: 'zip',
|
archive_format: 'zip',
|
||||||
});
|
});
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
||||||
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
|
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
return artifact.name == "pr.json"
|
return artifact.name == "pr.json"
|
||||||
})[0];
|
})[0];
|
||||||
var download = await github.actions.downloadArtifact({
|
var download = await github.rest.actions.downloadArtifact({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
artifact_id: prInfoArtifact.id,
|
artifact_id: prInfoArtifact.id,
|
||||||
archive_format: 'zip',
|
archive_format: 'zip',
|
||||||
});
|
});
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
|
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
|
||||||
- name: Extract Artifacts
|
- name: Extract Artifacts
|
||||||
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
|
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
|
||||||
- name: 'Read PR Info'
|
- name: 'Read PR Info'
|
||||||
id: readctx
|
id: readctx
|
||||||
uses: actions/github-script@v3.1.0
|
uses: actions/github-script@v6.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
|
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
|
||||||
console.log(`::set-output name=prnumber::${pr.number}`);
|
console.log(`::set-output name=prnumber::${pr.number}`);
|
||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: netlify
|
id: netlify
|
||||||
uses: nwtgck/actions-netlify@v1.2
|
uses: nwtgck/actions-netlify@v1.2.3
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: "Deploy from GitHub Actions"
|
deploy-message: "Deploy from GitHub Actions"
|
||||||
# These don't work because we're in workflow_run
|
# These don't work because we're in workflow_run
|
||||||
enable-pull-request-comment: false
|
enable-pull-request-comment: false
|
||||||
enable-commit-comment: false
|
enable-commit-comment: false
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
- name: Edit PR Description
|
- name: Edit PR Description
|
||||||
uses: velas/pr-description@v1.0.1
|
uses: velas/pr-description@v1.0.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
||||||
description-message: |
|
description-message: |
|
||||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts.
|
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||||
|
|||||||
34
.github/workflows/docker.yaml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Publish Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_registry:
|
|
||||||
name: Push Docker image to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v3
|
|
||||||
with:
|
|
||||||
images: ajbura/cinny
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
7
.github/workflows/netlify-dev.yaml
vendored
@@ -11,11 +11,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3.0.0
|
||||||
- uses: jsmrcaga/action-netlify-deploy@9cc40dcd499dd1511b3cc99912444f8970411cc6
|
- uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
||||||
with:
|
with:
|
||||||
|
install_command: "npm ci"
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE2_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE2_ID }}
|
||||||
BUILD_DIRECTORY: "dist"
|
BUILD_DIRECTORY: "dist"
|
||||||
NETLIFY_DEPLOY_MESSAGE: "Dev deploy v${{ github.ref }}"
|
NETLIFY_DEPLOY_MESSAGE: "Dev deploy v${{ github.ref }}"
|
||||||
NETLIFY_DEPLOY_TO_PROD: true
|
NETLIFY_DEPLOY_TO_PROD: true
|
||||||
|
|||||||
20
.github/workflows/netlify-prod.yaml
vendored
@@ -1,20 +0,0 @@
|
|||||||
name: 'Deploy to Netlify (prod)'
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: 'Deploy'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: jsmrcaga/action-netlify-deploy@9cc40dcd499dd1511b3cc99912444f8970411cc6
|
|
||||||
with:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
|
||||||
BUILD_DIRECTORY: "dist"
|
|
||||||
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
|
|
||||||
NETLIFY_DEPLOY_TO_PROD: true
|
|
||||||
56
.github/workflows/prod-deploy.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: 'Production deploy'
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-to-netlify:
|
||||||
|
name: 'Deploy to Netlify'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v3.0.0
|
||||||
|
- name: Build and deploy to Netlify
|
||||||
|
uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
||||||
|
with:
|
||||||
|
install_command: "npm ci"
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||||
|
BUILD_DIRECTORY: "dist"
|
||||||
|
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
|
||||||
|
NETLIFY_DEPLOY_TO_PROD: true
|
||||||
|
- name: Get version from tag
|
||||||
|
id: vars
|
||||||
|
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||||
|
- name: Create tar.gz
|
||||||
|
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
||||||
|
- name: Upload tagged release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
|
|
||||||
|
push_to_dockerhub:
|
||||||
|
name: Push Docker image to Docker Hub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v3.0.0
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1.14.1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3.6.2
|
||||||
|
with:
|
||||||
|
images: ajbura/cinny
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v2.10.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
14
Dockerfile
@@ -1,20 +1,18 @@
|
|||||||
## Builder
|
## Builder
|
||||||
FROM node:14-alpine as builder
|
FROM node:17.7.1-alpine3.15 as builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src
|
COPY package.json package-lock.json /src/
|
||||||
RUN npm install \
|
RUN npm ci
|
||||||
&& npm run build
|
COPY . /src/
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
## App
|
## App
|
||||||
FROM nginx:alpine
|
FROM nginx:1.21.6-alpine
|
||||||
|
|
||||||
COPY --from=builder /src/dist /app
|
COPY --from=builder /src/dist /app
|
||||||
|
|
||||||
# Insert wasm type into Nginx mime.types file so they load correctly.
|
|
||||||
RUN sed -i '3i\ \ \ \ application/wasm wasm\;' /etc/nginx/mime.types
|
|
||||||
|
|
||||||
RUN rm -rf /usr/share/nginx/html \
|
RUN rm -rf /usr/share/nginx/html \
|
||||||
&& ln -s /app /usr/share/nginx/html
|
&& ln -s /app /usr/share/nginx/html
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
- [About](#about)
|
- [About](#about)
|
||||||
- [Getting Started](https://cinny.in)
|
- [Getting Started](https://cinny.in)
|
||||||
- [Contributing](./CONTRIBUTING.md)
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
|
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
||||||
|
|
||||||
## About <a name = "about"></a>
|
## About <a name = "about"></a>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"defaultHomeserver": 5,
|
"defaultHomeserver": 4,
|
||||||
"homeserverList": [
|
"homeserverList": [
|
||||||
"boba.best",
|
|
||||||
"converser.eu",
|
"converser.eu",
|
||||||
"envs.net",
|
"envs.net",
|
||||||
"halogen.city",
|
"halogen.city",
|
||||||
"kde.org",
|
"kde.org",
|
||||||
"matrix.org",
|
"matrix.org",
|
||||||
"mozilla.modular.im"
|
"chat.mozilla.org"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
10716
package-lock.json
generated
75
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cinny",
|
"name": "cinny",
|
||||||
"version": "1.7.0",
|
"version": "1.8.1",
|
||||||
"description": "Yet another matrix client",
|
"description": "Yet another matrix client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -15,64 +15,71 @@
|
|||||||
"author": "Ajay Bura",
|
"author": "Ajay Bura",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/inter": "^4.5.5",
|
||||||
|
"@fontsource/roboto": "^4.5.3",
|
||||||
"@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.5",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"dateformat": "^4.5.1",
|
"dateformat": "^5.0.3",
|
||||||
"emojibase-data": "^6.2.0",
|
"emojibase-data": "^7.0.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"flux": "^4.0.1",
|
"flux": "^4.0.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"html-react-parser": "^1.2.7",
|
"html-react-parser": "^1.4.8",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"matrix-js-sdk": "^15.2.1",
|
"matrix-js-sdk": "^15.6.0",
|
||||||
"micromark": "^3.0.3",
|
"micromark": "^3.0.10",
|
||||||
"micromark-extension-gfm": "^1.0.0",
|
"micromark-extension-gfm": "^2.0.1",
|
||||||
"prop-types": "^15.7.2",
|
"micromark-util-chunked": "^1.0.0",
|
||||||
|
"micromark-util-resolve-all": "^1.0.0",
|
||||||
|
"micromark-util-symbol": "^1.0.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-autosize-textarea": "^7.1.0",
|
"react-autosize-textarea": "^7.1.0",
|
||||||
|
"react-dnd": "^15.1.1",
|
||||||
|
"react-dnd-html5-backend": "^15.1.2",
|
||||||
"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.13.1",
|
"react-modal": "^3.14.4",
|
||||||
"sanitize-html": "^2.5.3",
|
"sanitize-html": "^2.7.0",
|
||||||
"tippy.js": "^6.3.1",
|
"tippy.js": "^6.3.7",
|
||||||
"twemoji": "^13.1.0"
|
"twemoji": "^14.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.15.5",
|
"@babel/core": "^7.17.7",
|
||||||
"@babel/preset-env": "^7.13.12",
|
"@babel/preset-env": "^7.16.11",
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"copy-webpack-plugin": "^9.0.1",
|
"copy-webpack-plugin": "^10.2.4",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"css-loader": "^5.2.0",
|
"css-loader": "^6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "^1.3.0",
|
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||||
"eslint": "^7.23.0",
|
"eslint": "^8.11.0",
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-react": "^7.23.1",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"favicons": "^6.2.1",
|
"favicons": "^6.2.2",
|
||||||
"favicons-webpack-plugin": "^5.0.2",
|
"favicons-webpack-plugin": "^5.0.2",
|
||||||
"file-loader": "^6.2.0",
|
"html-loader": "^3.1.0",
|
||||||
"html-loader": "^2.1.2",
|
|
||||||
"html-webpack-plugin": "^5.3.1",
|
"html-webpack-plugin": "^5.3.1",
|
||||||
"mini-css-extract-plugin": "^1.4.0",
|
"mini-css-extract-plugin": "^2.6.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.49.9",
|
||||||
"sass-loader": "^11.0.1",
|
"sass-loader": "^12.6.0",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^3.3.1",
|
||||||
|
"url": "^0.11.0",
|
||||||
"util": "^0.12.4",
|
"util": "^0.12.4",
|
||||||
"webpack": "^5.62.1",
|
"webpack": "^5.70.0",
|
||||||
"webpack-cli": "^4.9.1",
|
"webpack-cli": "^4.9.2",
|
||||||
"webpack-dev-server": "^4.4.0",
|
"webpack-dev-server": "^4.7.4",
|
||||||
"webpack-merge": "^5.7.3"
|
"webpack-merge": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0 user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Cinny</title>
|
<title>Cinny</title>
|
||||||
<meta name="name" content="Cinny">
|
<meta name="name" content="Cinny">
|
||||||
<meta name="author" content="Ajay Bura">
|
<meta name="author" content="Ajay Bura">
|
||||||
|
|||||||
395
public/res/LICENSE
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
Attribution 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution 4.0 International Public License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution 4.0 International Public License ("Public License"). To the
|
||||||
|
extent this Public License may be interpreted as a contract, You are
|
||||||
|
granted the Licensed Rights in consideration of Your acceptance of
|
||||||
|
these terms and conditions, and the Licensor grants You such rights in
|
||||||
|
consideration of benefits the Licensor receives from making the
|
||||||
|
Licensed Material available under these terms and conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
d. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
f. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
g. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
i. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
j. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
k. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified
|
||||||
|
form), You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
4. If You Share Adapted Material You produce, the Adapter's
|
||||||
|
License You apply must not prevent recipients of the Adapted
|
||||||
|
Material from complying with this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material; and
|
||||||
|
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public
|
||||||
|
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||||
|
its public licenses to material it publishes and in those instances
|
||||||
|
will be considered the “Licensor.” The text of the Creative Commons
|
||||||
|
public licenses is dedicated to the public domain under the CC0 Public
|
||||||
|
Domain Dedication. Except for the limited purpose of indicating that
|
||||||
|
material is shared under a Creative Commons public license or as
|
||||||
|
otherwise permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the
|
||||||
|
public licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
7
public/res/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Graphics (c) by Ajay Bura (ajbura)
|
||||||
|
|
||||||
|
Graphic content is licensed under a
|
||||||
|
Creative Commons Attribution 4.0 International License.
|
||||||
|
|
||||||
|
You should have received a copy of the license along with this
|
||||||
|
work. If not, see <http://creativecommons.org/licenses/by/4.0/>;.
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
18
public/res/ic/filled/category.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#231F20" d="M9,11H5c-1.1,0-2-0.9-2-2V5c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C11,10.1,10.1,11,9,11z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#231F20" d="M19,11h-4c-1.1,0-2-0.9-2-2V5c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C21,10.1,20.1,11,19,11z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#231F20" d="M9,21H5c-1.1,0-2-0.9-2-2v-4c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C11,20.1,10.1,21,9,21z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#231F20" d="M19,21h-4c-1.1,0-2-0.9-2-2v-4c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2v4C21,20.1,20.1,21,19,21z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 940 B |
11
public/res/ic/outlined/add-pin.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M13.8,4.7l0.7,0.7l-3.4,3.4L7.7,10l-1-1l-1.4,1.4l3.5,3.5l-5.7,5.7L4.6,21l5.7-5.7l3.5,3.5l1.4-1.4l-1-1l1.1-3.4l3.4-3.4
|
||||||
|
l0.7,0.7L20.9,9l-5.7-5.7L13.8,4.7z M13.7,12l-1,2.9l-3.4-3.4l2.9-1l3.7-3.7l1.4,1.4L13.7,12z"/>
|
||||||
|
<polygon points="10,3.3 7.8,3.3 7.8,1 6.3,1 6.3,3.3 4,3.3 4,4.8 6.3,4.8 6.3,7 7.8,7 7.8,4.8 10,4.8 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 781 B |
15
public/res/ic/outlined/category.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#221F1F" d="M9,5v4H5V5H9 M9,3H5C3.9,3,3,3.9,3,5v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2V5C11,3.9,10.1,3,9,3L9,3z"/>
|
||||||
|
<path fill="#221F1F" d="M19,5v4h-4V5H19 M19,3h-4c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3L19,3
|
||||||
|
z"/>
|
||||||
|
<path fill="#221F1F" d="M9,15v4H5v-4H9 M9,13H5c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2v-4C11,13.9,10.1,13,9,13
|
||||||
|
L9,13z"/>
|
||||||
|
<path fill="#221F1F" d="M19,15v4h-4v-4H19 M19,13h-4c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h4c1.1,0,2-0.9,2-2v-4
|
||||||
|
C21,13.9,20.1,13,19,13L19,13z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 995 B |
11
public/res/ic/outlined/horizontal-menu.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<circle cx="5" cy="12" r="2"/>
|
||||||
|
<circle cx="12" cy="12" r="2"/>
|
||||||
|
<circle cx="19" cy="12" r="2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 551 B |
16
public/res/ic/outlined/recent-clock.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<polygon fill="#010101" points="11,6 11,12 15.2,16.2 16.7,14.8 13,11.2 13,6 "/>
|
||||||
|
<path fill="#010101" d="M12,2C6.5,2,2,6.5,2,12H0.2L3,14.8L5.8,12H4c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8c-1.9,0-3.7-0.7-5-1.8
|
||||||
|
l-1.2,1.6C7.4,21.2,9.6,22,12,22c5.5,0,10-4.5,10-10S17.5,2,12,2z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon fill="#010101" points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/>
|
||||||
|
<path fill="#010101" d="M50,40c-5.5,0-10,4.5-10,10h-1.8l2.8,2.8l2.8-2.8H42c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8
|
||||||
|
c-1.9,0-3.7-0.7-5-1.8l-1.2,1.6c1.7,1.4,3.9,2.2,6.3,2.2c5.5,0,10-4.5,10-10S55.5,40,50,40z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
12
public/res/ic/outlined/space-plus.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M15.4,13.7L14,14l-0.2,1.4c-0.4,2.8-1.3,4.2-1.7,4.6c-0.4-0.3-1.3-1.8-1.7-4.6L10,14l-1.4-0.2c-2.8-0.4-4.2-1.3-4.6-1.7
|
||||||
|
c0.3-0.4,1.8-1.3,4.6-1.7L10,10l0.2-1.4c0.4-2.8,1.3-4.2,1.7-4.6V2c-1.7,0-3.1,2.6-3.7,6.3C4.6,8.9,2,10.3,2,12s2.6,3.1,6.3,3.7
|
||||||
|
c0.6,3.7,2,6.3,3.7,6.3s3.1-2.6,3.7-6.3c3.7-0.6,6.3-2,6.3-3.7h-2.1C19.6,12.4,18.2,13.3,15.4,13.7z"/>
|
||||||
|
<path d="M19,0c-2.8,0-5,2.2-5,5s2.2,5,5,5s5-2.2,5-5S21.8,0,19,0z M22,5.8h-2.3V8h-1.5V5.8H16V4.3h2.3V2h1.5v2.3H22V5.8z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 930 B |
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14576) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="789.322px" height="336.807px" viewBox="0 0 789.322 336.807" enable-background="new 0 0 789.322 336.807"
|
|
||||||
xml:space="preserve">
|
|
||||||
<path d="M8.876,7.71v321.386h23.13v7.711H0V0h32.006v7.71H8.876z"/>
|
|
||||||
<path d="M97.989,109.594v16.264h0.463c4.338-6.191,9.563-10.998,15.684-14.406c6.117-3.402,13.129-5.11,21.027-5.11
|
|
||||||
c7.588,0,14.521,1.475,20.793,4.415c6.274,2.945,11.038,8.131,14.291,15.567c3.56-5.265,8.4-9.913,14.521-13.94
|
|
||||||
c6.117-4.025,13.358-6.042,21.724-6.042c6.351,0,12.234,0.776,17.66,2.325c5.418,1.549,10.065,4.027,13.938,7.434
|
|
||||||
c3.869,3.41,6.889,7.863,9.062,13.357c2.167,5.504,3.253,12.122,3.253,19.869v80.385h-32.993v-68.074
|
|
||||||
c0-4.025-0.154-7.82-0.465-11.385c-0.313-3.56-1.161-6.656-2.555-9.293c-1.395-2.631-3.45-4.724-6.157-6.274
|
|
||||||
c-2.711-1.543-6.391-2.322-11.037-2.322s-8.403,0.896-11.269,2.671c-2.868,1.784-5.112,4.109-6.737,6.971
|
|
||||||
c-1.626,2.869-2.711,6.12-3.252,9.762c-0.545,3.638-0.814,7.318-0.814,11.035v66.91h-32.991v-67.375c0-3.562-0.081-7.087-0.23-10.57
|
|
||||||
c-0.158-3.487-0.814-6.7-1.978-9.645c-1.162-2.94-3.099-5.304-5.809-7.088c-2.711-1.775-6.699-2.671-11.965-2.671
|
|
||||||
c-1.551,0-3.603,0.349-6.156,1.048c-2.556,0.697-5.036,2.016-7.435,3.949c-2.404,1.938-4.454,4.726-6.158,8.363
|
|
||||||
c-1.705,3.642-2.556,8.402-2.556,14.287v69.701h-32.99V109.594H97.989z"/>
|
|
||||||
<path d="M271.545,127.254c3.405-5.113,7.744-9.215,13.012-12.316c5.264-3.097,11.186-5.303,17.771-6.621
|
|
||||||
c6.582-1.315,13.205-1.976,19.865-1.976c6.042,0,12.158,0.428,18.354,1.277c6.195,0.855,11.85,2.522,16.962,4.997
|
|
||||||
c5.111,2.477,9.292,5.926,12.546,10.338c3.253,4.414,4.879,10.262,4.879,17.543v62.494c0,5.428,0.31,10.611,0.931,15.567
|
|
||||||
c0.615,4.959,1.701,8.676,3.251,11.153h-33.455c-0.621-1.86-1.126-3.755-1.511-5.693c-0.39-1.933-0.661-3.908-0.813-5.923
|
|
||||||
c-5.267,5.422-11.465,9.217-18.585,11.386c-7.127,2.163-14.407,3.251-21.842,3.251c-5.733,0-11.077-0.698-16.033-2.09
|
|
||||||
c-4.958-1.395-9.293-3.562-13.01-6.51c-3.718-2.938-6.622-6.656-8.713-11.147s-3.138-9.84-3.138-16.033
|
|
||||||
c0-6.813,1.199-12.43,3.604-16.84c2.399-4.417,5.495-7.939,9.295-10.575c3.793-2.632,8.129-4.607,13.01-5.923
|
|
||||||
c4.878-1.315,9.795-2.358,14.752-3.137c4.957-0.772,9.835-1.393,14.638-1.857c4.801-0.466,9.062-1.164,12.779-2.093
|
|
||||||
c3.718-0.929,6.658-2.282,8.829-4.065c2.165-1.781,3.172-4.375,3.02-7.785c0-3.56-0.58-6.389-1.742-8.479
|
|
||||||
c-1.161-2.09-2.711-3.719-4.646-4.88c-1.937-1.161-4.183-1.936-6.737-2.325c-2.557-0.382-5.309-0.58-8.248-0.58
|
|
||||||
c-6.506,0-11.617,1.395-15.335,4.183c-3.716,2.788-5.889,7.437-6.506,13.94h-32.991C266.2,138.793,268.133,132.362,271.545,127.254z
|
|
||||||
M336.714,173.837c-2.09,0.696-4.337,1.275-6.736,1.741c-2.402,0.465-4.918,0.853-7.551,1.161c-2.635,0.313-5.268,0.698-7.899,1.163
|
|
||||||
c-2.48,0.461-4.919,1.086-7.317,1.857c-2.404,0.779-4.495,1.822-6.274,3.138c-1.784,1.317-3.216,2.985-4.3,4.994
|
|
||||||
c-1.085,2.014-1.626,4.571-1.626,7.668c0,2.94,0.541,5.422,1.626,7.431c1.084,2.017,2.558,3.604,4.416,4.765
|
|
||||||
s4.025,1.976,6.507,2.438c2.475,0.466,5.031,0.698,7.665,0.698c6.505,0,11.537-1.082,15.103-3.253
|
|
||||||
c3.561-2.166,6.192-4.762,7.899-7.785c1.702-3.019,2.749-6.072,3.137-9.174c0.384-3.097,0.58-5.576,0.58-7.434V170.93
|
|
||||||
C340.548,172.172,338.806,173.139,336.714,173.837z"/>
|
|
||||||
<path d="M461.826,109.594v22.072h-24.161v59.479c0,5.573,0.928,9.292,2.788,11.149c1.856,1.859,5.576,2.788,11.152,2.788
|
|
||||||
c1.859,0,3.638-0.076,5.343-0.232c1.703-0.152,3.33-0.388,4.878-0.696v25.557c-2.788,0.465-5.887,0.773-9.293,0.931
|
|
||||||
c-3.407,0.149-6.737,0.23-9.99,0.23c-5.111,0-9.953-0.35-14.521-1.048c-4.571-0.695-8.597-2.047-12.081-4.063
|
|
||||||
c-3.486-2.011-6.236-4.88-8.248-8.597c-2.016-3.714-3.021-8.595-3.021-14.639v-70.859h-19.98v-22.072h19.98V73.582h32.992v36.012
|
|
||||||
H461.826z"/>
|
|
||||||
<path d="M508.989,109.594v22.306h0.465c1.546-3.72,3.636-7.163,6.272-10.341c2.634-3.172,5.652-5.885,9.06-8.131
|
|
||||||
c3.405-2.242,7.047-3.985,10.923-5.228c3.868-1.237,7.898-1.859,12.081-1.859c2.168,0,4.566,0.39,7.202,1.163v30.67
|
|
||||||
c-1.551-0.312-3.41-0.584-5.576-0.814c-2.17-0.233-4.26-0.35-6.274-0.35c-6.041,0-11.152,1.01-15.332,3.021
|
|
||||||
c-4.182,2.014-7.55,4.761-10.107,8.247c-2.555,3.487-4.379,7.55-5.462,12.198c-1.083,4.645-1.625,9.682-1.625,15.102v54.133h-32.991
|
|
||||||
V109.594H508.989z"/>
|
|
||||||
<path d="M568.931,91.006V63.823h32.994v27.183H568.931z M601.925,109.594v120.117h-32.994V109.594H601.925z"/>
|
|
||||||
<path d="M619.116,109.594h37.637l21.144,31.365l20.911-31.365h36.476l-39.496,56.226l44.377,63.892h-37.64l-25.093-37.87
|
|
||||||
l-25.094,37.87H615.4l43.213-63.193L619.116,109.594z"/>
|
|
||||||
<path d="M780.444,329.096V7.71h-23.13V0h32.008v336.807h-32.008v-7.711H780.444z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -8,20 +8,29 @@ import Text from '../text/Text';
|
|||||||
import RawIcon from '../system-icons/RawIcon';
|
import RawIcon from '../system-icons/RawIcon';
|
||||||
|
|
||||||
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
|
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
|
||||||
|
import { avatarInitials } from '../../../util/common';
|
||||||
|
|
||||||
function Avatar({
|
const Avatar = React.forwardRef(({
|
||||||
text, bgColor, iconSrc, iconColor, imageSrc, size,
|
text, bgColor, iconSrc, iconColor, imageSrc, size,
|
||||||
}) {
|
}, ref) => {
|
||||||
let textSize = 's1';
|
let textSize = 's1';
|
||||||
if (size === 'large') textSize = 'h1';
|
if (size === 'large') textSize = 'h1';
|
||||||
if (size === 'small') textSize = 'b1';
|
if (size === 'small') textSize = 'b1';
|
||||||
if (size === 'extra-small') textSize = 'b3';
|
if (size === 'extra-small') textSize = 'b3';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`avatar-container avatar-container__${size} noselect`}>
|
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
|
||||||
{
|
{
|
||||||
imageSrc !== null
|
imageSrc !== null
|
||||||
? <img draggable="false" src={imageSrc} onError={(e) => { e.target.src = ImageBrokenSVG; }} alt="avatar" />
|
? (
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
src={imageSrc}
|
||||||
|
onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
|
||||||
|
onError={(e) => { e.target.src = ImageBrokenSVG; }}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)
|
||||||
: (
|
: (
|
||||||
<span
|
<span
|
||||||
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
||||||
@@ -32,7 +41,7 @@ function Avatar({
|
|||||||
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||||
: text !== null && (
|
: text !== null && (
|
||||||
<Text variant={textSize} primary>
|
<Text variant={textSize} primary>
|
||||||
{twemojify([...text][0])}
|
{twemojify(avatarInitials(text))}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -41,7 +50,7 @@ function Avatar({
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
Avatar.defaultProps = {
|
Avatar.defaultProps = {
|
||||||
text: null,
|
text: null,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar__border {
|
.avatar__border {
|
||||||
|
|||||||
61
src/app/atoms/avatar/render.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { avatarInitials } from '../../../util/common';
|
||||||
|
|
||||||
|
function cssVar(name) {
|
||||||
|
return getComputedStyle(document.body).getPropertyValue(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// renders the avatar and returns it as an URL
|
||||||
|
export default async function renderAvatar({
|
||||||
|
text, bgColor, imageSrc, size, borderRadius, scale,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = size * scale;
|
||||||
|
canvas.height = size * scale;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
// rounded corners
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(size, size);
|
||||||
|
ctx.arcTo(0, size, 0, 0, borderRadius);
|
||||||
|
ctx.arcTo(0, 0, size, 0, borderRadius);
|
||||||
|
ctx.arcTo(size, 0, size, size, borderRadius);
|
||||||
|
ctx.arcTo(size, size, 0, size, borderRadius);
|
||||||
|
|
||||||
|
if (imageSrc) {
|
||||||
|
// clip corners of image
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.clip();
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
img.onerror = reject;
|
||||||
|
img.onload = resolve;
|
||||||
|
});
|
||||||
|
img.src = imageSrc;
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0, size, size);
|
||||||
|
} else {
|
||||||
|
// colored background
|
||||||
|
ctx.fillStyle = cssVar(bgColor);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// centered letter
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`;
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(avatarInitials(text), size / 2, size / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvas.toDataURL();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return imageSrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@use 'state';
|
@use 'state';
|
||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/text';
|
||||||
|
|
||||||
.btn-surface,
|
.btn-surface,
|
||||||
.btn-primary,
|
.btn-primary,
|
||||||
@@ -18,11 +19,16 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@include state.disabled;
|
@include state.disabled;
|
||||||
|
|
||||||
|
& .text {
|
||||||
|
@extend .cp-txt__ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
&--icon {
|
&--icon {
|
||||||
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
||||||
|
|
||||||
.ic-raw {
|
.ic-raw {
|
||||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
|
|||||||
import './Checkbox.scss';
|
import './Checkbox.scss';
|
||||||
|
|
||||||
function Checkbox({
|
function Checkbox({
|
||||||
variant, isActive, onToggle, disabled,
|
variant, isActive, onToggle,
|
||||||
|
disabled, tabIndex,
|
||||||
}) {
|
}) {
|
||||||
const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`;
|
const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`;
|
||||||
if (onToggle === null) return <span className={className} />;
|
if (onToggle === null) return <span className={className} />;
|
||||||
@@ -14,6 +15,7 @@ function Checkbox({
|
|||||||
className={className}
|
className={className}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
tabIndex={tabIndex}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -23,6 +25,7 @@ Checkbox.defaultProps = {
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
onToggle: null,
|
onToggle: null,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
tabIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
Checkbox.propTypes = {
|
Checkbox.propTypes = {
|
||||||
@@ -30,6 +33,7 @@ Checkbox.propTypes = {
|
|||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
onToggle: PropTypes.func,
|
onToggle: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
tabIndex: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Checkbox;
|
export default Checkbox;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function Input({
|
|||||||
id, label, name, value, placeholder,
|
id, label, name, value, placeholder,
|
||||||
required, type, onChange, forwardRef,
|
required, type, onChange, forwardRef,
|
||||||
resizable, minHeight, onResize, state,
|
resizable, minHeight, onResize, state,
|
||||||
onKeyDown, disabled,
|
onKeyDown, disabled, autoFocus,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
@@ -30,6 +30,7 @@ function Input({
|
|||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
autoFocus={autoFocus}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
@@ -45,6 +46,8 @@ function Input({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
|
autoFocus={autoFocus}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +70,7 @@ Input.defaultProps = {
|
|||||||
state: 'normal',
|
state: 'normal',
|
||||||
onKeyDown: null,
|
onKeyDown: null,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
autoFocus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Input.propTypes = {
|
Input.propTypes = {
|
||||||
@@ -85,6 +89,7 @@ Input.propTypes = {
|
|||||||
state: PropTypes.oneOf(['normal', 'success', 'error']),
|
state: PropTypes.oneOf(['normal', 'success', 'error']),
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
autoFocus: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RawModal.scss';
|
import './RawModal.scss';
|
||||||
|
|
||||||
@@ -26,7 +26,9 @@ function RawModal({
|
|||||||
modalClass += 'raw-modal__small ';
|
modalClass += 'raw-modal__small ';
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation.setIsRawModalVisible(isOpen);
|
useEffect(() => {
|
||||||
|
navigation.setIsRawModalVisible(isOpen);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const modalOverlayClass = (overlayClassName !== null) ? `${overlayClassName} ` : '';
|
const modalOverlayClass = (overlayClassName !== null) ? `${overlayClassName} ` : '';
|
||||||
return (
|
return (
|
||||||
|
|||||||
25
src/app/hooks/useCategorizedSpaces.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import initMatrix from '../../client/initMatrix';
|
||||||
|
import cons from '../../client/state/cons';
|
||||||
|
|
||||||
|
export function useCategorizedSpaces() {
|
||||||
|
const { accountData } = initMatrix;
|
||||||
|
const [categorizedSpaces, setCategorizedSpaces] = useState([...accountData.categorizedSpaces]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCategorizedSpaces = () => {
|
||||||
|
setCategorizedSpaces([...accountData.categorizedSpaces]);
|
||||||
|
};
|
||||||
|
accountData.on(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, handleCategorizedSpaces);
|
||||||
|
return () => {
|
||||||
|
accountData.removeListener(
|
||||||
|
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
|
||||||
|
handleCategorizedSpaces,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [categorizedSpaces];
|
||||||
|
}
|
||||||
28
src/app/hooks/usePermission.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function usePermission(name, initial) {
|
||||||
|
const [state, setState] = useState(initial);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let descriptor;
|
||||||
|
|
||||||
|
const update = () => setState(descriptor.state);
|
||||||
|
|
||||||
|
if (navigator.permissions?.query) {
|
||||||
|
navigator.permissions.query({ name }).then((_descriptor) => {
|
||||||
|
descriptor = _descriptor;
|
||||||
|
|
||||||
|
update();
|
||||||
|
descriptor.addEventListener('change', update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (descriptor) descriptor.removeEventListener('change', update);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [state, setState];
|
||||||
|
}
|
||||||
@@ -5,16 +5,19 @@ import initMatrix from '../../client/initMatrix';
|
|||||||
import cons from '../../client/state/cons';
|
import cons from '../../client/state/cons';
|
||||||
|
|
||||||
export function useSpaceShortcut() {
|
export function useSpaceShortcut() {
|
||||||
const { roomList } = initMatrix;
|
const { accountData } = initMatrix;
|
||||||
const [spaceShortcut, setSpaceShortcut] = useState([...roomList.spaceShortcut]);
|
const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onSpaceShortcutUpdated = () => {
|
const onSpaceShortcutUpdated = () => {
|
||||||
setSpaceShortcut([...roomList.spaceShortcut]);
|
setSpaceShortcut([...accountData.spaceShortcut]);
|
||||||
};
|
};
|
||||||
roomList.on(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
|
accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
|
||||||
return () => {
|
return () => {
|
||||||
roomList.removeListener(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
|
accountData.removeListener(
|
||||||
|
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
|
||||||
|
onSpaceShortcutUpdated,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ function Dialog({
|
|||||||
<div className="dialog__content">
|
<div className="dialog__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
|
{
|
||||||
|
typeof title === 'string'
|
||||||
|
? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
|
||||||
|
: title
|
||||||
|
}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
@@ -56,7 +60,7 @@ Dialog.defaultProps = {
|
|||||||
Dialog.propTypes = {
|
Dialog.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.node.isRequired,
|
||||||
contentOptions: PropTypes.node,
|
contentOptions: PropTypes.node,
|
||||||
onAfterOpen: PropTypes.func,
|
onAfterOpen: PropTypes.func,
|
||||||
onAfterClose: PropTypes.func,
|
onAfterClose: PropTypes.func,
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
.dialog-model {
|
.dialog-model {
|
||||||
--modal-height: 656px;
|
--modal-height: 656px;
|
||||||
max-height: var(--modal-height) !important;
|
max-height: min(100%, var(--modal-height));
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog,
|
||||||
|
.dialog__content,
|
||||||
|
.dialog__content__wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
width: 100%;
|
|
||||||
max-height: inherit;
|
|
||||||
background-color: var(--bg-surface);
|
background-color: var(--bg-surface);
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dialog__content-container {
|
.dialog__content-container {
|
||||||
padding-top: var(--sp-extra-tight);
|
padding-top: var(--sp-extra-tight);
|
||||||
padding-bottom: var(--sp-extra-loose);
|
padding-bottom: var(--sp-extra-loose);
|
||||||
}
|
|
||||||
.dialog__content__wrapper {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, {
|
||||||
|
useState, useEffect, useCallback, useRef,
|
||||||
|
} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Message.scss';
|
import './Message.scss';
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ import colorMXID from '../../../util/colorMXID';
|
|||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
||||||
import {
|
import {
|
||||||
openEmojiBoard, openProfileViewer, openReadReceipts, replyTo,
|
openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
|
||||||
} from '../../../client/action/navigation';
|
} from '../../../client/action/navigation';
|
||||||
import { sanitizeCustomHtml } from '../../../util/sanitize';
|
import { sanitizeCustomHtml } from '../../../util/sanitize';
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
|
|||||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||||
|
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
|
||||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||||
|
|
||||||
function PlaceholderMessage() {
|
function PlaceholderMessage() {
|
||||||
@@ -114,22 +117,31 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
|||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const timelineSet = roomTimeline.getUnfilteredTimelineSet();
|
const timelineSet = roomTimeline.getUnfilteredTimelineSet();
|
||||||
const loadReply = async () => {
|
const loadReply = async () => {
|
||||||
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
|
try {
|
||||||
await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
|
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
|
||||||
|
await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
|
||||||
|
|
||||||
const mEvent = eTimeline.getTimelineSet().findEventById(eventId);
|
const mEvent = eTimeline.getTimelineSet().findEventById(eventId);
|
||||||
|
|
||||||
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 content ***';
|
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
|
||||||
setReply({
|
setReply({
|
||||||
to: username,
|
to: username,
|
||||||
color: colorMXID(mEvent.getSender()),
|
color: colorMXID(mEvent.getSender()),
|
||||||
body: parseReply(rawBody)?.body ?? rawBody ?? fallbackBody,
|
body: parseReply(rawBody)?.body ?? rawBody ?? fallbackBody,
|
||||||
event: mEvent,
|
event: mEvent,
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
setReply({
|
||||||
|
to: '** Unknown user **',
|
||||||
|
color: 'var(--tc-danger-normal)',
|
||||||
|
body: '*** Unable to load reply ***',
|
||||||
|
event: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
loadReply();
|
loadReply();
|
||||||
|
|
||||||
@@ -138,9 +150,13 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const focusReply = () => {
|
const focusReply = (ev) => {
|
||||||
if (reply?.event.isRedacted()) return;
|
if (!ev.keyCode || ev.keyCode === 32 || ev.keyCode === 13) {
|
||||||
roomTimeline.loadEventTimeline(eventId);
|
if (ev.keyCode) ev.preventDefault();
|
||||||
|
if (reply?.event === null) return;
|
||||||
|
if (reply?.event.isRedacted()) return;
|
||||||
|
roomTimeline.loadEventTimeline(eventId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -188,10 +204,10 @@ const MessageBody = React.memo(({
|
|||||||
// Count the number of emojis
|
// Count the number of emojis
|
||||||
const nEmojis = content.filter((e) => e.type === 'img').length;
|
const nEmojis = content.filter((e) => e.type === 'img').length;
|
||||||
|
|
||||||
// Make sure there's no text besides whitespace
|
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
||||||
if (nEmojis <= 10 && content.every((element) => (
|
if (nEmojis <= 10 && content.every((element) => (
|
||||||
(typeof element === 'object' && element.type === 'img')
|
(typeof element === 'object' && element.type === 'img')
|
||||||
|| (typeof element === 'string' && /^\s*$/g.test(element))
|
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
||||||
))) {
|
))) {
|
||||||
emojiOnly = true;
|
emojiOnly = true;
|
||||||
}
|
}
|
||||||
@@ -235,6 +251,12 @@ MessageBody.propTypes = {
|
|||||||
function MessageEdit({ body, onSave, onCancel }) {
|
function MessageEdit({ body, onSave, onCancel }) {
|
||||||
const editInputRef = useRef(null);
|
const editInputRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// makes the cursor end up at the end of the line instead of the beginning
|
||||||
|
editInputRef.current.value = '';
|
||||||
|
editInputRef.current.value = body;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.keyCode === 13 && e.shiftKey === false) {
|
if (e.keyCode === 13 && e.shiftKey === false) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -251,6 +273,7 @@ function MessageEdit({ body, onSave, onCancel }) {
|
|||||||
placeholder="Edit message"
|
placeholder="Edit message"
|
||||||
required
|
required
|
||||||
resizable
|
resizable
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="message__edit-btns">
|
<div className="message__edit-btns">
|
||||||
<Button type="submit" variant="primary">Save</Button>
|
<Button type="submit" variant="primary">Save</Button>
|
||||||
@@ -304,9 +327,11 @@ function genReactionMsg(userIds, reaction) {
|
|||||||
{userIds.map((userId, index) => (
|
{userIds.map((userId, index) => (
|
||||||
<React.Fragment key={userId}>
|
<React.Fragment key={userId}>
|
||||||
{twemojify(getUsername(userId))}
|
{twemojify(getUsername(userId))}
|
||||||
<span style={{ opacity: '.6' }}>
|
{index < userIds.length - 1 && (
|
||||||
{index === userIds.length - 1 ? ' and ' : ', '}
|
<span style={{ opacity: '.6' }}>
|
||||||
</span>
|
{index === userIds.length - 2 ? ' and ' : ', '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
||||||
@@ -355,13 +380,12 @@ MessageReaction.propTypes = {
|
|||||||
|
|
||||||
function MessageReactionGroup({ roomTimeline, mEvent }) {
|
function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||||
const { roomId, room, reactionTimeline } = roomTimeline;
|
const { roomId, room, reactionTimeline } = roomTimeline;
|
||||||
const eventId = mEvent.getId();
|
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const reactions = {};
|
const reactions = {};
|
||||||
const shortcodeToEmoji = getShortcodeToCustomEmoji(room);
|
const shortcodeToEmoji = getShortcodeToCustomEmoji(room);
|
||||||
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
||||||
|
|
||||||
const eventReactions = reactionTimeline.get(eventId);
|
const eventReactions = reactionTimeline.get(mEvent.getId());
|
||||||
const addReaction = (key, count, senderId, isActive) => {
|
const addReaction = (key, count, senderId, isActive) => {
|
||||||
let reaction = reactions[key];
|
let reaction = reactions[key];
|
||||||
if (reaction === undefined) {
|
if (reaction === undefined) {
|
||||||
@@ -412,7 +436,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
|||||||
users={reactions[key].users}
|
users={reactions[key].users}
|
||||||
isActive={reactions[key].isActive}
|
isActive={reactions[key].isActive}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toggleEmoji(roomId, eventId, key, roomTimeline);
|
toggleEmoji(roomId, mEvent.getId(), key, roomTimeline);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -420,7 +444,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
|||||||
{canSendReaction && (
|
{canSendReaction && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
pickEmoji(e, roomId, eventId, roomTimeline);
|
pickEmoji(e, roomId, mEvent.getId(), roomTimeline);
|
||||||
}}
|
}}
|
||||||
src={EmojiAddIC}
|
src={EmojiAddIC}
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
@@ -445,12 +469,23 @@ function isMedia(mE) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if editedTimeline has mEventId then pass editedMEvent else pass mEvent to openViewSource
|
||||||
|
function handleOpenViewSource(mEvent, roomTimeline) {
|
||||||
|
const eventId = mEvent.getId();
|
||||||
|
const { editedTimeline } = roomTimeline ?? {};
|
||||||
|
let editedMEvent;
|
||||||
|
if (editedTimeline?.has(eventId)) {
|
||||||
|
const editedList = editedTimeline.get(eventId);
|
||||||
|
editedMEvent = editedList[editedList.length - 1];
|
||||||
|
}
|
||||||
|
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
||||||
|
}
|
||||||
|
|
||||||
const MessageOptions = React.memo(({
|
const MessageOptions = React.memo(({
|
||||||
roomTimeline, mEvent, edit, reply,
|
roomTimeline, mEvent, edit, reply,
|
||||||
}) => {
|
}) => {
|
||||||
const { roomId, room } = roomTimeline;
|
const { roomId, room } = roomTimeline;
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const eventId = mEvent.getId();
|
|
||||||
const senderId = mEvent.getSender();
|
const senderId = mEvent.getSender();
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
|
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
|
||||||
@@ -461,7 +496,7 @@ const MessageOptions = React.memo(({
|
|||||||
<div className="message__options">
|
<div className="message__options">
|
||||||
{canSendReaction && (
|
{canSendReaction && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => pickEmoji(e, roomId, eventId, roomTimeline)}
|
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
|
||||||
src={EmojiAddIC}
|
src={EmojiAddIC}
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
tooltip="Add reaction"
|
tooltip="Add reaction"
|
||||||
@@ -491,6 +526,12 @@ const MessageOptions = React.memo(({
|
|||||||
>
|
>
|
||||||
Read receipts
|
Read receipts
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={CmdIC}
|
||||||
|
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
|
||||||
|
>
|
||||||
|
View source
|
||||||
|
</MenuItem>
|
||||||
{(canIRedact || senderId === mx.getUserId()) && (
|
{(canIRedact || senderId === mx.getUserId()) && (
|
||||||
<>
|
<>
|
||||||
<MenuBorder />
|
<MenuBorder />
|
||||||
@@ -499,7 +540,7 @@ const MessageOptions = React.memo(({
|
|||||||
iconSrc={BinIC}
|
iconSrc={BinIC}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm('Are you sure you want to delete this event')) {
|
if (window.confirm('Are you sure you want to delete this event')) {
|
||||||
redactEvent(roomId, eventId);
|
redactEvent(roomId, mEvent.getId());
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -629,7 +670,7 @@ function Message({
|
|||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
}, []);
|
}, []);
|
||||||
const reply = useCallback(() => {
|
const reply = useCallback(() => {
|
||||||
replyTo(senderId, eventId, body);
|
replyTo(senderId, mEvent.getId(), body);
|
||||||
}, [body]);
|
}, [body]);
|
||||||
|
|
||||||
if (body === undefined) return null;
|
if (body === undefined) return null;
|
||||||
|
|||||||
@@ -293,7 +293,7 @@
|
|||||||
@include dir.prop(left, unset, 60px);
|
@include dir.prop(left, unset, 60px);
|
||||||
|
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-100%);
|
||||||
|
|
||||||
border-radius: var(--bo-radius);
|
border-radius: var(--bo-radius);
|
||||||
box-shadow: var(--bs-surface-border);
|
box-shadow: var(--bs-surface-border);
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
@use '../../partials/text';
|
@use '../../partials/text';
|
||||||
@use '../../partials/dir';
|
|
||||||
|
|
||||||
.people-selector {
|
.people-selector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--sp-extra-tight);
|
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-surface-hover);
|
background-color: var(--bg-surface-hover);
|
||||||
|
|||||||
@@ -51,14 +51,16 @@ PWContentSelector.propTypes = {
|
|||||||
function PopupWindow({
|
function PopupWindow({
|
||||||
className, isOpen, title, contentTitle,
|
className, isOpen, title, contentTitle,
|
||||||
drawer, drawerOptions, contentOptions,
|
drawer, drawerOptions, contentOptions,
|
||||||
onRequestClose, children,
|
onAfterClose, onRequestClose, children,
|
||||||
}) {
|
}) {
|
||||||
const haveDrawer = drawer !== null;
|
const haveDrawer = drawer !== null;
|
||||||
|
const cTitle = contentTitle !== null ? contentTitle : title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RawModal
|
<RawModal
|
||||||
className={`${className === null ? '' : `${className} `}pw-model`}
|
className={`${className === null ? '' : `${className} `}pw-model`}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
onAfterClose={onAfterClose}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
size={haveDrawer ? 'large' : 'medium'}
|
size={haveDrawer ? 'large' : 'medium'}
|
||||||
>
|
>
|
||||||
@@ -68,7 +70,11 @@ function PopupWindow({
|
|||||||
<Header>
|
<Header>
|
||||||
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
|
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="s1" weight="medium" primary>{twemojify(title)}</Text>
|
{
|
||||||
|
typeof title === 'string'
|
||||||
|
? <Text variant="s1" weight="medium" primary>{twemojify(title)}</Text>
|
||||||
|
: title
|
||||||
|
}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{drawerOptions}
|
{drawerOptions}
|
||||||
</Header>
|
</Header>
|
||||||
@@ -84,7 +90,11 @@ function PopupWindow({
|
|||||||
<div className="pw__content">
|
<div className="pw__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="h2" weight="medium" primary>{twemojify(contentTitle !== null ? contentTitle : title)}</Text>
|
{
|
||||||
|
typeof cTitle === 'string'
|
||||||
|
? <Text variant="h2" weight="medium" primary>{twemojify(cTitle)}</Text>
|
||||||
|
: cTitle
|
||||||
|
}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
@@ -107,17 +117,19 @@ PopupWindow.defaultProps = {
|
|||||||
contentTitle: null,
|
contentTitle: null,
|
||||||
drawerOptions: null,
|
drawerOptions: null,
|
||||||
contentOptions: null,
|
contentOptions: null,
|
||||||
|
onAfterClose: null,
|
||||||
onRequestClose: null,
|
onRequestClose: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
PopupWindow.propTypes = {
|
PopupWindow.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.node.isRequired,
|
||||||
contentTitle: PropTypes.string,
|
contentTitle: PropTypes.node,
|
||||||
drawer: PropTypes.node,
|
drawer: PropTypes.node,
|
||||||
drawerOptions: PropTypes.node,
|
drawerOptions: PropTypes.node,
|
||||||
contentOptions: PropTypes.node,
|
contentOptions: PropTypes.node,
|
||||||
|
onAfterClose: PropTypes.func,
|
||||||
onRequestClose: PropTypes.func,
|
onRequestClose: PropTypes.func,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ function RoomAliases({ roomId }) {
|
|||||||
<div className="room-aliases">
|
<div className="room-aliases">
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Publish to room directory"
|
title="Publish to room directory"
|
||||||
content={<Text variant="b3">{`Publish this room to the ${hsString}'s public room directory?`}</Text>}
|
content={<Text variant="b3">{`Publish this ${room.isSpaceRoom() ? 'space' : 'room'} to the ${hsString}'s public room directory?`}</Text>}
|
||||||
options={(
|
options={(
|
||||||
<Toggle
|
<Toggle
|
||||||
isActive={isPublic}
|
isActive={isPublic}
|
||||||
@@ -308,14 +308,18 @@ function RoomAliases({ roomId }) {
|
|||||||
{(aliases.published.length === 0) && <Text className="room-aliases__message">No published addresses</Text>}
|
{(aliases.published.length === 0) && <Text className="room-aliases__message">No published addresses</Text>}
|
||||||
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">No Main address (select one from below)</Text>}
|
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">No Main address (select one from below)</Text>}
|
||||||
{aliases.published.map(renderAlias)}
|
{aliases.published.map(renderAlias)}
|
||||||
<Text className="room-aliases__message" variant="b3">Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.</Text>
|
<Text className="room-aliases__message" variant="b3">
|
||||||
|
{`Published addresses can be used by anyone on any server to join your ${room.isSpaceRoom() ? 'space' : 'room'}. To publish an address, it needs to be set as a local address first.`}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{ isLocalVisible && (
|
{ isLocalVisible && (
|
||||||
<div className="room-aliases__content">
|
<div className="room-aliases__content">
|
||||||
<MenuHeader>Local addresses</MenuHeader>
|
<MenuHeader>Local addresses</MenuHeader>
|
||||||
{(aliases.local.length === 0) && <Text className="room-aliases__message">No local addresses</Text>}
|
{(aliases.local.length === 0) && <Text className="room-aliases__message">No local addresses</Text>}
|
||||||
{aliases.local.map(renderAlias)}
|
{aliases.local.map(renderAlias)}
|
||||||
<Text className="room-aliases__message" variant="b3">Set local addresses for this room so users can find this room through your homeserver.</Text>
|
<Text className="room-aliases__message" variant="b3">
|
||||||
|
{`Set local addresses for this ${room.isSpaceRoom() ? 'space' : 'room'} so users can find this ${room.isSpaceRoom() ? 'space' : 'room'} through your homeserver.`}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Text className="room-aliases__form-label" variant="b2">Add local address</Text>
|
<Text className="room-aliases__form-label" variant="b2">Add local address</Text>
|
||||||
<form className="room-aliases__form" onSubmit={handleAliasSubmit}>
|
<form className="room-aliases__form" onSubmit={handleAliasSubmit}>
|
||||||
@@ -324,7 +328,7 @@ function RoomAliases({ roomId }) {
|
|||||||
name="alias-input"
|
name="alias-input"
|
||||||
state={inputState}
|
state={inputState}
|
||||||
onChange={handleAliasChange}
|
onChange={handleAliasChange}
|
||||||
placeholder="my_room_address"
|
placeholder={`my_${room.isSpaceRoom() ? 'space' : 'room'}_address`}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
204
src/app/molecules/room-members/RoomMembers.jsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import React, {
|
||||||
|
useState, useEffect, useCallback,
|
||||||
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './RoomMembers.scss';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
|
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
||||||
|
import AsyncSearch from '../../../util/AsyncSearch';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
|
import Input from '../../atoms/input/Input';
|
||||||
|
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
|
||||||
|
import PeopleSelector from '../people-selector/PeopleSelector';
|
||||||
|
|
||||||
|
const PER_PAGE_MEMBER = 50;
|
||||||
|
|
||||||
|
function AtoZ(m1, m2) {
|
||||||
|
const aName = m1.name;
|
||||||
|
const bName = m2.name;
|
||||||
|
|
||||||
|
if (aName.toLowerCase() < bName.toLowerCase()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (aName.toLowerCase() > bName.toLowerCase()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
function sortByPowerLevel(m1, m2) {
|
||||||
|
const pl1 = m1.powerLevel;
|
||||||
|
const pl2 = m2.powerLevel;
|
||||||
|
|
||||||
|
if (pl1 > pl2) return -1;
|
||||||
|
if (pl1 < pl2) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
function normalizeMembers(members) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
return members.map((member) => ({
|
||||||
|
userId: member.userId,
|
||||||
|
name: getUsernameOfRoomMember(member),
|
||||||
|
username: member.userId.slice(1, member.userId.indexOf(':')),
|
||||||
|
avatarSrc: member.getAvatarUrl(mx.baseUrl, 24, 24, 'crop'),
|
||||||
|
peopleRole: getPowerLabel(member.powerLevel),
|
||||||
|
powerLevel: members.powerLevel,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMemberOfMembership(roomId, membership) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
const [members, setMembers] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
let isLoadingMembers = false;
|
||||||
|
|
||||||
|
const updateMemberList = (event) => {
|
||||||
|
if (isLoadingMembers) return;
|
||||||
|
if (event && event?.getRoomId() !== roomId) return;
|
||||||
|
const memberOfMembership = normalizeMembers(
|
||||||
|
room.getMembersWithMembership(membership)
|
||||||
|
.sort(AtoZ).sort(sortByPowerLevel),
|
||||||
|
);
|
||||||
|
setMembers(memberOfMembership);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMemberList();
|
||||||
|
isLoadingMembers = true;
|
||||||
|
room.loadMembersIfNeeded().then(() => {
|
||||||
|
isLoadingMembers = false;
|
||||||
|
if (!isMounted) return;
|
||||||
|
updateMemberList();
|
||||||
|
});
|
||||||
|
|
||||||
|
mx.on('RoomMember.membership', updateMemberList);
|
||||||
|
mx.on('RoomMember.powerLevel', updateMemberList);
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
mx.removeListener('RoomMember.membership', updateMemberList);
|
||||||
|
mx.removeListener('RoomMember.powerLevel', updateMemberList);
|
||||||
|
};
|
||||||
|
}, [membership]);
|
||||||
|
|
||||||
|
return [members];
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSearchMembers(members) {
|
||||||
|
const [searchMembers, setSearchMembers] = useState(null);
|
||||||
|
const [asyncSearch] = useState(new AsyncSearch());
|
||||||
|
|
||||||
|
const reSearch = useCallback(() => {
|
||||||
|
if (searchMembers) {
|
||||||
|
asyncSearch.search(searchMembers.term);
|
||||||
|
}
|
||||||
|
}, [searchMembers, asyncSearch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
asyncSearch.setup(members, {
|
||||||
|
keys: ['name', 'username', 'userId'],
|
||||||
|
limit: PER_PAGE_MEMBER,
|
||||||
|
});
|
||||||
|
reSearch();
|
||||||
|
}, [members, asyncSearch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSearchData = (data, term) => setSearchMembers({ data, term });
|
||||||
|
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
|
||||||
|
return () => {
|
||||||
|
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
|
||||||
|
};
|
||||||
|
}, [asyncSearch]);
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
const term = e.target.value;
|
||||||
|
if (term === '' || term === undefined) {
|
||||||
|
setSearchMembers(null);
|
||||||
|
} else asyncSearch.search(term);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [searchMembers, handleSearch];
|
||||||
|
}
|
||||||
|
|
||||||
|
function RoomMembers({ roomId }) {
|
||||||
|
const [itemCount, setItemCount] = useState(PER_PAGE_MEMBER);
|
||||||
|
const [membership, setMembership] = useState('join');
|
||||||
|
const [members] = useMemberOfMembership(roomId, membership);
|
||||||
|
const [searchMembers, handleSearch] = useSearchMembers(members);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItemCount(PER_PAGE_MEMBER);
|
||||||
|
}, [searchMembers]);
|
||||||
|
|
||||||
|
const loadMorePeople = () => {
|
||||||
|
setItemCount(itemCount + PER_PAGE_MEMBER);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mList = searchMembers ? searchMembers.data : members.slice(0, itemCount);
|
||||||
|
return (
|
||||||
|
<div className="room-members">
|
||||||
|
<MenuHeader>Search member</MenuHeader>
|
||||||
|
<Input
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder="Search for name"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="room-members__header">
|
||||||
|
<MenuHeader>{`${searchMembers ? `Found — ${mList.length}` : members.length} members`}</MenuHeader>
|
||||||
|
<SegmentedControls
|
||||||
|
selected={
|
||||||
|
(() => {
|
||||||
|
const getSegmentIndex = { join: 0, invite: 1, ban: 2 };
|
||||||
|
return getSegmentIndex[membership];
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
|
||||||
|
onSelect={(index) => {
|
||||||
|
const memberships = ['join', 'invite', 'ban'];
|
||||||
|
setMembership(memberships[index]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="room-members__list">
|
||||||
|
{mList.map((member) => (
|
||||||
|
<PeopleSelector
|
||||||
|
key={member.userId}
|
||||||
|
onClick={() => openProfileViewer(member.userId, roomId)}
|
||||||
|
avatarSrc={member.avatarSrc}
|
||||||
|
name={member.name}
|
||||||
|
color={colorMXID(member.userId)}
|
||||||
|
peopleRole={member.peopleRole}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{
|
||||||
|
(searchMembers?.data.length === 0 || members.length === 0)
|
||||||
|
&& (
|
||||||
|
<div className="room-members__status">
|
||||||
|
<Text variant="b2">
|
||||||
|
{searchMembers ? `No results found for "${searchMembers.term}"` : 'No members to display'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
mList.length !== 0
|
||||||
|
&& members.length > itemCount
|
||||||
|
&& searchMembers === null
|
||||||
|
&& <Button onClick={loadMorePeople}>View more</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomMembers.propTypes = {
|
||||||
|
roomId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomMembers;
|
||||||
39
src/app/molecules/room-members/RoomMembers.scss
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@use '../../partials/flex';
|
||||||
|
@use '../../partials/dir';
|
||||||
|
|
||||||
|
.room-members {
|
||||||
|
& .input-container {
|
||||||
|
margin: var(--sp-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
& .context-menu__header {
|
||||||
|
@extend .cp-fx__item-one;
|
||||||
|
margin-top: 14px;
|
||||||
|
border-top: 1px solid var(--bg-surface-border);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
& .segmented-controls {
|
||||||
|
@include dir.side(margin, 0, var(--sp-normal));
|
||||||
|
& > button {
|
||||||
|
padding: var(--sp-ultra-tight) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__list {
|
||||||
|
|
||||||
|
& .people-selector__container:last-child {
|
||||||
|
margin-bottom: var(--sp-extra-tight);
|
||||||
|
}
|
||||||
|
& > .btn-surface {
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
margin: var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
margin: var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,28 +32,9 @@ const items = [{
|
|||||||
type: cons.notifs.MUTE,
|
type: cons.notifs.MUTE,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
function getNotifType(roomId) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const pushRule = mx.getRoomPushRule('global', roomId);
|
|
||||||
|
|
||||||
if (typeof pushRule === 'undefined') {
|
|
||||||
const overridePushRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
|
|
||||||
if (typeof overridePushRules === 'undefined') return 0;
|
|
||||||
|
|
||||||
const isMuteOverride = overridePushRules.find((rule) => (
|
|
||||||
rule.rule_id === roomId
|
|
||||||
&& rule.actions[0] === 'dont_notify'
|
|
||||||
&& rule.conditions[0].kind === 'event_match'
|
|
||||||
));
|
|
||||||
|
|
||||||
return isMuteOverride ? cons.notifs.MUTE : cons.notifs.DEFAULT;
|
|
||||||
}
|
|
||||||
if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES;
|
|
||||||
return cons.notifs.MENTIONS_AND_KEYWORDS;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRoomNotifType(roomId, newType) {
|
function setRoomNotifType(roomId, newType) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
const { notifications } = initMatrix;
|
||||||
const roomPushRule = mx.getRoomPushRule('global', roomId);
|
const roomPushRule = mx.getRoomPushRule('global', roomId);
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
@@ -76,7 +57,7 @@ function setRoomNotifType(roomId, newType) {
|
|||||||
return promises;
|
return promises;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldState = getNotifType(roomId);
|
const oldState = notifications.getNotiType(roomId);
|
||||||
if (oldState === cons.notifs.MUTE) {
|
if (oldState === cons.notifs.MUTE) {
|
||||||
promises.push(mx.deletePushRule('global', 'override', roomId));
|
promises.push(mx.deletePushRule('global', 'override', roomId));
|
||||||
}
|
}
|
||||||
@@ -115,8 +96,9 @@ function setRoomNotifType(roomId, newType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useNotifications(roomId) {
|
function useNotifications(roomId) {
|
||||||
const [activeType, setActiveType] = useState(getNotifType(roomId));
|
const { notifications } = initMatrix;
|
||||||
useEffect(() => setActiveType(getNotifType(roomId)), [roomId]);
|
const [activeType, setActiveType] = useState(notifications.getNotiType(roomId));
|
||||||
|
useEffect(() => setActiveType(notifications.getNotiType(roomId)), [roomId]);
|
||||||
|
|
||||||
const setNotification = useCallback((item) => {
|
const setNotification = useCallback((item) => {
|
||||||
if (item.type === activeType.type) return;
|
if (item.type === activeType.type) return;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function RoomOptions({ roomId, afterOptionSelect }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ maxWidth: '256px' }}>
|
||||||
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
||||||
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
|
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -51,7 +51,7 @@ function RoomOptions({ roomId, afterOptionSelect }) {
|
|||||||
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
|
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
|
||||||
<MenuHeader>Notification</MenuHeader>
|
<MenuHeader>Notification</MenuHeader>
|
||||||
<RoomNotification roomId={roomId} />
|
<RoomNotification roomId={roomId} />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,11 +218,12 @@ function RoomPermissions({ roomId }) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const permsGroups = room.isSpaceRoom() ? spacePermsGroups : roomPermsGroups;
|
||||||
return (
|
return (
|
||||||
<div className="room-permissions">
|
<div className="room-permissions">
|
||||||
{
|
{
|
||||||
Object.keys(roomPermsGroups).map((groupKey) => {
|
Object.keys(permsGroups).map((groupKey) => {
|
||||||
const groupedPermKeys = roomPermsGroups[groupKey];
|
const groupedPermKeys = permsGroups[groupKey];
|
||||||
return (
|
return (
|
||||||
<div className="room-permissions__card" key={groupKey}>
|
<div className="room-permissions__card" key={groupKey}>
|
||||||
<MenuHeader>{groupKey}</MenuHeader>
|
<MenuHeader>{groupKey}</MenuHeader>
|
||||||
@@ -232,7 +233,7 @@ function RoomPermissions({ roomId }) {
|
|||||||
|
|
||||||
let powerLevel = 0;
|
let powerLevel = 0;
|
||||||
let permValue = permInfo.parent
|
let permValue = permInfo.parent
|
||||||
? permissions[permInfo.parent][permKey]
|
? permissions[permInfo.parent]?.[permKey]
|
||||||
: permissions[permKey];
|
: permissions[permKey];
|
||||||
|
|
||||||
if (!permValue) permValue = permInfo.default;
|
if (!permValue) permValue = permInfo.default;
|
||||||
|
|||||||
@@ -125,9 +125,9 @@ function RoomProfile({ roomId }) {
|
|||||||
|
|
||||||
const renderEditNameAndTopic = () => (
|
const renderEditNameAndTopic = () => (
|
||||||
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
||||||
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Room name" required />}
|
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" required />}
|
||||||
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
|
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
|
||||||
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change room ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
|
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
|
||||||
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
||||||
{ status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>}
|
{ status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>}
|
||||||
{ status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>}
|
{ status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>}
|
||||||
@@ -148,7 +148,7 @@ function RoomProfile({ roomId }) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
src={PencilIC}
|
src={PencilIC}
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
tooltip="Edit room name and topic"
|
tooltip="Edit"
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => setIsEditing(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ function RoomSearch({ roomId }) {
|
|||||||
placeholder="Search for keywords"
|
placeholder="Search for keywords"
|
||||||
name="room-search-input"
|
name="room-search-input"
|
||||||
disabled={isRoomEncrypted}
|
disabled={isRoomEncrypted}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<Button iconSrc={SearchIC} variant="primary" type="submit">Search</Button>
|
<Button iconSrc={SearchIC} variant="primary" type="submit">Search</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +164,7 @@ function RoomSearch({ roomId }) {
|
|||||||
|
|
||||||
{!isRoomEncrypted && searchData?.results.length === 0 && (
|
{!isRoomEncrypted && searchData?.results.length === 0 && (
|
||||||
<div className="room-search__help">
|
<div className="room-search__help">
|
||||||
<Text>No result found</Text>
|
<Text>No results found</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isRoomEncrypted && (
|
{isRoomEncrypted && (
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
|||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
function RoomSelectorWrapper({
|
function RoomSelectorWrapper({
|
||||||
isSelected, isUnread, onClick,
|
isSelected, isMuted, isUnread, onClick,
|
||||||
content, options, onContextMenu,
|
content, options, onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
let myClass = isUnread ? ' room-selector--unread' : '';
|
const classes = ['room-selector'];
|
||||||
myClass += isSelected ? ' room-selector--selected' : '';
|
if (isMuted) classes.push('room-selector--muted');
|
||||||
|
if (isUnread) classes.push('room-selector--unread');
|
||||||
|
if (isSelected) classes.push('room-selector--selected');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`room-selector${myClass}`}>
|
<div className={classes.join(' ')}>
|
||||||
<button
|
<button
|
||||||
className="room-selector__content"
|
className="room-selector__content"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -32,11 +35,13 @@ function RoomSelectorWrapper({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
RoomSelectorWrapper.defaultProps = {
|
RoomSelectorWrapper.defaultProps = {
|
||||||
|
isMuted: false,
|
||||||
options: null,
|
options: null,
|
||||||
onContextMenu: null,
|
onContextMenu: null,
|
||||||
};
|
};
|
||||||
RoomSelectorWrapper.propTypes = {
|
RoomSelectorWrapper.propTypes = {
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
isMuted: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool.isRequired,
|
isUnread: PropTypes.bool.isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
content: PropTypes.node.isRequired,
|
content: PropTypes.node.isRequired,
|
||||||
@@ -46,12 +51,13 @@ RoomSelectorWrapper.propTypes = {
|
|||||||
|
|
||||||
function RoomSelector({
|
function RoomSelector({
|
||||||
name, parentName, roomId, imageSrc, iconSrc,
|
name, parentName, roomId, imageSrc, iconSrc,
|
||||||
isSelected, isUnread, notificationCount, isAlert,
|
isSelected, isMuted, isUnread, notificationCount, isAlert,
|
||||||
options, onClick, onContextMenu,
|
options, onClick, onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RoomSelectorWrapper
|
<RoomSelectorWrapper
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
isMuted={isMuted}
|
||||||
isUnread={isUnread}
|
isUnread={isUnread}
|
||||||
content={(
|
content={(
|
||||||
<>
|
<>
|
||||||
@@ -91,6 +97,7 @@ RoomSelector.defaultProps = {
|
|||||||
isSelected: false,
|
isSelected: false,
|
||||||
imageSrc: null,
|
imageSrc: null,
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
|
isMuted: false,
|
||||||
options: null,
|
options: null,
|
||||||
onContextMenu: null,
|
onContextMenu: null,
|
||||||
};
|
};
|
||||||
@@ -101,6 +108,7 @@ RoomSelector.propTypes = {
|
|||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
iconSrc: PropTypes.string,
|
iconSrc: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
|
isMuted: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool.isRequired,
|
isUnread: PropTypes.bool.isRequired,
|
||||||
notificationCount: PropTypes.oneOfType([
|
notificationCount: PropTypes.oneOfType([
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
border-radius: var(--bo-radius);
|
border-radius: var(--bo-radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&--muted {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
&--unread {
|
&--unread {
|
||||||
.room-selector__content > .text {
|
.room-selector__content > .text {
|
||||||
color: var(--tc-surface-high);
|
color: var(--tc-surface-high);
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ import './SidebarAvatar.scss';
|
|||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Tooltip from '../../atoms/tooltip/Tooltip';
|
import Tooltip from '../../atoms/tooltip/Tooltip';
|
||||||
import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
|
||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
const SidebarAvatar = React.forwardRef(({
|
const SidebarAvatar = React.forwardRef(({
|
||||||
tooltip, text, bgColor, imageSrc,
|
tooltip, active, onClick, onContextMenu,
|
||||||
iconSrc, active, onClick, isUnread, notificationCount, isAlert,
|
avatar, notificationBadge,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
let activeClass = '';
|
let activeClass = '';
|
||||||
if (active) activeClass = ' sidebar-avatar--active';
|
if (active) activeClass = ' sidebar-avatar--active';
|
||||||
@@ -27,50 +25,28 @@ const SidebarAvatar = React.forwardRef(({
|
|||||||
type="button"
|
type="button"
|
||||||
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
|
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
>
|
>
|
||||||
<Avatar
|
{avatar}
|
||||||
text={text}
|
{notificationBadge}
|
||||||
bgColor={bgColor}
|
|
||||||
imageSrc={imageSrc}
|
|
||||||
iconSrc={iconSrc}
|
|
||||||
size="normal"
|
|
||||||
/>
|
|
||||||
{ isUnread && (
|
|
||||||
<NotificationBadge
|
|
||||||
alert={isAlert}
|
|
||||||
content={notificationCount !== 0 ? notificationCount : null}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
SidebarAvatar.defaultProps = {
|
SidebarAvatar.defaultProps = {
|
||||||
text: null,
|
|
||||||
bgColor: 'transparent',
|
|
||||||
iconSrc: null,
|
|
||||||
imageSrc: null,
|
|
||||||
active: false,
|
active: false,
|
||||||
onClick: null,
|
onClick: null,
|
||||||
isUnread: false,
|
onContextMenu: null,
|
||||||
notificationCount: 0,
|
notificationBadge: null,
|
||||||
isAlert: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SidebarAvatar.propTypes = {
|
SidebarAvatar.propTypes = {
|
||||||
tooltip: PropTypes.string.isRequired,
|
tooltip: PropTypes.string.isRequired,
|
||||||
text: PropTypes.string,
|
|
||||||
bgColor: PropTypes.string,
|
|
||||||
imageSrc: PropTypes.string,
|
|
||||||
iconSrc: PropTypes.string,
|
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
isUnread: PropTypes.bool,
|
onContextMenu: PropTypes.func,
|
||||||
notificationCount: PropTypes.oneOfType([
|
avatar: PropTypes.node.isRequired,
|
||||||
PropTypes.string,
|
notificationBadge: PropTypes.node,
|
||||||
PropTypes.number,
|
|
||||||
]),
|
|
||||||
isAlert: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SidebarAvatar;
|
export default SidebarAvatar;
|
||||||
|
|||||||
230
src/app/molecules/space-add-existing/SpaceAddExisting.jsx
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './SpaceAddExisting.scss';
|
||||||
|
|
||||||
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import cons from '../../../client/state/cons';
|
||||||
|
import navigation from '../../../client/state/navigation';
|
||||||
|
import { joinRuleToIconSrc, getIdServer, genRoomVia } from '../../../util/matrixUtil';
|
||||||
|
import { Debounce } from '../../../util/common';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import Checkbox from '../../atoms/button/Checkbox';
|
||||||
|
import Input from '../../atoms/input/Input';
|
||||||
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
|
import RoomSelector from '../room-selector/RoomSelector';
|
||||||
|
import Dialog from '../dialog/Dialog';
|
||||||
|
|
||||||
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
|
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
|
function SpaceAddExistingContent({ roomId }) {
|
||||||
|
const mountStore = useStore(roomId);
|
||||||
|
const [debounce] = useState(new Debounce());
|
||||||
|
const [process, setProcess] = useState(null);
|
||||||
|
const [allRoomIds, setAllRoomIds] = useState([]);
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [searchIds, setSearchIds] = useState(null);
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const {
|
||||||
|
spaces, rooms, directs, roomIdToParents,
|
||||||
|
} = initMatrix.roomList;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
|
||||||
|
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
|
||||||
|
));
|
||||||
|
setAllRoomIds(allIds);
|
||||||
|
}, [roomId]);
|
||||||
|
|
||||||
|
const toggleSelection = (rId) => {
|
||||||
|
if (process !== null) return;
|
||||||
|
const newSelected = [...selected];
|
||||||
|
const selectedIndex = newSelected.indexOf(rId);
|
||||||
|
|
||||||
|
if (selectedIndex > -1) {
|
||||||
|
newSelected.splice(selectedIndex, 1);
|
||||||
|
setSelected(newSelected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newSelected.push(rId);
|
||||||
|
setSelected(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = async () => {
|
||||||
|
setProcess(`Adding ${selected.length} items...`);
|
||||||
|
|
||||||
|
const promises = selected.map((rId) => {
|
||||||
|
const room = mx.getRoom(rId);
|
||||||
|
const via = genRoomVia(room);
|
||||||
|
if (via.length === 0) {
|
||||||
|
via.push(getIdServer(rId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mx.sendStateEvent(roomId, 'm.space.child', {
|
||||||
|
auto_join: false,
|
||||||
|
suggested: false,
|
||||||
|
via,
|
||||||
|
}, rId);
|
||||||
|
});
|
||||||
|
|
||||||
|
mountStore.setItem(true);
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
if (mountStore.getItem() !== true) return;
|
||||||
|
|
||||||
|
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
|
||||||
|
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId) && !selected.includes(rId)
|
||||||
|
));
|
||||||
|
setAllRoomIds(allIds);
|
||||||
|
setProcess(null);
|
||||||
|
setSelected([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (ev) => {
|
||||||
|
const term = ev.target.value.toLocaleLowerCase().replaceAll(' ', '');
|
||||||
|
if (term === '') {
|
||||||
|
setSearchIds(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debounce._(() => {
|
||||||
|
const searchedIds = allRoomIds.filter((rId) => {
|
||||||
|
let name = mx.getRoom(rId)?.name;
|
||||||
|
if (!name) return false;
|
||||||
|
name = name.normalize('NFKC')
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.replaceAll(' ', '');
|
||||||
|
return name.includes(term);
|
||||||
|
});
|
||||||
|
setSearchIds(searchedIds);
|
||||||
|
}, 200)();
|
||||||
|
};
|
||||||
|
const handleSearchClear = (ev) => {
|
||||||
|
const btn = ev.currentTarget;
|
||||||
|
btn.parentElement.searchInput.value = '';
|
||||||
|
setSearchIds(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form onSubmit={(ev) => { ev.preventDefault(); }}>
|
||||||
|
<RawIcon size="small" src={SearchIC} />
|
||||||
|
<Input
|
||||||
|
name="searchInput"
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder="Search room"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<IconButton size="small" type="button" onClick={handleSearchClear} src={CrossIC} />
|
||||||
|
</form>
|
||||||
|
{searchIds?.length === 0 && <Text>No results found</Text>}
|
||||||
|
{
|
||||||
|
(searchIds || allRoomIds).map((rId) => {
|
||||||
|
const room = mx.getRoom(rId);
|
||||||
|
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
|
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
|
|
||||||
|
const parentSet = roomIdToParents.get(rId);
|
||||||
|
const parentNames = parentSet
|
||||||
|
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
|
||||||
|
: undefined;
|
||||||
|
const parents = parentNames ? parentNames.join(', ') : null;
|
||||||
|
|
||||||
|
const handleSelect = () => toggleSelection(rId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RoomSelector
|
||||||
|
key={rId}
|
||||||
|
name={room.name}
|
||||||
|
parentName={parents}
|
||||||
|
roomId={rId}
|
||||||
|
imageSrc={directs.has(rId) ? imageSrc : null}
|
||||||
|
iconSrc={
|
||||||
|
directs.has(rId)
|
||||||
|
? null
|
||||||
|
: joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())
|
||||||
|
}
|
||||||
|
isUnread={false}
|
||||||
|
notificationCount={0}
|
||||||
|
isAlert={false}
|
||||||
|
onClick={handleSelect}
|
||||||
|
options={(
|
||||||
|
<Checkbox
|
||||||
|
isActive={selected.includes(rId)}
|
||||||
|
variant="positive"
|
||||||
|
onToggle={handleSelect}
|
||||||
|
tabIndex={-1}
|
||||||
|
disabled={process !== null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{selected.length !== 0 && (
|
||||||
|
<div className="space-add-existing__footer">
|
||||||
|
{process && <Spinner size="small" />}
|
||||||
|
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
|
||||||
|
{ !process && (
|
||||||
|
<Button onClick={handleAdd} variant="primary">Add</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SpaceAddExistingContent.propTypes = {
|
||||||
|
roomId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function useVisibilityToggle() {
|
||||||
|
const [roomId, setRoomId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOpen = (rId) => setRoomId(rId);
|
||||||
|
navigation.on(cons.events.navigation.SPACE_ADDEXISTING_OPENED, handleOpen);
|
||||||
|
return () => {
|
||||||
|
navigation.removeListener(cons.events.navigation.SPACE_ADDEXISTING_OPENED, handleOpen);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestClose = () => setRoomId(null);
|
||||||
|
|
||||||
|
return [roomId, requestClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
function SpaceAddExisting() {
|
||||||
|
const [roomId, requestClose] = useVisibilityToggle();
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
isOpen={roomId !== null}
|
||||||
|
className="space-add-existing"
|
||||||
|
title={(
|
||||||
|
<Text variant="s1" weight="medium" primary>
|
||||||
|
{roomId && twemojify(room.name)}
|
||||||
|
<span style={{ color: 'var(--tc-surface-low)' }}> — add existing rooms</span>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
|
||||||
|
onRequestClose={requestClose}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
roomId
|
||||||
|
? <SpaceAddExistingContent roomId={roomId} />
|
||||||
|
: <div />
|
||||||
|
}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SpaceAddExisting;
|
||||||
77
src/app/molecules/space-add-existing/SpaceAddExisting.scss
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/flex';
|
||||||
|
|
||||||
|
.space-add-existing {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.dialog__content-container {
|
||||||
|
padding: 0;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
@include dir.side(padding, var(--sp-extra-tight), 0);
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
margin: var(--sp-loose) var(--sp-normal);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& form {
|
||||||
|
@extend .cp-fx__row--s-c;
|
||||||
|
padding: var(--sp-extra-tight);
|
||||||
|
padding-top: var(--sp-normal);
|
||||||
|
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
|
||||||
|
& > .ic-raw,
|
||||||
|
& > .ic-btn {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
& > .ic-raw {
|
||||||
|
margin: 0 var(--sp-tight);
|
||||||
|
}
|
||||||
|
& > .ic-btn {
|
||||||
|
border-radius: calc(var(--bo-radius) / 2);
|
||||||
|
@include dir.prop(right, var(--sp-tight), unset);
|
||||||
|
@include dir.prop(left, unset, var(--sp-tight));
|
||||||
|
}
|
||||||
|
& input {
|
||||||
|
padding: var(--sp-tight) 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
@extend .cp-fx__item-one;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-selector {
|
||||||
|
margin: 0 var(--sp-extra-tight);
|
||||||
|
}
|
||||||
|
.room-selector__options {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-add-existing__footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--sp-normal);
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border-top: 1px solid var(--bg-surface-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
@extend .cp-fx__item-one;
|
||||||
|
padding: 0 var(--sp-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > button {
|
||||||
|
@include dir.side(margin, var(--sp-normal), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/app/molecules/space-options/SpaceOptions.jsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import { openSpaceSettings, openSpaceManage, openInviteUser } from '../../../client/action/navigation';
|
||||||
|
import { leave } from '../../../client/action/room';
|
||||||
|
import {
|
||||||
|
createSpaceShortcut,
|
||||||
|
deleteSpaceShortcut,
|
||||||
|
categorizeSpace,
|
||||||
|
unCategorizeSpace,
|
||||||
|
} from '../../../client/action/accountData';
|
||||||
|
|
||||||
|
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
|
||||||
|
import CategoryIC from '../../../../public/res/ic/outlined/category.svg';
|
||||||
|
import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg';
|
||||||
|
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||||
|
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||||
|
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
|
||||||
|
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||||
|
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
|
||||||
|
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
|
||||||
|
|
||||||
|
function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
const canInvite = room?.canInvite(mx.getUserId());
|
||||||
|
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
|
||||||
|
const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId);
|
||||||
|
|
||||||
|
const handleInviteClick = () => {
|
||||||
|
openInviteUser(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
};
|
||||||
|
const handlePinClick = () => {
|
||||||
|
if (isPinned) deleteSpaceShortcut(roomId);
|
||||||
|
else createSpaceShortcut(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
};
|
||||||
|
const handleCategorizeClick = () => {
|
||||||
|
if (isCategorized) unCategorizeSpace(roomId);
|
||||||
|
else categorizeSpace(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
};
|
||||||
|
const handleSettingsClick = () => {
|
||||||
|
openSpaceSettings(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
};
|
||||||
|
const handleManageRoom = () => {
|
||||||
|
openSpaceManage(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeaveClick = () => {
|
||||||
|
if (confirm('Are you really want to leave this space?')) {
|
||||||
|
leave(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
|
||||||
|
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleCategorizeClick}
|
||||||
|
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
|
||||||
|
>
|
||||||
|
{isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handlePinClick}
|
||||||
|
iconSrc={isPinned ? PinFilledIC : PinIC}
|
||||||
|
>
|
||||||
|
{isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={AddUserIC}
|
||||||
|
onClick={handleInviteClick}
|
||||||
|
disabled={!canInvite}
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>Manage rooms</MenuItem>
|
||||||
|
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>Settings</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
variant="danger"
|
||||||
|
onClick={handleLeaveClick}
|
||||||
|
iconSrc={LeaveArrowIC}
|
||||||
|
>
|
||||||
|
Leave
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpaceOptions.defaultProps = {
|
||||||
|
afterOptionSelect: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
SpaceOptions.propTypes = {
|
||||||
|
roomId: PropTypes.string.isRequired,
|
||||||
|
afterOptionSelect: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpaceOptions;
|
||||||
@@ -2,79 +2,84 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './CreateRoom.scss';
|
import './CreateRoom.scss';
|
||||||
|
|
||||||
|
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 { isRoomAliasAvailable } from '../../../util/matrixUtil';
|
import navigation from '../../../client/state/navigation';
|
||||||
|
import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
import { selectRoom } from '../../../client/action/navigation';
|
import { isRoomAliasAvailable, getIdServer } from '../../../util/matrixUtil';
|
||||||
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
import Toggle from '../../atoms/button/Toggle';
|
import Toggle from '../../atoms/button/Toggle';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
import Input from '../../atoms/input/Input';
|
import Input from '../../atoms/input/Input';
|
||||||
import Spinner from '../../atoms/spinner/Spinner';
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
import SegmentControl from '../../atoms/segmented-controls/SegmentedControls';
|
import SegmentControl from '../../atoms/segmented-controls/SegmentedControls';
|
||||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
import Dialog from '../../molecules/dialog/Dialog';
|
||||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||||||
|
|
||||||
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
|
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
|
||||||
|
import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg';
|
||||||
|
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
||||||
|
import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg';
|
||||||
|
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
|
||||||
|
import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
|
||||||
|
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
|
||||||
|
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
|
||||||
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
function CreateRoom({ isOpen, onRequestClose }) {
|
function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
|
||||||
const [isPublic, togglePublic] = useState(false);
|
const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite');
|
||||||
const [isEncrypted, toggleEncrypted] = useState(true);
|
const [isEncrypted, setIsEncrypted] = useState(true);
|
||||||
const [isValidAddress, updateIsValidAddress] = useState(null);
|
const [isCreatingRoom, setIsCreatingRoom] = useState(false);
|
||||||
const [isCreatingRoom, updateIsCreatingRoom] = useState(false);
|
const [creatingError, setCreatingError] = useState(null);
|
||||||
const [creatingError, updateCreatingError] = useState(null);
|
|
||||||
|
|
||||||
const [titleValue, updateTitleValue] = useState(undefined);
|
const [isValidAddress, setIsValidAddress] = useState(null);
|
||||||
const [topicValue, updateTopicValue] = useState(undefined);
|
const [addressValue, setAddressValue] = useState(undefined);
|
||||||
const [addressValue, updateAddressValue] = useState(undefined);
|
|
||||||
const [roleIndex, setRoleIndex] = useState(0);
|
const [roleIndex, setRoleIndex] = useState(0);
|
||||||
|
|
||||||
const addressRef = useRef(null);
|
const addressRef = useRef(null);
|
||||||
const topicRef = useRef(null);
|
|
||||||
const nameRef = useRef(null);
|
|
||||||
|
|
||||||
const userId = initMatrix.matrixClient.getUserId();
|
const mx = initMatrix.matrixClient;
|
||||||
const hsString = userId.slice(userId.indexOf(':'));
|
const userHs = getIdServer(mx.getUserId());
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
togglePublic(false);
|
|
||||||
toggleEncrypted(true);
|
|
||||||
updateIsValidAddress(null);
|
|
||||||
updateIsCreatingRoom(false);
|
|
||||||
updateCreatingError(null);
|
|
||||||
updateTitleValue(undefined);
|
|
||||||
updateTopicValue(undefined);
|
|
||||||
updateAddressValue(undefined);
|
|
||||||
setRoleIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCreated = (roomId) => {
|
|
||||||
resetForm();
|
|
||||||
selectRoom(roomId);
|
|
||||||
onRequestClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { roomList } = initMatrix;
|
const { roomList } = initMatrix;
|
||||||
|
const onCreated = (roomId) => {
|
||||||
|
setIsCreatingRoom(false);
|
||||||
|
setCreatingError(null);
|
||||||
|
setIsValidAddress(null);
|
||||||
|
setAddressValue(undefined);
|
||||||
|
|
||||||
|
if (!mx.getRoom(roomId)?.isSpaceRoom()) {
|
||||||
|
selectRoom(roomId);
|
||||||
|
}
|
||||||
|
onRequestClose();
|
||||||
|
};
|
||||||
roomList.on(cons.events.roomList.ROOM_CREATED, onCreated);
|
roomList.on(cons.events.roomList.ROOM_CREATED, onCreated);
|
||||||
return () => {
|
return () => {
|
||||||
roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated);
|
roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function createRoom() {
|
const handleSubmit = async (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { target } = evt;
|
||||||
|
|
||||||
if (isCreatingRoom) return;
|
if (isCreatingRoom) return;
|
||||||
updateIsCreatingRoom(true);
|
setIsCreatingRoom(true);
|
||||||
updateCreatingError(null);
|
setCreatingError(null);
|
||||||
const name = nameRef.current.value;
|
|
||||||
let topic = topicRef.current.value;
|
const name = target.name.value;
|
||||||
|
let topic = target.topic.value;
|
||||||
if (topic.trim() === '') topic = undefined;
|
if (topic.trim() === '') topic = undefined;
|
||||||
let roomAlias;
|
let roomAlias;
|
||||||
if (isPublic) {
|
if (joinRule === 'public') {
|
||||||
roomAlias = addressRef?.current?.value;
|
roomAlias = addressRef?.current?.value;
|
||||||
if (roomAlias.trim() === '') roomAlias = undefined;
|
if (roomAlias.trim() === '') roomAlias = undefined;
|
||||||
}
|
}
|
||||||
@@ -82,115 +87,217 @@ function CreateRoom({ isOpen, onRequestClose }) {
|
|||||||
const powerLevel = roleIndex === 1 ? 101 : undefined;
|
const powerLevel = roleIndex === 1 ? 101 : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await roomActions.create({
|
await roomActions.createRoom({
|
||||||
name, topic, isPublic, roomAlias, isEncrypted, powerLevel,
|
name,
|
||||||
|
topic,
|
||||||
|
joinRule,
|
||||||
|
alias: roomAlias,
|
||||||
|
isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted,
|
||||||
|
powerLevel,
|
||||||
|
isSpace,
|
||||||
|
parentId,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === 'M_UNKNOWN: Invalid characters in room alias') {
|
if (e.message === 'M_UNKNOWN: Invalid characters in room alias') {
|
||||||
updateCreatingError('ERROR: Invalid characters in room address');
|
setCreatingError('ERROR: Invalid characters in address');
|
||||||
updateIsValidAddress(false);
|
setIsValidAddress(false);
|
||||||
} else if (e.message === 'M_ROOM_IN_USE: Room alias already taken') {
|
} else if (e.message === 'M_ROOM_IN_USE: Room alias already taken') {
|
||||||
updateCreatingError('ERROR: Room address is already in use');
|
setCreatingError('ERROR: This address is already in use');
|
||||||
updateIsValidAddress(false);
|
setIsValidAddress(false);
|
||||||
} else updateCreatingError(e.message);
|
} else setCreatingError(e.message);
|
||||||
updateIsCreatingRoom(false);
|
setIsCreatingRoom(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function validateAddress(e) {
|
const validateAddress = (e) => {
|
||||||
const myAddress = e.target.value;
|
const myAddress = e.target.value;
|
||||||
updateIsValidAddress(null);
|
setIsValidAddress(null);
|
||||||
updateAddressValue(e.target.value);
|
setAddressValue(e.target.value);
|
||||||
updateCreatingError(null);
|
setCreatingError(null);
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (myAddress !== addressRef.current.value) return;
|
if (myAddress !== addressRef.current.value) return;
|
||||||
const roomAlias = addressRef.current.value;
|
const roomAlias = addressRef.current.value;
|
||||||
if (roomAlias === '') return;
|
if (roomAlias === '') return;
|
||||||
const roomAddress = `#${roomAlias}${hsString}`;
|
const roomAddress = `#${roomAlias}:${userHs}`;
|
||||||
|
|
||||||
if (await isRoomAliasAvailable(roomAddress)) {
|
if (await isRoomAliasAvailable(roomAddress)) {
|
||||||
updateIsValidAddress(true);
|
setIsValidAddress(true);
|
||||||
} else {
|
} else {
|
||||||
updateIsValidAddress(false);
|
setIsValidAddress(false);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
};
|
||||||
function handleTitleChange(e) {
|
|
||||||
if (e.target.value.trim() === '') updateTitleValue(undefined);
|
const joinRules = ['invite', 'restricted', 'public'];
|
||||||
updateTitleValue(e.target.value);
|
const joinRuleShortText = ['Private', 'Restricted', 'Public'];
|
||||||
}
|
const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)'];
|
||||||
function handleTopicChange(e) {
|
const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
|
||||||
if (e.target.value.trim() === '') updateTopicValue(undefined);
|
const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
|
||||||
updateTopicValue(e.target.value);
|
const handleJoinRule = (evt) => {
|
||||||
}
|
openReusableContextMenu(
|
||||||
|
'bottom',
|
||||||
|
getEventCords(evt, '.btn-surface'),
|
||||||
|
(closeMenu) => (
|
||||||
|
<>
|
||||||
|
<MenuHeader>Visibility (who can join)</MenuHeader>
|
||||||
|
{
|
||||||
|
joinRules.map((rule) => (
|
||||||
|
<MenuItem
|
||||||
|
key={rule}
|
||||||
|
variant={rule === joinRule ? 'positive' : 'surface'}
|
||||||
|
iconSrc={
|
||||||
|
isSpace
|
||||||
|
? jrSpaceIC[joinRules.indexOf(rule)]
|
||||||
|
: jrRoomIC[joinRules.indexOf(rule)]
|
||||||
|
}
|
||||||
|
onClick={() => { closeMenu(); setJoinRule(rule); }}
|
||||||
|
disabled={!parentId && rule === 'restricted'}
|
||||||
|
>
|
||||||
|
{ joinRuleText[joinRules.indexOf(rule)] }
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopupWindow
|
<div className="create-room">
|
||||||
isOpen={isOpen}
|
<form className="create-room__form" onSubmit={handleSubmit}>
|
||||||
title="Create room"
|
<SettingTile
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
|
title="Visibility"
|
||||||
onRequestClose={onRequestClose}
|
options={(
|
||||||
>
|
<Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
|
||||||
<div className="create-room">
|
{joinRuleShortText[joinRules.indexOf(joinRule)]}
|
||||||
<form className="create-room__form" onSubmit={(e) => { e.preventDefault(); createRoom(); }}>
|
</Button>
|
||||||
<SettingTile
|
|
||||||
title="Make room public"
|
|
||||||
options={<Toggle isActive={isPublic} onToggle={togglePublic} />}
|
|
||||||
content={<Text variant="b3">Public room can be joined by anyone.</Text>}
|
|
||||||
/>
|
|
||||||
{isPublic && (
|
|
||||||
<div>
|
|
||||||
<Text className="create-room__address__label" variant="b2">Room address</Text>
|
|
||||||
<div className="create-room__address">
|
|
||||||
<Text variant="b1">#</Text>
|
|
||||||
<Input value={addressValue} onChange={validateAddress} state={(isValidAddress === false) ? 'error' : 'normal'} forwardRef={addressRef} placeholder="my_room" required />
|
|
||||||
<Text variant="b1">{hsString}</Text>
|
|
||||||
</div>
|
|
||||||
{isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{`#${addressValue}${hsString} is already in use`}</span></Text>}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{!isPublic && (
|
content={<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>}
|
||||||
<SettingTile
|
/>
|
||||||
title="Enable end-to-end encryption"
|
{joinRule === 'public' && (
|
||||||
options={<Toggle isActive={isEncrypted} onToggle={toggleEncrypted} />}
|
<div>
|
||||||
content={<Text variant="b3">You can’t disable this later. Bridges & most bots won’t work yet.</Text>}
|
<Text className="create-room__address__label" variant="b2">{isSpace ? 'Space address' : 'Room address'}</Text>
|
||||||
|
<div className="create-room__address">
|
||||||
|
<Text variant="b1">#</Text>
|
||||||
|
<Input
|
||||||
|
value={addressValue}
|
||||||
|
onChange={validateAddress}
|
||||||
|
state={(isValidAddress === false) ? 'error' : 'normal'}
|
||||||
|
forwardRef={addressRef}
|
||||||
|
placeholder="my_address"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Text variant="b1">{`:${userHs}`}</Text>
|
||||||
|
</div>
|
||||||
|
{isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{`#${addressValue}:${userHs} is already in use`}</span></Text>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isSpace && joinRule !== 'public' && (
|
||||||
|
<SettingTile
|
||||||
|
title="Enable end-to-end encryption"
|
||||||
|
options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
|
||||||
|
content={<Text variant="b3">You can’t disable this later. Bridges & most bots won’t work yet.</Text>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SettingTile
|
||||||
|
title="Select your role"
|
||||||
|
options={(
|
||||||
|
<SegmentControl
|
||||||
|
selected={roleIndex}
|
||||||
|
segments={[{ text: 'Admin' }, { text: 'Founder' }]}
|
||||||
|
onSelect={setRoleIndex}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SettingTile
|
content={(
|
||||||
title="Select your role"
|
<Text variant="b3">Override the default (100) power level.</Text>
|
||||||
options={(
|
|
||||||
<SegmentControl
|
|
||||||
selected={roleIndex}
|
|
||||||
segments={[{ text: 'Admin' }, { text: 'Founder' }]}
|
|
||||||
onSelect={setRoleIndex}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
content={(
|
|
||||||
<Text variant="b3">Override the default (100) power level.</Text>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Input value={topicValue} onChange={handleTopicChange} forwardRef={topicRef} minHeight={174} resizable label="Topic (optional)" />
|
|
||||||
<div className="create-room__name-wrapper">
|
|
||||||
<Input value={titleValue} onChange={handleTitleChange} forwardRef={nameRef} label="Room name" required />
|
|
||||||
<Button disabled={isValidAddress === false || isCreatingRoom} iconSrc={HashPlusIC} type="submit" variant="primary">Create</Button>
|
|
||||||
</div>
|
|
||||||
{isCreatingRoom && (
|
|
||||||
<div className="create-room__loading">
|
|
||||||
<Spinner size="small" />
|
|
||||||
<Text>Creating room...</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
|
/>
|
||||||
</form>
|
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
|
||||||
</div>
|
<div className="create-room__name-wrapper">
|
||||||
</PopupWindow>
|
<Input name="name" label={`${isSpace ? 'Space' : 'Room'} name`} required />
|
||||||
|
<Button
|
||||||
|
disabled={isValidAddress === false || isCreatingRoom}
|
||||||
|
iconSrc={isSpace ? SpacePlusIC : HashPlusIC}
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isCreatingRoom && (
|
||||||
|
<div className="create-room__loading">
|
||||||
|
<Spinner size="small" />
|
||||||
|
<Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
CreateRoomContent.defaultProps = {
|
||||||
CreateRoom.propTypes = {
|
parentId: null,
|
||||||
isOpen: PropTypes.bool.isRequired,
|
};
|
||||||
|
CreateRoomContent.propTypes = {
|
||||||
|
isSpace: PropTypes.bool.isRequired,
|
||||||
|
parentId: PropTypes.string,
|
||||||
onRequestClose: PropTypes.func.isRequired,
|
onRequestClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function useWindowToggle() {
|
||||||
|
const [create, setCreate] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOpen = (isSpace, parentId) => {
|
||||||
|
setCreate({
|
||||||
|
isSpace,
|
||||||
|
parentId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
navigation.on(cons.events.navigation.CREATE_ROOM_OPENED, handleOpen);
|
||||||
|
return () => {
|
||||||
|
navigation.removeListener(cons.events.navigation.CREATE_ROOM_OPENED, handleOpen);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onRequestClose = () => setCreate(null);
|
||||||
|
|
||||||
|
return [create, onRequestClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateRoom() {
|
||||||
|
const [create, onRequestClose] = useWindowToggle();
|
||||||
|
const { isSpace, parentId } = create ?? {};
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(parentId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
isOpen={create !== null}
|
||||||
|
title={(
|
||||||
|
<Text variant="s1" weight="medium" primary>
|
||||||
|
{parentId ? twemojify(room.name) : 'Home'}
|
||||||
|
<span style={{ color: 'var(--tc-surface-low)' }}>
|
||||||
|
{` — create ${isSpace ? 'space' : 'room'}`}
|
||||||
|
</span>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
create
|
||||||
|
? (
|
||||||
|
<CreateRoomContent
|
||||||
|
isSpace={isSpace}
|
||||||
|
parentId={parentId}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
/>
|
||||||
|
) : <div />
|
||||||
|
}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default CreateRoom;
|
export default CreateRoom;
|
||||||
|
|||||||
24
src/app/organisms/drag-drop/DragDrop.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './DragDrop.scss';
|
||||||
|
|
||||||
|
import RawModal from '../../atoms/modal/RawModal';
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
|
||||||
|
function DragDrop({ isOpen }) {
|
||||||
|
return (
|
||||||
|
<RawModal
|
||||||
|
className="drag-drop__model"
|
||||||
|
overlayClassName="drag-drop__overlay"
|
||||||
|
isOpen={isOpen}
|
||||||
|
>
|
||||||
|
<Text variant="h2" weight="medium">Drop file to upload</Text>
|
||||||
|
</RawModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DragDrop.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DragDrop;
|
||||||
12
src/app/organisms/drag-drop/DragDrop.scss
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.drag-drop__model {
|
||||||
|
box-shadow: none;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop__overlay {
|
||||||
|
background-color: var(--bg-overlay-low);
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="emoji-group">
|
<div className="emoji-group">
|
||||||
<Text className="emoji-group__header" variant="b2" weight="bold">{name}</Text>
|
<Text className="emoji-group__header" variant="b2" weight="bold">{name}</Text>
|
||||||
{groupEmojis.length !== 0 && <div className="emoji-set">{getEmojiBoard()}</div>}
|
{groupEmojis.length !== 0 && <div className="emoji-set noselect">{getEmojiBoard()}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -128,8 +128,7 @@ function SearchedEmoji() {
|
|||||||
return <EmojiGroup key="-1" name={searchedEmojis.emojis.length === 0 ? 'No search result found' : 'Search results'} groupEmojis={searchedEmojis.emojis} />;
|
return <EmojiGroup key="-1" name={searchedEmojis.emojis.length === 0 ? 'No search result found' : 'Search results'} groupEmojis={searchedEmojis.emojis} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiBoard({ onSelect }) {
|
function EmojiBoard({ onSelect, searchRef }) {
|
||||||
const searchRef = useRef(null);
|
|
||||||
const scrollEmojisRef = useRef(null);
|
const scrollEmojisRef = useRef(null);
|
||||||
const emojiInfo = useRef(null);
|
const emojiInfo = useRef(null);
|
||||||
|
|
||||||
@@ -182,8 +181,8 @@ function EmojiBoard({ onSelect }) {
|
|||||||
setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
|
setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchChange(e) {
|
function handleSearchChange() {
|
||||||
const term = e.target.value;
|
const term = searchRef.current.value;
|
||||||
asyncSearch.search(term);
|
asyncSearch.search(term);
|
||||||
scrollEmojisRef.current.scrollTop = 0;
|
scrollEmojisRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
@@ -213,9 +212,16 @@ function EmojiBoard({ onSelect }) {
|
|||||||
setAvailableEmojis(packs);
|
setAvailableEmojis(packs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onOpen = () => {
|
||||||
|
searchRef.current.value = '';
|
||||||
|
handleSearchChange();
|
||||||
|
};
|
||||||
|
|
||||||
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
|
||||||
|
navigation.on(cons.events.navigation.EMOJIBOARD_OPENED, onOpen);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
|
||||||
|
navigation.removeListener(cons.events.navigation.EMOJIBOARD_OPENED, onOpen);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -312,6 +318,7 @@ function EmojiBoard({ onSelect }) {
|
|||||||
|
|
||||||
EmojiBoard.propTypes = {
|
EmojiBoard.propTypes = {
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
searchRef: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EmojiBoard;
|
export default EmojiBoard;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
|
|||||||
|
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
|
import settings from '../../../client/state/settings';
|
||||||
|
|
||||||
import ContextMenu from '../../atoms/context-menu/ContextMenu';
|
import ContextMenu from '../../atoms/context-menu/ContextMenu';
|
||||||
import EmojiBoard from './EmojiBoard';
|
import EmojiBoard from './EmojiBoard';
|
||||||
@@ -10,6 +11,7 @@ let requestCallback = null;
|
|||||||
let isEmojiBoardVisible = false;
|
let isEmojiBoardVisible = false;
|
||||||
function EmojiBoardOpener() {
|
function EmojiBoardOpener() {
|
||||||
const openerRef = useRef(null);
|
const openerRef = useRef(null);
|
||||||
|
const searchRef = useRef(null);
|
||||||
|
|
||||||
function openEmojiBoard(cords, requestEmojiCallback) {
|
function openEmojiBoard(cords, requestEmojiCallback) {
|
||||||
if (requestCallback !== null || isEmojiBoardVisible) {
|
if (requestCallback !== null || isEmojiBoardVisible) {
|
||||||
@@ -25,7 +27,9 @@ function EmojiBoardOpener() {
|
|||||||
|
|
||||||
function afterEmojiBoardToggle(isVisible) {
|
function afterEmojiBoardToggle(isVisible) {
|
||||||
isEmojiBoardVisible = isVisible;
|
isEmojiBoardVisible = isVisible;
|
||||||
if (!isVisible) {
|
if (isVisible) {
|
||||||
|
if (!settings.isTouchScreenDevice) searchRef.current.focus();
|
||||||
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!isEmojiBoardVisible) requestCallback = null;
|
if (!isEmojiBoardVisible) requestCallback = null;
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -46,7 +50,7 @@ function EmojiBoardOpener() {
|
|||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
content={(
|
content={(
|
||||||
<EmojiBoard onSelect={addEmoji} />
|
<EmojiBoard onSelect={addEmoji} searchRef={searchRef} />
|
||||||
)}
|
)}
|
||||||
afterToggle={afterEmojiBoardToggle}
|
afterToggle={afterEmojiBoardToggle}
|
||||||
render={(toggleMenu) => (
|
render={(toggleMenu) => (
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function addToGroup(emoji) {
|
|||||||
const emojis = [];
|
const emojis = [];
|
||||||
emojisData.forEach((emoji) => {
|
emojisData.forEach((emoji) => {
|
||||||
const myShortCodes = shortcodes[emoji.hexcode];
|
const myShortCodes = shortcodes[emoji.hexcode];
|
||||||
|
if (!myShortCodes) return;
|
||||||
const em = {
|
const em = {
|
||||||
...emoji,
|
...emoji,
|
||||||
shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes,
|
shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import initMatrix from '../../../client/initMatrix';
|
|||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
import { selectRoom } from '../../../client/action/navigation';
|
import { selectRoom } from '../../../client/action/navigation';
|
||||||
|
import { hasDMWith } from '../../../util/matrixUtil';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
@@ -104,17 +105,19 @@ function InviteUser({
|
|||||||
|
|
||||||
async function createDM(userId) {
|
async function createDM(userId) {
|
||||||
if (mx.getUserId() === userId) return;
|
if (mx.getUserId() === userId) return;
|
||||||
|
const dmRoomId = hasDMWith(userId);
|
||||||
|
if (dmRoomId) {
|
||||||
|
selectRoom(dmRoomId);
|
||||||
|
onRequestClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addUserToProc(userId);
|
addUserToProc(userId);
|
||||||
procUserError.delete(userId);
|
procUserError.delete(userId);
|
||||||
updateUserProcError(getMapCopy(procUserError));
|
updateUserProcError(getMapCopy(procUserError));
|
||||||
|
|
||||||
const result = await roomActions.create({
|
const result = await roomActions.createDM(userId);
|
||||||
isPublic: false,
|
|
||||||
isEncrypted: true,
|
|
||||||
isDirect: true,
|
|
||||||
invite: [userId],
|
|
||||||
});
|
|
||||||
roomIdToUserId.set(result.room_id, userId);
|
roomIdToUserId.set(result.room_id, userId);
|
||||||
updateRoomIdToUserId(getMapCopy(roomIdToUserId));
|
updateRoomIdToUserId(getMapCopy(roomIdToUserId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { selectRoom } from '../../../client/action/navigation';
|
|
||||||
import Postie from '../../../util/Postie';
|
import Postie from '../../../util/Postie';
|
||||||
|
|
||||||
import Selector from './Selector';
|
import RoomsCategory from './RoomsCategory';
|
||||||
|
|
||||||
import { AtoZ } from './common';
|
import { AtoZ } from './common';
|
||||||
|
|
||||||
@@ -15,55 +14,34 @@ function Directs() {
|
|||||||
const { roomList, notifications } = initMatrix;
|
const { roomList, notifications } = initMatrix;
|
||||||
const directIds = [...roomList.directs].sort(AtoZ);
|
const directIds = [...roomList.directs].sort(AtoZ);
|
||||||
|
|
||||||
const [, forceUpdate] = useState({});
|
|
||||||
|
|
||||||
function selectorChanged(selectedRoomId, prevSelectedRoomId) {
|
|
||||||
if (!drawerPostie.hasTopic('selector-change')) return;
|
|
||||||
const addresses = [];
|
|
||||||
if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId);
|
|
||||||
if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId);
|
|
||||||
if (addresses.length === 0) return;
|
|
||||||
drawerPostie.post('selector-change', addresses, selectedRoomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notiChanged(roomId, total, prevTotal) {
|
|
||||||
if (total === prevTotal) return;
|
|
||||||
if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
|
|
||||||
drawerPostie.post('unread-change', roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function roomListUpdated() {
|
|
||||||
const { spaces, rooms, directs } = initMatrix.roomList;
|
|
||||||
if (!(
|
|
||||||
spaces.has(navigation.selectedRoomId)
|
|
||||||
|| rooms.has(navigation.selectedRoomId)
|
|
||||||
|| directs.has(navigation.selectedRoomId))
|
|
||||||
) {
|
|
||||||
selectRoom(null);
|
|
||||||
}
|
|
||||||
forceUpdate({});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
|
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
|
||||||
|
if (!drawerPostie.hasTopic('selector-change')) return;
|
||||||
|
const addresses = [];
|
||||||
|
if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId);
|
||||||
|
if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId);
|
||||||
|
if (addresses.length === 0) return;
|
||||||
|
drawerPostie.post('selector-change', addresses, selectedRoomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notiChanged = (roomId, total, prevTotal) => {
|
||||||
|
if (total === prevTotal) return;
|
||||||
|
if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
|
||||||
|
drawerPostie.post('unread-change', roomId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
return () => {
|
return () => {
|
||||||
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
|
|
||||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return directIds.map((id) => (
|
return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
|
||||||
<Selector
|
|
||||||
key={id}
|
|
||||||
roomId={id}
|
|
||||||
drawerPostie={drawerPostie}
|
|
||||||
onClick={() => selectRoom(id)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Directs;
|
export default Directs;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import './Drawer.scss';
|
|||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { selectTab, selectSpace } from '../../../client/action/navigation';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
@@ -14,6 +13,7 @@ import DrawerBreadcrumb from './DrawerBreadcrumb';
|
|||||||
import Home from './Home';
|
import Home from './Home';
|
||||||
import Directs from './Directs';
|
import Directs from './Directs';
|
||||||
|
|
||||||
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
import { useSelectedTab } from '../../hooks/useSelectedTab';
|
import { useSelectedTab } from '../../hooks/useSelectedTab';
|
||||||
import { useSelectedSpace } from '../../hooks/useSelectedSpace';
|
import { useSelectedSpace } from '../../hooks/useSelectedSpace';
|
||||||
|
|
||||||
@@ -40,8 +40,17 @@ function Drawer() {
|
|||||||
const [systemState] = useSystemState();
|
const [systemState] = useSystemState();
|
||||||
const [selectedTab] = useSelectedTab();
|
const [selectedTab] = useSelectedTab();
|
||||||
const [spaceId] = useSelectedSpace();
|
const [spaceId] = useSelectedSpace();
|
||||||
|
const [, forceUpdate] = useForceUpdate();
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { roomList } = initMatrix;
|
||||||
|
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate);
|
||||||
|
return () => {
|
||||||
|
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
scrollRef.current.scrollTop = 0;
|
scrollRef.current.scrollTop = 0;
|
||||||
@@ -52,7 +61,7 @@ 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">
|
||||||
{selectedTab !== cons.tabs.DIRECTS && <DrawerBreadcrumb spaceId={spaceId} />}
|
{navigation.selectedSpacePath.length > 1 && <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">
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
1px solid var(--bg-surface-border),
|
1px solid var(--bg-surface-border),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
& .header {
|
||||||
|
padding: var(--sp-extra-tight);
|
||||||
|
& > .header__title-wrapper {
|
||||||
|
@include dir.side(margin, var(--sp-ultra-tight), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__content-wrapper {
|
&__content-wrapper {
|
||||||
@extend .cp-fx__item-one;
|
@extend .cp-fx__item-one;
|
||||||
@extend .cp-fx__column;
|
@extend .cp-fx__column;
|
||||||
@@ -46,19 +53,4 @@
|
|||||||
var(--bg-surface-low),
|
var(--bg-surface-low),
|
||||||
var(--bg-surface-low-transparent));
|
var(--bg-surface-low-transparent));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
& > .room-selector {
|
|
||||||
width: calc(100% - var(--sp-extra-tight));
|
|
||||||
@include dir.side(margin, auto, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .room-selector:first-child {
|
|
||||||
margin-top: var(--sp-extra-tight);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .cat-header {
|
|
||||||
margin: var(--sp-normal);
|
|
||||||
margin-bottom: var(--sp-extra-tight);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ function DrawerBreadcrumb({ spaceId }) {
|
|||||||
};
|
};
|
||||||
}, [spaceId]);
|
}, [spaceId]);
|
||||||
|
|
||||||
if (spacePath.length === 1) return null;
|
|
||||||
|
|
||||||
function getHomeNotiExcept(childId) {
|
function getHomeNotiExcept(childId) {
|
||||||
const orphans = roomList.getOrphans();
|
const orphans = roomList.getOrphans();
|
||||||
const childIndex = orphans.indexOf(childId);
|
const childIndex = orphans.indexOf(childId);
|
||||||
@@ -74,20 +72,28 @@ function DrawerBreadcrumb({ spaceId }) {
|
|||||||
const noti = notifications.getNoti(roomId);
|
const noti = notifications.getNoti(roomId);
|
||||||
if (!notifications.hasNoti(childId)) return noti;
|
if (!notifications.hasNoti(childId)) return noti;
|
||||||
if (noti.from === null) return noti;
|
if (noti.from === null) return noti;
|
||||||
if (noti.from.has(childId) && noti.from.size === 1) return null;
|
|
||||||
|
|
||||||
const childNoti = notifications.getNoti(childId);
|
const childNoti = notifications.getNoti(childId);
|
||||||
|
|
||||||
return {
|
let noOther = true;
|
||||||
total: noti.total - childNoti.total,
|
let total = 0;
|
||||||
highlight: noti.highlight - childNoti.highlight,
|
let highlight = 0;
|
||||||
};
|
noti.from.forEach((fromId) => {
|
||||||
|
if (childNoti.from.has(fromId)) return;
|
||||||
|
noOther = false;
|
||||||
|
const fromNoti = notifications.getNoti(fromId);
|
||||||
|
total += fromNoti.total;
|
||||||
|
highlight += fromNoti.highlight;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (noOther) return null;
|
||||||
|
return { total, highlight };
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="breadcrumb__wrapper">
|
<div className="drawer-breadcrumb__wrapper">
|
||||||
<ScrollView ref={scrollRef} horizontal vertical={false} invisible>
|
<ScrollView ref={scrollRef} horizontal vertical={false} invisible>
|
||||||
<div className="breadcrumb">
|
<div className="drawer-breadcrumb">
|
||||||
{
|
{
|
||||||
spacePath.map((id, index) => {
|
spacePath.map((id, index) => {
|
||||||
const noti = (id !== cons.tabs.HOME && index < spacePath.length)
|
const noti = (id !== cons.tabs.HOME && index < spacePath.length)
|
||||||
@@ -100,7 +106,7 @@ function DrawerBreadcrumb({ spaceId }) {
|
|||||||
>
|
>
|
||||||
{ index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
{ index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
||||||
<Button
|
<Button
|
||||||
className={index === spacePath.length - 1 ? 'breadcrumb__btn--selected' : ''}
|
className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
|
||||||
onClick={() => selectSpace(id)}
|
onClick={() => selectSpace(id)}
|
||||||
>
|
>
|
||||||
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
|
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
@use '../../partials/text';
|
@use '../../partials/text';
|
||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.breadcrumb__wrapper {
|
.drawer-breadcrumb__wrapper {
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb {
|
.drawer-breadcrumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,78 +1,141 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import './DrawerHeader.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
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, openInviteUser,
|
openPublicRooms, openCreateRoom, openSpaceManage,
|
||||||
|
openSpaceAddExisting, openInviteUser, openReusableContextMenu,
|
||||||
} from '../../../client/action/navigation';
|
} from '../../../client/action/navigation';
|
||||||
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
import { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
|
||||||
|
|
||||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||||
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
|
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
|
||||||
|
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
|
||||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
|
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
|
||||||
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
|
import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg';
|
||||||
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
|
|
||||||
|
export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(spaceId);
|
||||||
|
const canManage = room
|
||||||
|
? room.currentState.maySendStateEvent('m.space.child', mx.getUserId())
|
||||||
|
: true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuHeader>Add rooms or spaces</MenuHeader>
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={SpacePlusIC}
|
||||||
|
onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
|
||||||
|
disabled={!canManage}
|
||||||
|
>
|
||||||
|
Create new space
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={HashPlusIC}
|
||||||
|
onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
|
||||||
|
disabled={!canManage}
|
||||||
|
>
|
||||||
|
Create new room
|
||||||
|
</MenuItem>
|
||||||
|
{ !spaceId && (
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={HashGlobeIC}
|
||||||
|
onClick={() => { afterOptionSelect(); openPublicRooms(); }}
|
||||||
|
>
|
||||||
|
Join public room
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{ spaceId && (
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={PlusIC}
|
||||||
|
onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
|
||||||
|
disabled={!canManage}
|
||||||
|
>
|
||||||
|
Add existing
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{ spaceId && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
|
||||||
|
iconSrc={HashSearchIC}
|
||||||
|
>
|
||||||
|
Manage rooms
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
HomeSpaceOptions.defaultProps = {
|
||||||
|
spaceId: null,
|
||||||
|
};
|
||||||
|
HomeSpaceOptions.propTypes = {
|
||||||
|
spaceId: PropTypes.string,
|
||||||
|
afterOptionSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
function DrawerHeader({ selectedTab, spaceId }) {
|
function DrawerHeader({ selectedTab, spaceId }) {
|
||||||
const [, forceUpdate] = useState({});
|
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages';
|
const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages';
|
||||||
|
|
||||||
|
const isDMTab = selectedTab === cons.tabs.DIRECTS;
|
||||||
const room = mx.getRoom(spaceId);
|
const room = mx.getRoom(spaceId);
|
||||||
const spaceName = selectedTab === cons.tabs.DIRECTS ? null : (room?.name || null);
|
const spaceName = isDMTab ? null : (room?.name || null);
|
||||||
|
|
||||||
|
const openSpaceOptions = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openReusableContextMenu(
|
||||||
|
'bottom',
|
||||||
|
getEventCords(e, '.header'),
|
||||||
|
(closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openHomeSpaceOptions = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openReusableContextMenu(
|
||||||
|
'right',
|
||||||
|
getEventCords(e, '.ic-btn'),
|
||||||
|
(closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
{spaceName ? (
|
||||||
<Text variant="s1" weight="medium" primary>{twemojify(spaceName) || tabName}</Text>
|
<button
|
||||||
</TitleWrapper>
|
className="drawer-header__btn"
|
||||||
{spaceName && (
|
onClick={openSpaceOptions}
|
||||||
<IconButton
|
type="button"
|
||||||
size="extra-small"
|
onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
|
||||||
variant="surface"
|
>
|
||||||
tooltip={initMatrix.roomList.spaceShortcut.has(spaceId) ? 'Unpin' : 'Pin to sidebar'}
|
<TitleWrapper>
|
||||||
src={initMatrix.roomList.spaceShortcut.has(spaceId) ? PinFilledIC : PinIC}
|
<Text variant="s1" weight="medium" primary>{twemojify(spaceName)}</Text>
|
||||||
onClick={() => {
|
</TitleWrapper>
|
||||||
if (initMatrix.roomList.spaceShortcut.has(spaceId)) deleteSpaceShortcut(spaceId);
|
<RawIcon size="small" src={ChevronBottomIC} />
|
||||||
else createSpaceShortcut(spaceId);
|
</button>
|
||||||
forceUpdate({});
|
) : (
|
||||||
}}
|
<TitleWrapper>
|
||||||
/>
|
<Text variant="s1" weight="medium" primary>{tabName}</Text>
|
||||||
|
</TitleWrapper>
|
||||||
)}
|
)}
|
||||||
{ selectedTab === cons.tabs.DIRECTS && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" /> }
|
|
||||||
{ selectedTab !== cons.tabs.DIRECTS && !spaceName && (
|
{ isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> }
|
||||||
<>
|
{ !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> }
|
||||||
<ContextMenu
|
|
||||||
content={(hideMenu) => (
|
|
||||||
<>
|
|
||||||
<MenuHeader>Add room</MenuHeader>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={HashPlusIC}
|
|
||||||
onClick={() => { hideMenu(); openCreateRoom(); }}
|
|
||||||
>
|
|
||||||
Create new room
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={HashSearchIC}
|
|
||||||
onClick={() => { hideMenu(); openPublicRooms(); }}
|
|
||||||
>
|
|
||||||
Add public room
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
render={(toggleMenu) => (<IconButton onClick={toggleMenu} tooltip="Add room" src={PlusIC} size="normal" />)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* <IconButton onClick={() => ''} tooltip="Menu" src={VerticalMenuIC} size="normal" /> */}
|
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/app/organisms/navigation/DrawerHeader.scss
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@use '../../partials/flex';
|
||||||
|
@use '../../partials/dir';
|
||||||
|
|
||||||
|
.drawer-header__btn {
|
||||||
|
min-width: 0;
|
||||||
|
@extend .cp-fx__row--s-c;
|
||||||
|
@include dir.side(margin, 0, auto);
|
||||||
|
padding: var(--sp-ultra-tight);
|
||||||
|
border-radius: calc(var(--bo-radius) / 2);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
& .header__title-wrapper {
|
||||||
|
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover:hover) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
box-shadow: var(--bs-surface-outline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background-color: var(--bg-surface-active);
|
||||||
|
box-shadow: var(--bs-surface-outline);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,106 +1,96 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { selectSpace, selectRoom } from '../../../client/action/navigation';
|
|
||||||
import Postie from '../../../util/Postie';
|
import Postie from '../../../util/Postie';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import RoomsCategory from './RoomsCategory';
|
||||||
import Selector from './Selector';
|
|
||||||
|
|
||||||
import { AtoZ } from './common';
|
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
|
||||||
|
import { AtoZ, RoomToDM } from './common';
|
||||||
|
|
||||||
const drawerPostie = new Postie();
|
const drawerPostie = new Postie();
|
||||||
function Home({ spaceId }) {
|
function Home({ spaceId }) {
|
||||||
const [, forceUpdate] = useState({});
|
const mx = initMatrix.matrixClient;
|
||||||
const { roomList, notifications } = initMatrix;
|
const { roomList, notifications, accountData } = initMatrix;
|
||||||
|
const { spaces, rooms, directs } = roomList;
|
||||||
|
useCategorizedSpaces();
|
||||||
|
const isCategorized = accountData.categorizedSpaces.has(spaceId);
|
||||||
|
|
||||||
|
let categories = null;
|
||||||
let spaceIds = [];
|
let spaceIds = [];
|
||||||
let roomIds = [];
|
let roomIds = [];
|
||||||
let directIds = [];
|
let directIds = [];
|
||||||
|
|
||||||
const spaceChildIds = roomList.getSpaceChildren(spaceId);
|
if (spaceId) {
|
||||||
if (spaceChildIds) {
|
const spaceChildIds = roomList.getSpaceChildren(spaceId);
|
||||||
spaceIds = spaceChildIds.filter((roomId) => roomList.spaces.has(roomId)).sort(AtoZ);
|
spaceIds = spaceChildIds.filter((roomId) => spaces.has(roomId));
|
||||||
roomIds = spaceChildIds.filter((roomId) => roomList.rooms.has(roomId)).sort(AtoZ);
|
roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId));
|
||||||
directIds = spaceChildIds.filter((roomId) => roomList.directs.has(roomId)).sort(AtoZ);
|
directIds = spaceChildIds.filter((roomId) => directs.has(roomId));
|
||||||
} else {
|
} else {
|
||||||
spaceIds = [...roomList.spaces]
|
spaceIds = roomList.getOrphanSpaces();
|
||||||
.filter((roomId) => !roomList.roomIdToParents.has(roomId)).sort(AtoZ);
|
roomIds = roomList.getOrphanRooms();
|
||||||
roomIds = [...roomList.rooms]
|
|
||||||
.filter((roomId) => !roomList.roomIdToParents.has(roomId)).sort(AtoZ);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectorChanged(selectedRoomId, prevSelectedRoomId) {
|
spaceIds.sort(AtoZ);
|
||||||
if (!drawerPostie.hasTopic('selector-change')) return;
|
roomIds.sort(AtoZ);
|
||||||
const addresses = [];
|
directIds.sort(AtoZ);
|
||||||
if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId);
|
|
||||||
if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId);
|
|
||||||
if (addresses.length === 0) return;
|
|
||||||
drawerPostie.post('selector-change', addresses, selectedRoomId);
|
|
||||||
}
|
|
||||||
function notiChanged(roomId, total, prevTotal) {
|
|
||||||
if (total === prevTotal) return;
|
|
||||||
if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
|
|
||||||
drawerPostie.post('unread-change', roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function roomListUpdated() {
|
if (isCategorized) {
|
||||||
const { spaces, rooms, directs } = initMatrix.roomList;
|
categories = roomList.getCategorizedSpaces(spaceIds);
|
||||||
if (!(
|
categories.delete(spaceId);
|
||||||
spaces.has(navigation.selectedRoomId)
|
|
||||||
|| rooms.has(navigation.selectedRoomId)
|
|
||||||
|| directs.has(navigation.selectedRoomId))
|
|
||||||
) {
|
|
||||||
selectRoom(null);
|
|
||||||
}
|
|
||||||
forceUpdate({});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
|
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
|
||||||
|
if (!drawerPostie.hasTopic('selector-change')) return;
|
||||||
|
const addresses = [];
|
||||||
|
if (drawerPostie.hasSubscriber('selector-change', selectedRoomId)) addresses.push(selectedRoomId);
|
||||||
|
if (drawerPostie.hasSubscriber('selector-change', prevSelectedRoomId)) addresses.push(prevSelectedRoomId);
|
||||||
|
if (addresses.length === 0) return;
|
||||||
|
drawerPostie.post('selector-change', addresses, selectedRoomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notiChanged = (roomId, total, prevTotal) => {
|
||||||
|
if (total === prevTotal) return;
|
||||||
|
if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
|
||||||
|
drawerPostie.post('unread-change', roomId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
return () => {
|
return () => {
|
||||||
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
|
|
||||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
|
||||||
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
|
||||||
|
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ spaceIds.length !== 0 && <Text className="cat-header" variant="b3" weight="bold">Spaces</Text> }
|
{ !isCategorized && spaceIds.length !== 0 && (
|
||||||
{ spaceIds.map((id) => (
|
<RoomsCategory name="Spaces" roomIds={spaceIds} drawerPostie={drawerPostie} />
|
||||||
<Selector
|
)}
|
||||||
key={id}
|
|
||||||
roomId={id}
|
|
||||||
isDM={false}
|
|
||||||
drawerPostie={drawerPostie}
|
|
||||||
onClick={() => selectSpace(id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{ roomIds.length !== 0 && <Text className="cat-header" variant="b3" weight="bold">Rooms</Text> }
|
{ roomIds.length !== 0 && (
|
||||||
{ roomIds.map((id) => (
|
<RoomsCategory name="Rooms" roomIds={roomIds} drawerPostie={drawerPostie} />
|
||||||
<Selector
|
)}
|
||||||
key={id}
|
|
||||||
roomId={id}
|
|
||||||
isDM={false}
|
|
||||||
drawerPostie={drawerPostie}
|
|
||||||
onClick={() => selectRoom(id)}
|
|
||||||
/>
|
|
||||||
)) }
|
|
||||||
|
|
||||||
{ directIds.length !== 0 && <Text className="cat-header" variant="b3" weight="bold">People</Text> }
|
{ directIds.length !== 0 && (
|
||||||
{ directIds.map((id) => (
|
<RoomsCategory name="People" roomIds={directIds} drawerPostie={drawerPostie} />
|
||||||
<Selector
|
)}
|
||||||
key={id}
|
|
||||||
roomId={id}
|
{ isCategorized && [...categories].map(([catId, childIds]) => (
|
||||||
|
<RoomsCategory
|
||||||
|
key={catId}
|
||||||
|
spaceId={catId}
|
||||||
|
name={mx.getRoom(catId).name}
|
||||||
|
roomIds={[...childIds].sort(AtoZ).sort(RoomToDM)}
|
||||||
drawerPostie={drawerPostie}
|
drawerPostie={drawerPostie}
|
||||||
onClick={() => selectRoom(id)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
92
src/app/organisms/navigation/RoomsCategory.jsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './RoomsCategory.scss';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import { selectSpace, selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import Selector from './Selector';
|
||||||
|
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
|
||||||
|
import { HomeSpaceOptions } from './DrawerHeader';
|
||||||
|
|
||||||
|
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||||
|
import HorizontalMenuIC from '../../../../public/res/ic/outlined/horizontal-menu.svg';
|
||||||
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
|
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
|
||||||
|
|
||||||
|
function RoomsCategory({
|
||||||
|
spaceId, name, hideHeader, roomIds, drawerPostie,
|
||||||
|
}) {
|
||||||
|
const { spaces, directs } = initMatrix.roomList;
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
|
const openSpaceOptions = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openReusableContextMenu(
|
||||||
|
'bottom',
|
||||||
|
getEventCords(e, '.header'),
|
||||||
|
(closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openHomeSpaceOptions = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openReusableContextMenu(
|
||||||
|
'right',
|
||||||
|
getEventCords(e, '.ic-btn'),
|
||||||
|
(closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSelector = (roomId) => {
|
||||||
|
const isSpace = spaces.has(roomId);
|
||||||
|
const isDM = directs.has(roomId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Selector
|
||||||
|
key={roomId}
|
||||||
|
roomId={roomId}
|
||||||
|
isDM={isDM}
|
||||||
|
drawerPostie={drawerPostie}
|
||||||
|
onClick={() => (isSpace ? selectSpace(roomId) : selectRoom(roomId))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="room-category">
|
||||||
|
{!hideHeader && (
|
||||||
|
<div className="room-category__header">
|
||||||
|
<button className="room-category__toggle" onClick={() => setIsOpen(!isOpen)} type="button">
|
||||||
|
<RawIcon src={isOpen ? ChevronBottomIC : ChevronRightIC} size="extra-small" />
|
||||||
|
<Text className="cat-header" variant="b3" weight="medium">{name}</Text>
|
||||||
|
</button>
|
||||||
|
{spaceId && <IconButton onClick={openSpaceOptions} tooltip="Space options" src={HorizontalMenuIC} size="extra-small" />}
|
||||||
|
{spaceId && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="extra-small" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(isOpen || hideHeader) && (
|
||||||
|
<div className="room-category__content">
|
||||||
|
{roomIds.map(renderSelector)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
RoomsCategory.defaultProps = {
|
||||||
|
spaceId: null,
|
||||||
|
hideHeader: false,
|
||||||
|
};
|
||||||
|
RoomsCategory.propTypes = {
|
||||||
|
spaceId: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
hideHeader: PropTypes.bool,
|
||||||
|
roomIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
drawerPostie: PropTypes.shape({}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomsCategory;
|
||||||
54
src/app/organisms/navigation/RoomsCategory.scss
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@use '../../partials/flex';
|
||||||
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/text';
|
||||||
|
|
||||||
|
.room-category {
|
||||||
|
&__header,
|
||||||
|
&__toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&__header {
|
||||||
|
margin-top: var(--sp-extra-tight);
|
||||||
|
|
||||||
|
& .ic-btn {
|
||||||
|
padding: var(--sp-ultra-tight);
|
||||||
|
border-radius: 4px;
|
||||||
|
@include dir.side(margin, 0, 5px);
|
||||||
|
& .ic-raw {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: var(--ic-surface-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__toggle {
|
||||||
|
@extend .cp-fx__item-one;
|
||||||
|
padding: var(--sp-extra-tight) var(--sp-tight);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
& .ic-raw {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: var(--ic-surface-low);
|
||||||
|
@include dir.side(margin, 0, var(--sp-ultra-tight));
|
||||||
|
}
|
||||||
|
& .text {
|
||||||
|
text-transform: uppercase;
|
||||||
|
@extend .cp-txt__ellipsis;
|
||||||
|
}
|
||||||
|
&:hover .text {
|
||||||
|
color: var(--tc-surface-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content:first-child {
|
||||||
|
margin-top: var(--sp-extra-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .room-selector {
|
||||||
|
width: calc(100% - var(--sp-extra-tight));
|
||||||
|
@include dir.side(margin, auto, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,119 +1,78 @@
|
|||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room';
|
|
||||||
import { getEventCords, abbreviateNumber } from '../../../util/common';
|
import { getEventCords, abbreviateNumber } from '../../../util/common';
|
||||||
|
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
|
||||||
|
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import RoomSelector from '../../molecules/room-selector/RoomSelector';
|
import RoomSelector from '../../molecules/room-selector/RoomSelector';
|
||||||
import RoomOptions from '../../molecules/room-options/RoomOptions';
|
import RoomOptions from '../../molecules/room-options/RoomOptions';
|
||||||
|
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
|
||||||
|
|
||||||
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
|
||||||
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
|
|
||||||
import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg';
|
|
||||||
import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
|
|
||||||
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
|
|
||||||
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
|
|
||||||
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
|
|
||||||
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
|
|
||||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||||
|
|
||||||
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
|
||||||
function Selector({
|
function Selector({
|
||||||
roomId, isDM, drawerPostie, onClick,
|
roomId, isDM, drawerPostie, onClick,
|
||||||
}) {
|
}) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const noti = initMatrix.notifications;
|
const noti = initMatrix.notifications;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
|
|
||||||
const [isSelected, setIsSelected] = useState(navigation.selectedRoomId === roomId);
|
const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
|
||||||
const [, forceUpdate] = useState({});
|
|
||||||
|
|
||||||
function selectorChanged(selectedRoomId) {
|
const [, forceUpdate] = useForceUpdate();
|
||||||
setIsSelected(selectedRoomId === roomId);
|
|
||||||
}
|
|
||||||
function changeNotificationBadge() {
|
|
||||||
forceUpdate({});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
drawerPostie.subscribe('selector-change', roomId, selectorChanged);
|
const unSub1 = drawerPostie.subscribe('selector-change', roomId, forceUpdate);
|
||||||
drawerPostie.subscribe('unread-change', roomId, changeNotificationBadge);
|
const unSub2 = drawerPostie.subscribe('unread-change', roomId, forceUpdate);
|
||||||
return () => {
|
return () => {
|
||||||
drawerPostie.unsubscribe('selector-change', roomId);
|
unSub1();
|
||||||
drawerPostie.unsubscribe('unread-change', roomId);
|
unSub2();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openRoomOptions = (e) => {
|
const openOptions = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openReusableContextMenu(
|
openReusableContextMenu(
|
||||||
'right',
|
'right',
|
||||||
getEventCords(e, '.room-selector'),
|
getEventCords(e, '.room-selector'),
|
||||||
(closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
|
room.isSpaceRoom()
|
||||||
|
? (closeMenu) => <SpaceOptions roomId={roomId} afterOptionSelect={closeMenu} />
|
||||||
|
: (closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const joinRuleToIconSrc = (joinRule) => ({
|
|
||||||
restricted: () => (room.isSpaceRoom() ? SpaceIC : HashIC),
|
|
||||||
invite: () => (room.isSpaceRoom() ? SpaceLockIC : HashLockIC),
|
|
||||||
public: () => (room.isSpaceRoom() ? SpaceGlobeIC : HashGlobeIC),
|
|
||||||
}[joinRule]?.() || null);
|
|
||||||
|
|
||||||
if (room.isSpaceRoom()) {
|
|
||||||
return (
|
|
||||||
<RoomSelector
|
|
||||||
key={roomId}
|
|
||||||
name={room.name}
|
|
||||||
roomId={roomId}
|
|
||||||
iconSrc={joinRuleToIconSrc(room.getJoinRule())}
|
|
||||||
isUnread={noti.hasNoti(roomId)}
|
|
||||||
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
|
|
||||||
isAlert={noti.getHighlightNoti(roomId) !== 0}
|
|
||||||
onClick={onClick}
|
|
||||||
options={(
|
|
||||||
<IconButton
|
|
||||||
size="extra-small"
|
|
||||||
variant="surface"
|
|
||||||
tooltip={initMatrix.roomList.spaceShortcut.has(roomId) ? 'Unpin' : 'Pin to sidebar'}
|
|
||||||
tooltipPlacement="right"
|
|
||||||
src={initMatrix.roomList.spaceShortcut.has(roomId) ? PinFilledIC : PinIC}
|
|
||||||
onClick={() => {
|
|
||||||
if (initMatrix.roomList.spaceShortcut.has(roomId)) deleteSpaceShortcut(roomId);
|
|
||||||
else createSpaceShortcut(roomId);
|
|
||||||
forceUpdate({});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomSelector
|
<RoomSelector
|
||||||
key={roomId}
|
key={roomId}
|
||||||
name={room.name}
|
name={room.name}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
imageSrc={isDM ? imageSrc : null}
|
imageSrc={isDM ? imageSrc : null}
|
||||||
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule())}
|
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
|
||||||
isSelected={isSelected}
|
isSelected={navigation.selectedRoomId === roomId}
|
||||||
isUnread={noti.hasNoti(roomId)}
|
isMuted={isMuted}
|
||||||
|
isUnread={!isMuted && noti.hasNoti(roomId)}
|
||||||
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
|
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
|
||||||
isAlert={noti.getHighlightNoti(roomId) !== 0}
|
isAlert={noti.getHighlightNoti(roomId) !== 0}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={openRoomOptions}
|
onContextMenu={openOptions}
|
||||||
options={(
|
options={(
|
||||||
<IconButton
|
<IconButton
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
tooltip="Options"
|
tooltip="Options"
|
||||||
tooltipPlacement="right"
|
tooltipPlacement="right"
|
||||||
src={VerticalMenuIC}
|
src={VerticalMenuIC}
|
||||||
onClick={openRoomOptions}
|
onClick={openOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,24 +1,48 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import './SideBar.scss';
|
import './SideBar.scss';
|
||||||
|
|
||||||
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import {
|
import {
|
||||||
selectTab, openInviteList, openSearch, openSettings,
|
selectTab, openShortcutSpaces, openInviteList,
|
||||||
|
openSearch, openSettings, openReusableContextMenu,
|
||||||
} from '../../../client/action/navigation';
|
} from '../../../client/action/navigation';
|
||||||
import { abbreviateNumber } from '../../../util/common';
|
import { moveSpaceShortcut } from '../../../client/action/accountData';
|
||||||
|
import { abbreviateNumber, getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
|
import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar';
|
import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar';
|
||||||
|
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
|
||||||
|
|
||||||
import HomeIC from '../../../../public/res/ic/outlined/home.svg';
|
import HomeIC from '../../../../public/res/ic/outlined/home.svg';
|
||||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||||
|
import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
|
||||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
|
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
|
||||||
|
|
||||||
import { useSelectedTab } from '../../hooks/useSelectedTab';
|
import { useSelectedTab } from '../../hooks/useSelectedTab';
|
||||||
import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
|
|
||||||
|
function useNotificationUpdate() {
|
||||||
|
const { notifications } = initMatrix;
|
||||||
|
const [, forceUpdate] = useState({});
|
||||||
|
useEffect(() => {
|
||||||
|
function onNotificationChanged(roomId, total, prevTotal) {
|
||||||
|
if (total === prevTotal) return;
|
||||||
|
forceUpdate({});
|
||||||
|
}
|
||||||
|
notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
|
||||||
|
return () => {
|
||||||
|
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
function ProfileAvatarMenu() {
|
function ProfileAvatarMenu() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
@@ -49,59 +73,29 @@ function ProfileAvatarMenu() {
|
|||||||
<SidebarAvatar
|
<SidebarAvatar
|
||||||
onClick={openSettings}
|
onClick={openSettings}
|
||||||
tooltip={profile.displayName}
|
tooltip={profile.displayName}
|
||||||
imageSrc={profile.avatarUrl !== null ? mx.mxcUrlToHttp(profile.avatarUrl, 42, 42, 'crop') : null}
|
avatar={(
|
||||||
bgColor={colorMXID(mx.getUserId())}
|
<Avatar
|
||||||
text={profile.displayName}
|
text={profile.displayName}
|
||||||
|
bgColor={colorMXID(mx.getUserId())}
|
||||||
|
size="normal"
|
||||||
|
imageSrc={profile.avatarUrl !== null ? mx.mxcUrlToHttp(profile.avatarUrl, 42, 42, 'crop') : null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTotalInvites() {
|
function FeaturedTab() {
|
||||||
const { roomList } = initMatrix;
|
const { roomList, accountData, notifications } = initMatrix;
|
||||||
const totalInviteCount = () => roomList.inviteRooms.size
|
|
||||||
+ roomList.inviteSpaces.size
|
|
||||||
+ roomList.inviteDirects.size;
|
|
||||||
const [totalInvites, updateTotalInvites] = useState(totalInviteCount());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onInviteListChange = () => {
|
|
||||||
updateTotalInvites(totalInviteCount());
|
|
||||||
};
|
|
||||||
roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
|
|
||||||
return () => {
|
|
||||||
roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [totalInvites];
|
|
||||||
}
|
|
||||||
|
|
||||||
function SideBar() {
|
|
||||||
const { roomList, notifications } = initMatrix;
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
|
|
||||||
const [selectedTab] = useSelectedTab();
|
const [selectedTab] = useSelectedTab();
|
||||||
const [spaceShortcut] = useSpaceShortcut();
|
useNotificationUpdate();
|
||||||
const [totalInvites] = useTotalInvites();
|
|
||||||
const [, forceUpdate] = useState({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onNotificationChanged(roomId, total, prevTotal) {
|
|
||||||
if (total === prevTotal) return;
|
|
||||||
forceUpdate({});
|
|
||||||
}
|
|
||||||
notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
|
|
||||||
return () => {
|
|
||||||
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function getHomeNoti() {
|
function getHomeNoti() {
|
||||||
const orphans = roomList.getOrphans();
|
const orphans = roomList.getOrphans();
|
||||||
let noti = null;
|
let noti = null;
|
||||||
|
|
||||||
orphans.forEach((roomId) => {
|
orphans.forEach((roomId) => {
|
||||||
if (roomList.spaceShortcut.has(roomId)) return;
|
if (accountData.spaceShortcut.has(roomId)) return;
|
||||||
if (!notifications.hasNoti(roomId)) return;
|
if (!notifications.hasNoti(roomId)) return;
|
||||||
if (noti === null) noti = { total: 0, highlight: 0 };
|
if (noti === null) noti = { total: 0, highlight: 0 };
|
||||||
const childNoti = notifications.getNoti(roomId);
|
const childNoti = notifications.getNoti(roomId);
|
||||||
@@ -126,58 +120,224 @@ function SideBar() {
|
|||||||
return noti;
|
return noti;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: bellow operations are heavy.
|
|
||||||
// refactor this component into more smaller components.
|
|
||||||
const dmsNoti = getDMsNoti();
|
const dmsNoti = getDMsNoti();
|
||||||
const homeNoti = getHomeNoti();
|
const homeNoti = getHomeNoti();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SidebarAvatar
|
||||||
|
tooltip="Home"
|
||||||
|
active={selectedTab === cons.tabs.HOME}
|
||||||
|
onClick={() => selectTab(cons.tabs.HOME)}
|
||||||
|
avatar={<Avatar iconSrc={HomeIC} size="normal" />}
|
||||||
|
notificationBadge={homeNoti ? (
|
||||||
|
<NotificationBadge
|
||||||
|
alert={homeNoti?.highlight > 0}
|
||||||
|
content={abbreviateNumber(homeNoti.total) || null}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
/>
|
||||||
|
<SidebarAvatar
|
||||||
|
tooltip="People"
|
||||||
|
active={selectedTab === cons.tabs.DIRECTS}
|
||||||
|
onClick={() => selectTab(cons.tabs.DIRECTS)}
|
||||||
|
avatar={<Avatar iconSrc={UserIC} size="normal" />}
|
||||||
|
notificationBadge={dmsNoti ? (
|
||||||
|
<NotificationBadge
|
||||||
|
alert={dmsNoti?.highlight > 0}
|
||||||
|
content={abbreviateNumber(dmsNoti.total) || null}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DraggableSpaceShortcut({
|
||||||
|
isActive, spaceId, index, moveShortcut, onDrop,
|
||||||
|
}) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const { notifications } = initMatrix;
|
||||||
|
const room = mx.getRoom(spaceId);
|
||||||
|
const shortcutRef = useRef(null);
|
||||||
|
const avatarRef = useRef(null);
|
||||||
|
|
||||||
|
const openSpaceOptions = (e, sId) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openReusableContextMenu(
|
||||||
|
'right',
|
||||||
|
getEventCords(e, '.sidebar-avatar'),
|
||||||
|
(closeMenu) => <SpaceOptions roomId={sId} afterOptionSelect={closeMenu} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [, drop] = useDrop({
|
||||||
|
accept: 'SPACE_SHORTCUT',
|
||||||
|
collect(monitor) {
|
||||||
|
return {
|
||||||
|
handlerId: monitor.getHandlerId(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
drop(item) {
|
||||||
|
onDrop(item.index, item.spaceId);
|
||||||
|
},
|
||||||
|
hover(item, monitor) {
|
||||||
|
if (!shortcutRef.current) return;
|
||||||
|
|
||||||
|
const dragIndex = item.index;
|
||||||
|
const hoverIndex = index;
|
||||||
|
if (dragIndex === hoverIndex) return;
|
||||||
|
|
||||||
|
const hoverBoundingRect = shortcutRef.current?.getBoundingClientRect();
|
||||||
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
moveShortcut(dragIndex, hoverIndex);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
item.index = hoverIndex;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [{ isDragging }, drag] = useDrag({
|
||||||
|
type: 'SPACE_SHORTCUT',
|
||||||
|
item: () => ({ spaceId, index }),
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
drag(avatarRef);
|
||||||
|
drop(shortcutRef);
|
||||||
|
|
||||||
|
if (shortcutRef.current) {
|
||||||
|
if (isDragging) shortcutRef.current.style.opacity = 0;
|
||||||
|
else shortcutRef.current.style.opacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarAvatar
|
||||||
|
ref={shortcutRef}
|
||||||
|
active={isActive}
|
||||||
|
tooltip={room.name}
|
||||||
|
onClick={() => selectTab(spaceId)}
|
||||||
|
onContextMenu={(e) => openSpaceOptions(e, spaceId)}
|
||||||
|
avatar={(
|
||||||
|
<Avatar
|
||||||
|
ref={avatarRef}
|
||||||
|
text={room.name}
|
||||||
|
bgColor={colorMXID(room.roomId)}
|
||||||
|
size="normal"
|
||||||
|
imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
notificationBadge={notifications.hasNoti(spaceId) ? (
|
||||||
|
<NotificationBadge
|
||||||
|
alert={notifications.getHighlightNoti(spaceId) > 0}
|
||||||
|
content={abbreviateNumber(notifications.getTotalNoti(spaceId)) || null}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DraggableSpaceShortcut.propTypes = {
|
||||||
|
spaceId: PropTypes.string.isRequired,
|
||||||
|
isActive: PropTypes.bool.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
moveShortcut: PropTypes.func.isRequired,
|
||||||
|
onDrop: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function SpaceShortcut() {
|
||||||
|
const { accountData } = initMatrix;
|
||||||
|
const [selectedTab] = useSelectedTab();
|
||||||
|
useNotificationUpdate();
|
||||||
|
const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleShortcut = () => setSpaceShortcut([...accountData.spaceShortcut]);
|
||||||
|
accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
|
||||||
|
return () => {
|
||||||
|
accountData.removeListener(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const moveShortcut = (dragIndex, hoverIndex) => {
|
||||||
|
const dragSpaceId = spaceShortcut[dragIndex];
|
||||||
|
const newShortcuts = [...spaceShortcut];
|
||||||
|
newShortcuts.splice(dragIndex, 1);
|
||||||
|
newShortcuts.splice(hoverIndex, 0, dragSpaceId);
|
||||||
|
setSpaceShortcut(newShortcuts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (dragIndex, dragSpaceId) => {
|
||||||
|
if ([...accountData.spaceShortcut][dragIndex] === dragSpaceId) return;
|
||||||
|
moveSpaceShortcut(dragSpaceId, dragIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
{
|
||||||
|
spaceShortcut.map((shortcut, index) => (
|
||||||
|
<DraggableSpaceShortcut
|
||||||
|
key={shortcut}
|
||||||
|
index={index}
|
||||||
|
spaceId={shortcut}
|
||||||
|
isActive={selectedTab === shortcut}
|
||||||
|
moveShortcut={moveShortcut}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</DndProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTotalInvites() {
|
||||||
|
const { roomList } = initMatrix;
|
||||||
|
const totalInviteCount = () => roomList.inviteRooms.size
|
||||||
|
+ roomList.inviteSpaces.size
|
||||||
|
+ roomList.inviteDirects.size;
|
||||||
|
const [totalInvites, updateTotalInvites] = useState(totalInviteCount());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onInviteListChange = () => {
|
||||||
|
updateTotalInvites(totalInviteCount());
|
||||||
|
};
|
||||||
|
roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
|
||||||
|
return () => {
|
||||||
|
roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [totalInvites];
|
||||||
|
}
|
||||||
|
|
||||||
|
function SideBar() {
|
||||||
|
const [totalInvites] = useTotalInvites();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<div className="sidebar__scrollable">
|
<div className="sidebar__scrollable">
|
||||||
<ScrollView invisible>
|
<ScrollView invisible>
|
||||||
<div className="scrollable-content">
|
<div className="scrollable-content">
|
||||||
<div className="featured-container">
|
<div className="featured-container">
|
||||||
<SidebarAvatar
|
<FeaturedTab />
|
||||||
active={selectedTab === cons.tabs.HOME}
|
|
||||||
onClick={() => selectTab(cons.tabs.HOME)}
|
|
||||||
tooltip="Home"
|
|
||||||
iconSrc={HomeIC}
|
|
||||||
isUnread={homeNoti !== null}
|
|
||||||
notificationCount={homeNoti !== null ? abbreviateNumber(homeNoti.total) : 0}
|
|
||||||
isAlert={homeNoti?.highlight > 0}
|
|
||||||
/>
|
|
||||||
<SidebarAvatar
|
|
||||||
active={selectedTab === cons.tabs.DIRECTS}
|
|
||||||
onClick={() => selectTab(cons.tabs.DIRECTS)}
|
|
||||||
tooltip="People"
|
|
||||||
iconSrc={UserIC}
|
|
||||||
isUnread={dmsNoti !== null}
|
|
||||||
notificationCount={dmsNoti !== null ? abbreviateNumber(dmsNoti.total) : 0}
|
|
||||||
isAlert={dmsNoti?.highlight > 0}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-divider" />
|
<div className="sidebar-divider" />
|
||||||
<div className="space-container">
|
<div className="space-container">
|
||||||
{
|
<SpaceShortcut />
|
||||||
spaceShortcut.map((shortcut) => {
|
<SidebarAvatar
|
||||||
const sRoomId = shortcut;
|
tooltip="Pin spaces"
|
||||||
const room = mx.getRoom(sRoomId);
|
onClick={() => openShortcutSpaces()}
|
||||||
return (
|
avatar={<Avatar iconSrc={AddPinIC} size="normal" />}
|
||||||
<SidebarAvatar
|
/>
|
||||||
active={selectedTab === sRoomId}
|
|
||||||
key={sRoomId}
|
|
||||||
tooltip={room.name}
|
|
||||||
bgColor={colorMXID(room.roomId)}
|
|
||||||
imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
|
|
||||||
text={room.name}
|
|
||||||
isUnread={notifications.hasNoti(sRoomId)}
|
|
||||||
notificationCount={abbreviateNumber(notifications.getTotalNoti(sRoomId))}
|
|
||||||
isAlert={notifications.getHighlightNoti(sRoomId) !== 0}
|
|
||||||
onClick={() => selectTab(shortcut)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -186,18 +346,16 @@ function SideBar() {
|
|||||||
<div className="sidebar-divider" />
|
<div className="sidebar-divider" />
|
||||||
<div className="sticky-container">
|
<div className="sticky-container">
|
||||||
<SidebarAvatar
|
<SidebarAvatar
|
||||||
onClick={() => openSearch()}
|
|
||||||
tooltip="Search"
|
tooltip="Search"
|
||||||
iconSrc={SearchIC}
|
onClick={() => openSearch()}
|
||||||
|
avatar={<Avatar iconSrc={SearchIC} size="normal" />}
|
||||||
/>
|
/>
|
||||||
{ totalInvites !== 0 && (
|
{ totalInvites !== 0 && (
|
||||||
<SidebarAvatar
|
<SidebarAvatar
|
||||||
isUnread
|
|
||||||
notificationCount={totalInvites}
|
|
||||||
isAlert
|
|
||||||
onClick={() => openInviteList()}
|
|
||||||
tooltip="Invites"
|
tooltip="Invites"
|
||||||
iconSrc={InviteIC}
|
onClick={() => openInviteList()}
|
||||||
|
avatar={<Avatar iconSrc={InviteIC} size="normal" />}
|
||||||
|
notificationBadge={<NotificationBadge alert content={totalInvites} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ProfileAvatarMenu />
|
<ProfileAvatarMenu />
|
||||||
|
|||||||
@@ -18,4 +18,13 @@ function AtoZ(aId, bId) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AtoZ };
|
const RoomToDM = (aId, bId) => {
|
||||||
|
const { directs } = initMatrix.roomList;
|
||||||
|
const aIsDm = directs.has(aId);
|
||||||
|
const bIsDm = directs.has(bId);
|
||||||
|
if (aIsDm && !bIsDm) return 1;
|
||||||
|
if (!aIsDm && bIsDm) return -1;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AtoZ, RoomToDM };
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import navigation from '../../../client/state/navigation';
|
|||||||
import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
|
import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
|
|
||||||
import { getUsername, getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
import {
|
||||||
|
getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith
|
||||||
|
} from '../../../util/matrixUtil';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
@@ -187,27 +189,18 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
|||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
const openDM = async () => {
|
const openDM = async () => {
|
||||||
const directIds = [...initMatrix.roomList.directs];
|
|
||||||
|
|
||||||
// Check and open if user already have a DM with userId.
|
// Check and open if user already have a DM with userId.
|
||||||
for (let i = 0; i < directIds.length; i += 1) {
|
const dmRoomId = hasDMWith(userId);
|
||||||
const dRoom = mx.getRoom(directIds[i]);
|
if (dmRoomId) {
|
||||||
const roomMembers = dRoom.getMembers();
|
selectRoom(dmRoomId);
|
||||||
if (roomMembers.length <= 2 && dRoom.getMember(userId)) {
|
onRequestClose();
|
||||||
selectRoom(directIds[i]);
|
return;
|
||||||
onRequestClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new DM
|
// Create new DM
|
||||||
try {
|
try {
|
||||||
setIsCreatingDM(true);
|
setIsCreatingDM(true);
|
||||||
await roomActions.create({
|
await roomActions.createDM(userId);
|
||||||
isEncrypted: true,
|
|
||||||
isDirect: true,
|
|
||||||
invite: [userId],
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
if (isMountedRef.current === false) return;
|
if (isMountedRef.current === false) return;
|
||||||
setIsCreatingDM(false);
|
setIsCreatingDM(false);
|
||||||
@@ -371,10 +364,16 @@ function ProfileViewer() {
|
|||||||
|
|
||||||
const handleChangePowerLevel = (newPowerLevel) => {
|
const handleChangePowerLevel = (newPowerLevel) => {
|
||||||
if (newPowerLevel === powerLevel) return;
|
if (newPowerLevel === powerLevel) return;
|
||||||
if (newPowerLevel === myPowerLevel
|
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
|
||||||
? confirm('You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?')
|
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
|
||||||
: true
|
|
||||||
) {
|
const isSharedPower = newPowerLevel === myPowerLevel;
|
||||||
|
const isDemotingMyself = userId === mx.getUserId();
|
||||||
|
if (isSharedPower || isDemotingMyself) {
|
||||||
|
if (confirm(isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG)) {
|
||||||
|
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,21 @@ import React from 'react';
|
|||||||
|
|
||||||
import ReadReceipts from '../read-receipts/ReadReceipts';
|
import ReadReceipts from '../read-receipts/ReadReceipts';
|
||||||
import ProfileViewer from '../profile-viewer/ProfileViewer';
|
import ProfileViewer from '../profile-viewer/ProfileViewer';
|
||||||
|
import ShortcutSpaces from '../shortcut-spaces/ShortcutSpaces';
|
||||||
|
import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
|
||||||
import Search from '../search/Search';
|
import Search from '../search/Search';
|
||||||
|
import ViewSource from '../view-source/ViewSource';
|
||||||
|
import CreateRoom from '../create-room/CreateRoom';
|
||||||
|
|
||||||
function Dialogs() {
|
function Dialogs() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReadReceipts />
|
<ReadReceipts />
|
||||||
|
<ViewSource />
|
||||||
<ProfileViewer />
|
<ProfileViewer />
|
||||||
|
<ShortcutSpaces />
|
||||||
|
<CreateRoom />
|
||||||
|
<SpaceAddExisting />
|
||||||
<Search />
|
<Search />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import navigation from '../../../client/state/navigation';
|
|||||||
|
|
||||||
import InviteList from '../invite-list/InviteList';
|
import InviteList from '../invite-list/InviteList';
|
||||||
import PublicRooms from '../public-rooms/PublicRooms';
|
import PublicRooms from '../public-rooms/PublicRooms';
|
||||||
import CreateRoom from '../create-room/CreateRoom';
|
|
||||||
import InviteUser from '../invite-user/InviteUser';
|
import InviteUser from '../invite-user/InviteUser';
|
||||||
import Settings from '../settings/Settings';
|
import Settings from '../settings/Settings';
|
||||||
|
import SpaceSettings from '../space-settings/SpaceSettings';
|
||||||
|
import SpaceManage from '../space-manage/SpaceManage';
|
||||||
|
|
||||||
function Windows() {
|
function Windows() {
|
||||||
const [isInviteList, changeInviteList] = useState(false);
|
const [isInviteList, changeInviteList] = useState(false);
|
||||||
const [publicRooms, changePublicRooms] = useState({
|
const [publicRooms, changePublicRooms] = useState({
|
||||||
isOpen: false, searchTerm: undefined,
|
isOpen: false, searchTerm: undefined,
|
||||||
});
|
});
|
||||||
const [isCreateRoom, changeCreateRoom] = useState(false);
|
|
||||||
const [inviteUser, changeInviteUser] = useState({
|
const [inviteUser, changeInviteUser] = useState({
|
||||||
isOpen: false, roomId: undefined, term: undefined,
|
isOpen: false, roomId: undefined, term: undefined,
|
||||||
});
|
});
|
||||||
@@ -29,9 +29,6 @@ function Windows() {
|
|||||||
searchTerm,
|
searchTerm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function openCreateRoom() {
|
|
||||||
changeCreateRoom(true);
|
|
||||||
}
|
|
||||||
function openInviteUser(roomId, searchTerm) {
|
function openInviteUser(roomId, searchTerm) {
|
||||||
changeInviteUser({
|
changeInviteUser({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@@ -46,13 +43,11 @@ function Windows() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
||||||
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
||||||
navigation.on(cons.events.navigation.CREATE_ROOM_OPENED, openCreateRoom);
|
|
||||||
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
|
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
|
||||||
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
|
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
|
||||||
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
|
||||||
navigation.removeListener(cons.events.navigation.CREATE_ROOM_OPENED, openCreateRoom);
|
|
||||||
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
|
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
|
||||||
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
|
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
|
||||||
};
|
};
|
||||||
@@ -69,10 +64,6 @@ function Windows() {
|
|||||||
searchTerm={publicRooms.searchTerm}
|
searchTerm={publicRooms.searchTerm}
|
||||||
onRequestClose={() => changePublicRooms({ isOpen: false, searchTerm: undefined })}
|
onRequestClose={() => changePublicRooms({ isOpen: false, searchTerm: undefined })}
|
||||||
/>
|
/>
|
||||||
<CreateRoom
|
|
||||||
isOpen={isCreateRoom}
|
|
||||||
onRequestClose={() => changeCreateRoom(false)}
|
|
||||||
/>
|
|
||||||
<InviteUser
|
<InviteUser
|
||||||
isOpen={inviteUser.isOpen}
|
isOpen={inviteUser.isOpen}
|
||||||
roomId={inviteUser.roomId}
|
roomId={inviteUser.roomId}
|
||||||
@@ -83,6 +74,8 @@ function Windows() {
|
|||||||
isOpen={settings}
|
isOpen={settings}
|
||||||
onRequestClose={() => changeSettings(false)}
|
onRequestClose={() => changeSettings(false)}
|
||||||
/>
|
/>
|
||||||
|
<SpaceSettings />
|
||||||
|
<SpaceManage />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/app/organisms/room/EventLimit.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
class EventLimit {
|
||||||
|
constructor() {
|
||||||
|
this._from = 0;
|
||||||
|
|
||||||
|
this.SMALLEST_EVT_HEIGHT = 32;
|
||||||
|
this.PAGES_COUNT = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxEvents() {
|
||||||
|
return Math.round(document.body.clientHeight / this.SMALLEST_EVT_HEIGHT) * this.PAGES_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
get from() {
|
||||||
|
return this._from;
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this._from + this.maxEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFrom(from) {
|
||||||
|
this._from = from < 0 ? 0 : from;
|
||||||
|
}
|
||||||
|
|
||||||
|
paginate(backwards, limit, timelineLength) {
|
||||||
|
this._from = backwards ? this._from - limit : this._from + limit;
|
||||||
|
|
||||||
|
if (!backwards && this.length > timelineLength) {
|
||||||
|
this._from = timelineLength - this.maxEvents;
|
||||||
|
}
|
||||||
|
if (this._from < 0) this._from = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventLimit;
|
||||||
@@ -103,10 +103,10 @@ function PeopleDrawer({ roomId }) {
|
|||||||
}, [memberList]);
|
}, [memberList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isGettingMembers = true;
|
let isLoadingMembers = false;
|
||||||
let isRoomChanged = false;
|
let isRoomChanged = false;
|
||||||
const updateMemberList = (event) => {
|
const updateMemberList = (event) => {
|
||||||
if (isGettingMembers) return;
|
if (isLoadingMembers) return;
|
||||||
if (event && event?.getRoomId() !== roomId) return;
|
if (event && event?.getRoomId() !== roomId) return;
|
||||||
setMemberList(
|
setMemberList(
|
||||||
simplyfiMembers(
|
simplyfiMembers(
|
||||||
@@ -117,8 +117,9 @@ function PeopleDrawer({ roomId }) {
|
|||||||
};
|
};
|
||||||
searchRef.current.value = '';
|
searchRef.current.value = '';
|
||||||
updateMemberList();
|
updateMemberList();
|
||||||
|
isLoadingMembers = true;
|
||||||
room.loadMembersIfNeeded().then(() => {
|
room.loadMembersIfNeeded().then(() => {
|
||||||
isGettingMembers = false;
|
isLoadingMembers = false;
|
||||||
if (isRoomChanged) return;
|
if (isRoomChanged) return;
|
||||||
updateMemberList();
|
updateMemberList();
|
||||||
});
|
});
|
||||||
@@ -194,7 +195,7 @@ function PeopleDrawer({ roomId }) {
|
|||||||
(searchedMembers?.data.length === 0 || memberList.length === 0)
|
(searchedMembers?.data.length === 0 || memberList.length === 0)
|
||||||
&& (
|
&& (
|
||||||
<div className="people-drawer__noresult">
|
<div className="people-drawer__noresult">
|
||||||
<Text variant="b2">No result found!</Text>
|
<Text variant="b2">No results found!</Text>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
--search-input-height: 40px;
|
--search-input-height: 40px;
|
||||||
min-height: var(--search-input-height);
|
min-height: var(--search-input-height);
|
||||||
|
|
||||||
margin: 0 var(--sp-normal);
|
margin: 0 var(--sp-extra-tight);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: var(--sp-normal);
|
bottom: var(--sp-normal);
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
& .input {
|
& .input {
|
||||||
padding: 0 calc(var(--sp-loose) + var(--sp-normal));
|
padding: 0 44px;
|
||||||
height: var(--search-input-height);
|
height: var(--search-input-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,14 @@
|
|||||||
padding-top: var(--sp-extra-tight);
|
padding-top: var(--sp-extra-tight);
|
||||||
padding-bottom: calc(2 * var(--sp-normal));
|
padding-bottom: calc(2 * var(--sp-normal));
|
||||||
|
|
||||||
|
& .people-selector {
|
||||||
|
padding: var(--sp-extra-tight);
|
||||||
|
border-radius: var(--bo-radius);
|
||||||
|
&__container {
|
||||||
|
@include dir.side(margin, var(--sp-extra-tight), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& .segmented-controls {
|
& .segmented-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: var(--sp-extra-tight);
|
margin-bottom: var(--sp-extra-tight);
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomSettings.scss';
|
import './RoomSettings.scss';
|
||||||
|
|
||||||
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import Tabs from '../../atoms/tabs/Tabs';
|
import Tabs from '../../atoms/tabs/Tabs';
|
||||||
@@ -21,19 +24,23 @@ import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
|||||||
import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility';
|
import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility';
|
||||||
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
|
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
|
||||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||||
|
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||||
|
|
||||||
|
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||||
|
import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg';
|
||||||
|
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
|
||||||
const tabText = {
|
const tabText = {
|
||||||
GENERAL: 'General',
|
GENERAL: 'General',
|
||||||
SEARCH: 'Search',
|
SEARCH: 'Search',
|
||||||
|
MEMBERS: 'Members',
|
||||||
PERMISSIONS: 'Permissions',
|
PERMISSIONS: 'Permissions',
|
||||||
SECURITY: 'Security',
|
SECURITY: 'Security',
|
||||||
};
|
};
|
||||||
@@ -46,6 +53,10 @@ const tabItems = [{
|
|||||||
iconSrc: SearchIC,
|
iconSrc: SearchIC,
|
||||||
text: tabText.SEARCH,
|
text: tabText.SEARCH,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
}, {
|
||||||
|
iconSrc: UserIC,
|
||||||
|
text: tabText.MEMBERS,
|
||||||
|
disabled: false,
|
||||||
}, {
|
}, {
|
||||||
iconSrc: ShieldUserIC,
|
iconSrc: ShieldUserIC,
|
||||||
text: tabText.PERMISSIONS,
|
text: tabText.PERMISSIONS,
|
||||||
@@ -64,6 +75,7 @@ function GeneralSettings({ roomId }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="room-settings__card">
|
<div className="room-settings__card">
|
||||||
|
<MenuHeader>Options</MenuHeader>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
onClick={() => openInviteUser(roomId)}
|
onClick={() => openInviteUser(roomId)}
|
||||||
@@ -124,6 +136,7 @@ SecuritySettings.propTypes = {
|
|||||||
function RoomSettings({ roomId }) {
|
function RoomSettings({ roomId }) {
|
||||||
const [, forceUpdate] = useForceUpdate();
|
const [, forceUpdate] = useForceUpdate();
|
||||||
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
|
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
|
||||||
|
const room = initMatrix.matrixClient.getRoom(roomId);
|
||||||
|
|
||||||
const handleTabChange = (tabItem) => {
|
const handleTabChange = (tabItem) => {
|
||||||
setSelectedTab(tabItem);
|
setSelectedTab(tabItem);
|
||||||
@@ -153,9 +166,20 @@ function RoomSettings({ roomId }) {
|
|||||||
<ScrollView autoHide>
|
<ScrollView autoHide>
|
||||||
<div className="room-settings__content">
|
<div className="room-settings__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<button
|
||||||
<Text variant="s1" weight="medium" primary>Room settings</Text>
|
className="room-settings__header-btn"
|
||||||
</TitleWrapper>
|
onClick={() => toggleRoomSettings()}
|
||||||
|
type="button"
|
||||||
|
onMouseUp={(e) => blurOnBubbling(e, '.room-settings__header-btn')}
|
||||||
|
>
|
||||||
|
<TitleWrapper>
|
||||||
|
<Text variant="s1" weight="medium" primary>
|
||||||
|
{`${room.name}`}
|
||||||
|
<span style={{ color: 'var(--tc-surface-low)' }}> — room settings</span>
|
||||||
|
</Text>
|
||||||
|
</TitleWrapper>
|
||||||
|
<RawIcon size="small" src={ChevronTopIC} />
|
||||||
|
</button>
|
||||||
</Header>
|
</Header>
|
||||||
<RoomProfile roomId={roomId} />
|
<RoomProfile roomId={roomId} />
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -166,6 +190,7 @@ function RoomSettings({ roomId }) {
|
|||||||
<div className="room-settings__cards-wrapper">
|
<div className="room-settings__cards-wrapper">
|
||||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||||
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
|
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
|
||||||
|
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||||
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
@use '../../partials/flex';
|
||||||
|
|
||||||
.room-settings {
|
.room-settings {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -6,6 +7,32 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .header {
|
||||||
|
padding: 0 var(--sp-extra-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-btn {
|
||||||
|
min-width: 0;
|
||||||
|
@extend .cp-fx__row--s-c;
|
||||||
|
@include dir.side(margin, 0, auto);
|
||||||
|
padding: var(--sp-ultra-tight) var(--sp-extra-tight);
|
||||||
|
border-radius: calc(var(--bo-radius) / 2);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (hover:hover) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
box-shadow: var(--bs-surface-outline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background-color: var(--bg-surface-active);
|
||||||
|
box-shadow: var(--bs-surface-outline);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
padding-bottom: calc(2 * var(--sp-extra-loose));
|
padding-bottom: calc(2 * var(--sp-extra-loose));
|
||||||
|
|
||||||
@@ -48,6 +75,7 @@
|
|||||||
|
|
||||||
.room-settings .room-permissions__card,
|
.room-settings .room-permissions__card,
|
||||||
.room-settings .room-search__form,
|
.room-settings .room-search__form,
|
||||||
.room-settings .room-search__result-item {
|
.room-settings .room-search__result-item ,
|
||||||
|
.room-settings .room-members {
|
||||||
@extend .room-settings__card;
|
@extend .room-settings__card;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,8 @@ import './RoomViewCmdBar.scss';
|
|||||||
import parse from 'html-react-parser';
|
import parse from 'html-react-parser';
|
||||||
import twemoji from 'twemoji';
|
import twemoji from 'twemoji';
|
||||||
|
|
||||||
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { toggleMarkdown } from '../../../client/action/settings';
|
import { toggleMarkdown } from '../../../client/action/settings';
|
||||||
import * as roomActions from '../../../client/action/room';
|
import * as roomActions from '../../../client/action/room';
|
||||||
@@ -124,9 +126,7 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
|||||||
result: emoji,
|
result: emoji,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{
|
<Text variant="b1">{renderEmoji(emoji)}</Text>
|
||||||
renderEmoji(emoji)
|
|
||||||
}
|
|
||||||
<Text variant="b2">{`:${emoji.shortcode}:`}</Text>
|
<Text variant="b2">{`:${emoji.shortcode}:`}</Text>
|
||||||
</CmdItem>
|
</CmdItem>
|
||||||
));
|
));
|
||||||
@@ -143,7 +143,7 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text variant="b2">{member.name}</Text>
|
<Text variant="b2">{twemojify(member.name)}</Text>
|
||||||
</CmdItem>
|
</CmdItem>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,20 +36,14 @@
|
|||||||
|
|
||||||
.cmd-item {
|
.cmd-item {
|
||||||
--cmd-item-bar: inset 0 -2px 0 0 var(--bg-caution);
|
--cmd-item-bar: inset 0 -2px 0 0 var(--bg-caution);
|
||||||
|
height: 100%;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||||
padding: 0 var(--sp-extra-tight);
|
padding: 0 var(--sp-extra-tight);
|
||||||
height: 100%;
|
|
||||||
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
& .emoji {
|
display: inline-flex;
|
||||||
width: 20px;
|
align-items: center;
|
||||||
height: 20px;
|
|
||||||
@include dir.side(margin, 0, var(--sp-ultra-tight));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-caution-hover);
|
background-color: var(--bg-caution-hover);
|
||||||
|
|||||||
@@ -7,16 +7,13 @@ import React, {
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewContent.scss';
|
import './RoomViewContent.scss';
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
import {
|
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
|
||||||
diffMinutes, isInSameDay, Throttle, getScrollInfo,
|
|
||||||
} from '../../../util/common';
|
|
||||||
|
|
||||||
import Divider from '../../atoms/divider/Divider';
|
import Divider from '../../atoms/divider/Divider';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
@@ -27,17 +24,15 @@ import TimelineChange from '../../molecules/message/TimelineChange';
|
|||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
import { parseTimelineChange } from './common';
|
import { parseTimelineChange } from './common';
|
||||||
|
import TimelineScroll from './TimelineScroll';
|
||||||
|
import EventLimit from './EventLimit';
|
||||||
|
|
||||||
const DEFAULT_MAX_EVENTS = 50;
|
|
||||||
const PAG_LIMIT = 30;
|
const PAG_LIMIT = 30;
|
||||||
const MAX_MSG_DIFF_MINUTES = 5;
|
const MAX_MSG_DIFF_MINUTES = 5;
|
||||||
const PLACEHOLDER_COUNT = 2;
|
const PLACEHOLDER_COUNT = 2;
|
||||||
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
||||||
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
||||||
|
|
||||||
const SMALLEST_MSG_HEIGHT = 32;
|
|
||||||
const PAGES_COUNT = 4;
|
|
||||||
|
|
||||||
function loadingMsgPlaceholders(key, count = 2) {
|
function loadingMsgPlaceholders(key, count = 2) {
|
||||||
const pl = [];
|
const pl = [];
|
||||||
const genPlaceholders = () => {
|
const genPlaceholders = () => {
|
||||||
@@ -124,178 +119,7 @@ function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus = false) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineScroll extends EventEmitter {
|
function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
|
||||||
constructor(target) {
|
|
||||||
super();
|
|
||||||
if (target === null) {
|
|
||||||
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
|
|
||||||
}
|
|
||||||
this.scroll = target;
|
|
||||||
|
|
||||||
this.backwards = false;
|
|
||||||
this.inTopHalf = false;
|
|
||||||
this.maxEvents = DEFAULT_MAX_EVENTS;
|
|
||||||
|
|
||||||
this.isScrollable = false;
|
|
||||||
this.top = 0;
|
|
||||||
this.bottom = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.viewHeight = 0;
|
|
||||||
|
|
||||||
this.topMsg = null;
|
|
||||||
this.bottomMsg = null;
|
|
||||||
this.diff = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, maxScrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// restore scroll using previous calc by this._updateTopBottomMsg() and this._calcDiff.
|
|
||||||
tryRestoringScroll() {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
|
|
||||||
let scrollTop = 0;
|
|
||||||
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
|
|
||||||
if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
|
|
||||||
else scrollTop = ot - this.diff;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, scrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToIndex(index, offset = 0) {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
|
||||||
const offsetTop = msgs[index]?.offsetTop;
|
|
||||||
|
|
||||||
if (offsetTop === undefined) return;
|
|
||||||
// if msg is already in visible are we don't need to scroll to that
|
|
||||||
if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
|
|
||||||
const to = offsetTop - offset;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
_scrollTo(scrollInfo, scrollTop) {
|
|
||||||
this.scroll.scrollTop = scrollTop;
|
|
||||||
|
|
||||||
// browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
|
|
||||||
// so here we flag that the upcoming 'onscroll' event is
|
|
||||||
// emitted as side effect of assigning 'this.scroll.scrollTop' above
|
|
||||||
// only if it's changes.
|
|
||||||
// by doing so we prevent this._updateCalc() from calc again.
|
|
||||||
if (scrollTop !== this.top) {
|
|
||||||
this.scrolledByCode = true;
|
|
||||||
}
|
|
||||||
const sInfo = { ...scrollInfo };
|
|
||||||
|
|
||||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
|
||||||
|
|
||||||
sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
|
|
||||||
this._updateCalc(sInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we maintain reference of top and bottom messages
|
|
||||||
// to restore the scroll position when
|
|
||||||
// messages gets removed from either end and added to other.
|
|
||||||
_updateTopBottomMsg() {
|
|
||||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
|
||||||
const lMsgIndex = msgs.length - 1;
|
|
||||||
|
|
||||||
this.topMsg = msgs[0]?.className === 'ph-msg'
|
|
||||||
? msgs[PLACEHOLDER_COUNT]
|
|
||||||
: msgs[0];
|
|
||||||
this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
|
|
||||||
? msgs[lMsgIndex - PLACEHOLDER_COUNT]
|
|
||||||
: msgs[lMsgIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
// we calculate the difference between first/last message and current scrollTop.
|
|
||||||
// if we are going above we calc diff between first and scrollTop
|
|
||||||
// else otherwise.
|
|
||||||
// NOTE: This will help to restore the scroll when msgs get's removed
|
|
||||||
// from one end and added to other end
|
|
||||||
_calcDiff(scrollInfo) {
|
|
||||||
if (!this.topMsg || !this.bottomMsg) return 0;
|
|
||||||
if (this.inTopHalf) {
|
|
||||||
return this.topMsg.offsetTop - scrollInfo.top;
|
|
||||||
}
|
|
||||||
return this.bottomMsg.offsetTop - scrollInfo.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_calcMaxEvents(scrollInfo) {
|
|
||||||
return Math.round(scrollInfo.viewHeight / SMALLEST_MSG_HEIGHT) * PAGES_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateCalc(scrollInfo) {
|
|
||||||
const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
|
|
||||||
const scrollMiddle = scrollInfo.top + halfViewHeight;
|
|
||||||
const lastMiddle = this.top + halfViewHeight;
|
|
||||||
|
|
||||||
this.backwards = scrollMiddle < lastMiddle;
|
|
||||||
this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
|
|
||||||
|
|
||||||
this.isScrollable = scrollInfo.isScrollable;
|
|
||||||
this.top = scrollInfo.top;
|
|
||||||
this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
|
|
||||||
this.height = scrollInfo.height;
|
|
||||||
|
|
||||||
// only calculate maxEvents if viewHeight change
|
|
||||||
if (this.viewHeight !== scrollInfo.viewHeight) {
|
|
||||||
this.maxEvents = this._calcMaxEvents(scrollInfo);
|
|
||||||
this.viewHeight = scrollInfo.viewHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateTopBottomMsg();
|
|
||||||
this.diff = this._calcDiff(scrollInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
calcScroll() {
|
|
||||||
if (this.scrolledByCode) {
|
|
||||||
this.scrolledByCode = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
this._updateCalc(scrollInfo);
|
|
||||||
|
|
||||||
this.emit('scroll', this.backwards);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let timelineScroll = null;
|
|
||||||
let jumpToItemIndex = -1;
|
|
||||||
const throttle = new Throttle();
|
|
||||||
const limit = {
|
|
||||||
from: 0,
|
|
||||||
getMaxEvents() {
|
|
||||||
return timelineScroll?.maxEvents ?? DEFAULT_MAX_EVENTS;
|
|
||||||
},
|
|
||||||
getEndIndex() {
|
|
||||||
return this.from + this.getMaxEvents();
|
|
||||||
},
|
|
||||||
calcNextFrom(backwards, tLength) {
|
|
||||||
let newFrom = backwards ? this.from - PAG_LIMIT : this.from + PAG_LIMIT;
|
|
||||||
if (!backwards && newFrom + this.getMaxEvents() > tLength) {
|
|
||||||
newFrom = tLength - this.getMaxEvents();
|
|
||||||
}
|
|
||||||
if (newFrom < 0) newFrom = 0;
|
|
||||||
return newFrom;
|
|
||||||
},
|
|
||||||
setFrom(from) {
|
|
||||||
if (from < 0) {
|
|
||||||
this.from = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.from = from;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function useTimeline(roomTimeline, eventId, readEventStore) {
|
|
||||||
const [timelineInfo, setTimelineInfo] = useState(null);
|
const [timelineInfo, setTimelineInfo] = useState(null);
|
||||||
|
|
||||||
const setEventTimeline = async (eId) => {
|
const setEventTimeline = async (eId) => {
|
||||||
@@ -309,6 +133,7 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
const initTimeline = (eId) => {
|
const initTimeline = (eId) => {
|
||||||
// NOTICE: eId can be id of readUpto, reply or specific event.
|
// NOTICE: eId can be id of readUpto, reply or specific event.
|
||||||
// readUpTo: when user click jump to unread message button.
|
// readUpTo: when user click jump to unread message button.
|
||||||
@@ -320,20 +145,20 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
|||||||
|
|
||||||
if (isSpecificEvent) {
|
if (isSpecificEvent) {
|
||||||
focusEventIndex = roomTimeline.getEventIndex(eId);
|
focusEventIndex = roomTimeline.getEventIndex(eId);
|
||||||
} else if (!readEventStore.getItem()) {
|
} else if (!readUptoEvtStore.getItem()) {
|
||||||
// either opening live timeline or jump to unread.
|
// either opening live timeline or jump to unread.
|
||||||
focusEventIndex = roomTimeline.getUnreadEventIndex(readUpToId);
|
focusEventIndex = roomTimeline.getUnreadEventIndex(readUpToId);
|
||||||
if (roomTimeline.hasEventInTimeline(readUpToId)) {
|
if (roomTimeline.hasEventInTimeline(readUpToId)) {
|
||||||
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
focusEventIndex = roomTimeline.getUnreadEventIndex(readEventStore.getItem().getId());
|
focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focusEventIndex > -1) {
|
if (focusEventIndex > -1) {
|
||||||
limit.setFrom(focusEventIndex - Math.round(limit.getMaxEvents() / 2));
|
limit.setFrom(focusEventIndex - Math.round(limit.maxEvents / 2));
|
||||||
} else {
|
} else {
|
||||||
limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents());
|
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||||
}
|
}
|
||||||
setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
|
setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
|
||||||
};
|
};
|
||||||
@@ -350,36 +175,45 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
|||||||
return timelineInfo;
|
return timelineInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
function usePaginate(
|
||||||
|
roomTimeline,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
) {
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOnPagination = (backwards, loaded) => {
|
const handlePaginatedFromServer = (backwards, loaded) => {
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
if (loaded === 0) return;
|
if (loaded === 0) return;
|
||||||
if (!readEventStore.getItem()) {
|
if (!readUptoEvtStore.getItem()) {
|
||||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||||
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
}
|
}
|
||||||
limit.setFrom(limit.calcNextFrom(backwards, roomTimeline.timeline.length));
|
limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
|
||||||
setTimeout(() => setInfo({
|
setTimeout(() => setInfo({
|
||||||
backwards,
|
backwards,
|
||||||
loaded,
|
loaded,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handleOnPagination);
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handleOnPagination);
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const autoPaginate = useCallback(async () => {
|
const autoPaginate = useCallback(async () => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
if (roomTimeline.isOngoingPagination) return;
|
if (roomTimeline.isOngoingPagination) return;
|
||||||
const tLength = roomTimeline.timeline.length;
|
const tLength = roomTimeline.timeline.length;
|
||||||
|
|
||||||
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
||||||
if (limit.getEndIndex() < tLength) {
|
if (limit.length < tLength) {
|
||||||
// paginate from memory
|
// paginate from memory
|
||||||
limit.setFrom(limit.calcNextFrom(false, tLength));
|
limit.paginate(false, PAG_LIMIT, tLength);
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
} else if (roomTimeline.canPaginateForward()) {
|
} else if (roomTimeline.canPaginateForward()) {
|
||||||
// paginate from server.
|
// paginate from server.
|
||||||
@@ -390,7 +224,7 @@ function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
|||||||
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
||||||
if (limit.from > 0) {
|
if (limit.from > 0) {
|
||||||
// paginate from memory
|
// paginate from memory
|
||||||
limit.setFrom(limit.calcNextFrom(true, tLength));
|
limit.paginate(true, PAG_LIMIT, tLength);
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
} else if (roomTimeline.canPaginateBackward()) {
|
} else if (roomTimeline.canPaginateBackward()) {
|
||||||
// paginate from server.
|
// paginate from server.
|
||||||
@@ -402,16 +236,25 @@ function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
|||||||
return [info, autoPaginate];
|
return [info, autoPaginate];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdateLimit) {
|
function useHandleScroll(
|
||||||
|
roomTimeline,
|
||||||
|
autoPaginate,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
) {
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// emit event to toggle scrollToBottom button visibility
|
// emit event to toggle scrollToBottom button visibility
|
||||||
const isAtBottom = (
|
const isAtBottom = (
|
||||||
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
||||||
&& limit.getEndIndex() >= roomTimeline.timeline.length
|
&& limit.length >= roomTimeline.timeline.length
|
||||||
);
|
);
|
||||||
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
||||||
if (isAtBottom && readEventStore.getItem()) {
|
if (isAtBottom && readUptoEvtStore.getItem()) {
|
||||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -419,11 +262,13 @@ function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdate
|
|||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const handleScrollToLive = useCallback(() => {
|
const handleScrollToLive = useCallback(() => {
|
||||||
if (readEventStore.getItem()) {
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
|
if (readUptoEvtStore.getItem()) {
|
||||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
||||||
}
|
}
|
||||||
if (roomTimeline.isServingLiveTimeline()) {
|
if (roomTimeline.isServingLiveTimeline()) {
|
||||||
limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents());
|
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||||
timelineScroll.scrollToBottom();
|
timelineScroll.scrollToBottom();
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
return;
|
return;
|
||||||
@@ -434,29 +279,32 @@ function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdate
|
|||||||
return [handleScroll, handleScrollToLive];
|
return [handleScroll, handleScrollToLive];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEventArrive(roomTimeline, readEventStore) {
|
function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef) {
|
||||||
const myUserId = initMatrix.matrixClient.getUserId();
|
const myUserId = initMatrix.matrixClient.getUserId();
|
||||||
const [newEvent, setEvent] = useState(null);
|
const [newEvent, setEvent] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
const sendReadReceipt = (event) => {
|
const sendReadReceipt = (event) => {
|
||||||
if (event.isSending()) return;
|
if (event.isSending()) return;
|
||||||
if (myUserId === event.getSender()) {
|
if (myUserId === event.getSender()) {
|
||||||
roomTimeline.markAllAsRead();
|
roomTimeline.markAllAsRead();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const readUpToEvent = readEventStore.getItem();
|
const readUpToEvent = readUptoEvtStore.getItem();
|
||||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||||
|
const isUnread = readUpToEvent?.getId() === readUpToId;
|
||||||
|
|
||||||
// if user doesn't have focus on app don't mark messages as read.
|
// if user doesn't have focus on app don't mark messages as read.
|
||||||
if (document.visibilityState === 'hidden' || timelineScroll.bottom >= 16) {
|
if (document.visibilityState === 'hidden' || timelineScroll.bottom >= 16) {
|
||||||
if (readUpToEvent === readUpToId) return;
|
if (isUnread) return;
|
||||||
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// user has not mark room as read
|
// user has not mark room as read
|
||||||
const isUnreadMsg = readUpToEvent?.getId() === readUpToId;
|
if (!isUnread) {
|
||||||
if (!isUnreadMsg) {
|
|
||||||
roomTimeline.markAllAsRead();
|
roomTimeline.markAllAsRead();
|
||||||
}
|
}
|
||||||
const { timeline } = roomTimeline;
|
const { timeline } = roomTimeline;
|
||||||
@@ -470,11 +318,11 @@ function useEventArrive(roomTimeline, readEventStore) {
|
|||||||
const tLength = roomTimeline.timeline.length;
|
const tLength = roomTimeline.timeline.length;
|
||||||
const isUserViewingLive = (
|
const isUserViewingLive = (
|
||||||
roomTimeline.isServingLiveTimeline()
|
roomTimeline.isServingLiveTimeline()
|
||||||
&& limit.getEndIndex() >= tLength - 1
|
&& limit.length >= tLength - 1
|
||||||
&& timelineScroll.bottom < SCROLL_TRIGGER_POS
|
&& timelineScroll.bottom < SCROLL_TRIGGER_POS
|
||||||
);
|
);
|
||||||
if (isUserViewingLive) {
|
if (isUserViewingLive) {
|
||||||
limit.setFrom(tLength - limit.getMaxEvents());
|
limit.setFrom(tLength - limit.maxEvents);
|
||||||
sendReadReceipt(event);
|
sendReadReceipt(event);
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
return;
|
return;
|
||||||
@@ -486,7 +334,7 @@ function useEventArrive(roomTimeline, readEventStore) {
|
|||||||
}
|
}
|
||||||
const isUserDitchedLive = (
|
const isUserDitchedLive = (
|
||||||
roomTimeline.isServingLiveTimeline()
|
roomTimeline.isServingLiveTimeline()
|
||||||
&& limit.getEndIndex() >= tLength - 1
|
&& limit.length >= tLength - 1
|
||||||
);
|
);
|
||||||
if (isUserDitchedLive) {
|
if (isUserDitchedLive) {
|
||||||
// This stateUpdate will help to put the
|
// This stateUpdate will help to put the
|
||||||
@@ -506,6 +354,7 @@ function useEventArrive(roomTimeline, readEventStore) {
|
|||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
if (timelineScroll.bottom < 16
|
if (timelineScroll.bottom < 16
|
||||||
&& !roomTimeline.canPaginateForward()
|
&& !roomTimeline.canPaginateForward()
|
||||||
@@ -517,27 +366,49 @@ function useEventArrive(roomTimeline, readEventStore) {
|
|||||||
}, [newEvent, roomTimeline]);
|
}, [newEvent, roomTimeline]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let jumpToItemIndex = -1;
|
||||||
|
|
||||||
function RoomViewContent({ eventId, roomTimeline }) {
|
function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
|
const [throttle] = useState(new Throttle());
|
||||||
|
|
||||||
const timelineSVRef = useRef(null);
|
const timelineSVRef = useRef(null);
|
||||||
const readEventStore = useStore(roomTimeline);
|
const timelineScrollRef = useRef(null);
|
||||||
const timelineInfo = useTimeline(roomTimeline, eventId, readEventStore);
|
const eventLimitRef = useRef(null);
|
||||||
|
|
||||||
|
const readUptoEvtStore = useStore(roomTimeline);
|
||||||
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
||||||
const [paginateInfo, autoPaginate] = usePaginate(roomTimeline, readEventStore, forceUpdateLimit);
|
|
||||||
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
const timelineInfo = useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef);
|
||||||
roomTimeline, autoPaginate, readEventStore, forceUpdateLimit,
|
const [paginateInfo, autoPaginate] = usePaginate(
|
||||||
|
roomTimeline,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
);
|
);
|
||||||
useEventArrive(roomTimeline, readEventStore);
|
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
||||||
|
roomTimeline,
|
||||||
|
autoPaginate,
|
||||||
|
readUptoEvtStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
);
|
||||||
|
useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
|
||||||
|
|
||||||
const { timeline } = roomTimeline;
|
const { timeline } = roomTimeline;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!roomTimeline.initialized) {
|
if (!roomTimeline.initialized) {
|
||||||
timelineScroll = new TimelineScroll(timelineSVRef.current);
|
timelineScrollRef.current = new TimelineScroll(timelineSVRef.current);
|
||||||
|
eventLimitRef.current = new EventLimit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// when active timeline changes
|
// when active timeline changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return undefined;
|
if (!roomTimeline.initialized) return undefined;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
|
||||||
if (timeline.length > 0) {
|
if (timeline.length > 0) {
|
||||||
if (jumpToItemIndex === -1) {
|
if (jumpToItemIndex === -1) {
|
||||||
@@ -547,7 +418,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
}
|
}
|
||||||
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
|
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
|
||||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||||
if (readEventStore.getItem()?.getId() === readUpToId || readUpToId === null) {
|
if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
|
||||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -555,11 +426,9 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
}
|
}
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
|
|
||||||
timelineScroll.on('scroll', handleScroll);
|
|
||||||
roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||||
return () => {
|
return () => {
|
||||||
if (timelineSVRef.current === null) return;
|
if (timelineSVRef.current === null) return;
|
||||||
timelineScroll.removeListener('scroll', handleScroll);
|
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||||
};
|
};
|
||||||
}, [timelineInfo]);
|
}, [timelineInfo]);
|
||||||
@@ -567,6 +436,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
// when paginating from server
|
// when paginating from server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
}, [paginateInfo]);
|
}, [paginateInfo]);
|
||||||
@@ -574,28 +444,35 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
// when paginating locally
|
// when paginating locally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
}, [onLimitUpdate]);
|
}, [onLimitUpdate]);
|
||||||
|
|
||||||
const handleTimelineScroll = (event) => {
|
const handleTimelineScroll = (event) => {
|
||||||
const { target } = event;
|
const timelineScroll = timelineScrollRef.current;
|
||||||
if (!target) return;
|
if (!event.target) return;
|
||||||
throttle._(() => timelineScroll?.calcScroll(), 400)(target);
|
|
||||||
|
throttle._(() => {
|
||||||
|
const backwards = timelineScroll?.calcScroll();
|
||||||
|
if (typeof backwards !== 'boolean') return;
|
||||||
|
handleScroll(backwards);
|
||||||
|
}, 200)();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTimeline = () => {
|
const renderTimeline = () => {
|
||||||
const tl = [];
|
const tl = [];
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
|
|
||||||
let itemCountIndex = 0;
|
let itemCountIndex = 0;
|
||||||
jumpToItemIndex = -1;
|
jumpToItemIndex = -1;
|
||||||
const readEvent = readEventStore.getItem();
|
const readUptoEvent = readUptoEvtStore.getItem();
|
||||||
let unreadDivider = false;
|
let unreadDivider = false;
|
||||||
|
|
||||||
if (roomTimeline.canPaginateBackward() || limit.from > 0) {
|
if (roomTimeline.canPaginateBackward() || limit.from > 0) {
|
||||||
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
||||||
itemCountIndex += PLACEHOLDER_COUNT;
|
itemCountIndex += PLACEHOLDER_COUNT;
|
||||||
}
|
}
|
||||||
for (let i = limit.from; i < limit.getEndIndex(); i += 1) {
|
for (let i = limit.from; i < limit.length; i += 1) {
|
||||||
if (i >= timeline.length) break;
|
if (i >= timeline.length) break;
|
||||||
const mEvent = timeline[i];
|
const mEvent = timeline[i];
|
||||||
const prevMEvent = timeline[i - 1] ?? null;
|
const prevMEvent = timeline[i - 1] ?? null;
|
||||||
@@ -614,9 +491,9 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
|
|
||||||
let isNewEvent = false;
|
let isNewEvent = false;
|
||||||
if (!unreadDivider) {
|
if (!unreadDivider) {
|
||||||
unreadDivider = (readEvent
|
unreadDivider = (readUptoEvent
|
||||||
&& prevMEvent?.getTs() <= readEvent.getTs()
|
&& prevMEvent?.getTs() <= readUptoEvent.getTs()
|
||||||
&& readEvent.getTs() < mEvent.getTs());
|
&& readUptoEvent.getTs() < mEvent.getTs());
|
||||||
if (unreadDivider) {
|
if (unreadDivider) {
|
||||||
isNewEvent = true;
|
isNewEvent = true;
|
||||||
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
|
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
|
||||||
@@ -637,7 +514,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
|||||||
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
|
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
|
||||||
itemCountIndex += 1;
|
itemCountIndex += 1;
|
||||||
}
|
}
|
||||||
if (roomTimeline.canPaginateForward() || limit.getEndIndex() < timeline.length) {
|
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
|
||||||
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ function RoomViewInput({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
settings.on(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
settings.on(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
||||||
|
roomsInput.on(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
|
||||||
viewEvent.on('focus_msg_input', requestFocusInput);
|
viewEvent.on('focus_msg_input', requestFocusInput);
|
||||||
return () => {
|
return () => {
|
||||||
settings.removeListener(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
settings.removeListener(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
||||||
|
roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
|
||||||
viewEvent.removeListener('focus_msg_input', requestFocusInput);
|
viewEvent.removeListener('focus_msg_input', requestFocusInput);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -301,12 +303,18 @@ function RoomViewInput({
|
|||||||
if (file !== null) roomsInput.setAttachment(roomId, file);
|
if (file !== null) roomsInput.setAttachment(roomId, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canISend = roomTimeline.room.currentState.maySendMessage(mx.getUserId());
|
|
||||||
|
|
||||||
function renderInputs() {
|
function renderInputs() {
|
||||||
if (!canISend) {
|
const canISend = roomTimeline.room.currentState.maySendMessage(mx.getUserId());
|
||||||
|
const tombstoneEvent = roomTimeline.room.currentState.getStateEvents('m.room.tombstone')[0];
|
||||||
|
if (!canISend || tombstoneEvent) {
|
||||||
return (
|
return (
|
||||||
<Text className="room-input__alert">You do not have permission to post to this room</Text>
|
<Text className="room-input__alert">
|
||||||
|
{
|
||||||
|
tombstoneEvent
|
||||||
|
? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.'
|
||||||
|
: 'You do not have permission to post to this room'
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
136
src/app/organisms/room/TimelineScroll.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { getScrollInfo } from '../../../util/common';
|
||||||
|
|
||||||
|
class TimelineScroll {
|
||||||
|
constructor(target) {
|
||||||
|
if (target === null) {
|
||||||
|
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
|
||||||
|
}
|
||||||
|
this.scroll = target;
|
||||||
|
|
||||||
|
this.backwards = false;
|
||||||
|
this.inTopHalf = false;
|
||||||
|
|
||||||
|
this.isScrollable = false;
|
||||||
|
this.top = 0;
|
||||||
|
this.bottom = 0;
|
||||||
|
this.height = 0;
|
||||||
|
this.viewHeight = 0;
|
||||||
|
|
||||||
|
this.topMsg = null;
|
||||||
|
this.bottomMsg = null;
|
||||||
|
this.diff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
||||||
|
|
||||||
|
this._scrollTo(scrollInfo, maxScrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use previous calc by this._updateTopBottomMsg() & this._calcDiff.
|
||||||
|
tryRestoringScroll() {
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
|
||||||
|
let scrollTop = 0;
|
||||||
|
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
|
||||||
|
if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
|
||||||
|
else scrollTop = ot - this.diff;
|
||||||
|
|
||||||
|
this._scrollTo(scrollInfo, scrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToIndex(index, offset = 0) {
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
||||||
|
const offsetTop = msgs[index]?.offsetTop;
|
||||||
|
|
||||||
|
if (offsetTop === undefined) return;
|
||||||
|
// if msg is already in visible are we don't need to scroll to that
|
||||||
|
if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
|
||||||
|
const to = offsetTop - offset;
|
||||||
|
|
||||||
|
this._scrollTo(scrollInfo, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollTo(scrollInfo, scrollTop) {
|
||||||
|
this.scroll.scrollTop = scrollTop;
|
||||||
|
|
||||||
|
// browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
|
||||||
|
// so here we flag that the upcoming 'onscroll' event is
|
||||||
|
// emitted as side effect of assigning 'this.scroll.scrollTop' above
|
||||||
|
// only if it's changes.
|
||||||
|
// by doing so we prevent this._updateCalc() from calc again.
|
||||||
|
if (scrollTop !== this.top) {
|
||||||
|
this.scrolledByCode = true;
|
||||||
|
}
|
||||||
|
const sInfo = { ...scrollInfo };
|
||||||
|
|
||||||
|
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
||||||
|
|
||||||
|
sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
|
||||||
|
this._updateCalc(sInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we maintain reference of top and bottom messages
|
||||||
|
// to restore the scroll position when
|
||||||
|
// messages gets removed from either end and added to other.
|
||||||
|
_updateTopBottomMsg() {
|
||||||
|
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
||||||
|
const lMsgIndex = msgs.length - 1;
|
||||||
|
|
||||||
|
// TODO: classname 'ph-msg' prevent this class from being used
|
||||||
|
const PLACEHOLDER_COUNT = 2;
|
||||||
|
this.topMsg = msgs[0]?.className === 'ph-msg'
|
||||||
|
? msgs[PLACEHOLDER_COUNT]
|
||||||
|
: msgs[0];
|
||||||
|
this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
|
||||||
|
? msgs[lMsgIndex - PLACEHOLDER_COUNT]
|
||||||
|
: msgs[lMsgIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// we calculate the difference between first/last message and current scrollTop.
|
||||||
|
// if we are going above we calc diff between first and scrollTop
|
||||||
|
// else otherwise.
|
||||||
|
// NOTE: This will help to restore the scroll when msgs get's removed
|
||||||
|
// from one end and added to other end
|
||||||
|
_calcDiff(scrollInfo) {
|
||||||
|
if (!this.topMsg || !this.bottomMsg) return 0;
|
||||||
|
if (this.inTopHalf) {
|
||||||
|
return this.topMsg.offsetTop - scrollInfo.top;
|
||||||
|
}
|
||||||
|
return this.bottomMsg.offsetTop - scrollInfo.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateCalc(scrollInfo) {
|
||||||
|
const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
|
||||||
|
const scrollMiddle = scrollInfo.top + halfViewHeight;
|
||||||
|
const lastMiddle = this.top + halfViewHeight;
|
||||||
|
|
||||||
|
this.backwards = scrollMiddle < lastMiddle;
|
||||||
|
this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
|
||||||
|
|
||||||
|
this.isScrollable = scrollInfo.isScrollable;
|
||||||
|
this.top = scrollInfo.top;
|
||||||
|
this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
|
||||||
|
this.height = scrollInfo.height;
|
||||||
|
this.viewHeight = scrollInfo.viewHeight;
|
||||||
|
|
||||||
|
this._updateTopBottomMsg();
|
||||||
|
this.diff = this._calcDiff(scrollInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
calcScroll() {
|
||||||
|
if (this.scrolledByCode) {
|
||||||
|
this.scrolledByCode = false;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
this._updateCalc(scrollInfo);
|
||||||
|
|
||||||
|
return this.backwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimelineScroll;
|
||||||
@@ -87,7 +87,7 @@ function getTimelineJSXMessages() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{twemojify(user)}</b>
|
||||||
{' set the avatar'}
|
{' set a avatar'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -95,7 +95,7 @@ function getTimelineJSXMessages() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{twemojify(user)}</b>
|
||||||
{' changed the avatar'}
|
{' changed their avatar'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -103,7 +103,7 @@ function getTimelineJSXMessages() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{twemojify(user)}</b>
|
||||||
{' removed the avatar'}
|
{' removed their avatar'}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -111,7 +111,7 @@ function getTimelineJSXMessages() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{twemojify(user)}</b>
|
||||||
{' set the display name to '}
|
{' set display name to '}
|
||||||
<b>{twemojify(newName)}</b>
|
<b>{twemojify(newName)}</b>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -120,7 +120,7 @@ function getTimelineJSXMessages() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{twemojify(user)}</b>
|
||||||
{' changed the display name to '}
|
{' changed their display name to '}
|
||||||
<b>{twemojify(newName)}</b>
|
<b>{twemojify(newName)}</b>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -129,7 +129,7 @@ function getTimelineJSXMessages() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>{twemojify(user)}</b>
|
<b>{twemojify(user)}</b>
|
||||||
{' removed the display name '}
|
{' removed their display name '}
|
||||||
<b>{twemojify(lastName)}</b>
|
<b>{twemojify(lastName)}</b>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import cons from '../../../client/state/cons';
|
|||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import AsyncSearch from '../../../util/AsyncSearch';
|
import AsyncSearch from '../../../util/AsyncSearch';
|
||||||
import { selectRoom, selectTab } from '../../../client/action/navigation';
|
import { selectRoom, selectTab } from '../../../client/action/navigation';
|
||||||
|
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
@@ -16,12 +17,6 @@ import ScrollView from '../../atoms/scroll/ScrollView';
|
|||||||
import RoomSelector from '../../molecules/room-selector/RoomSelector';
|
import RoomSelector from '../../molecules/room-selector/RoomSelector';
|
||||||
|
|
||||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
|
||||||
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
|
|
||||||
import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg';
|
|
||||||
import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
|
|
||||||
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
|
|
||||||
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
|
|
||||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
function useVisiblityToggle(setResult) {
|
function useVisiblityToggle(setResult) {
|
||||||
@@ -183,12 +178,7 @@ function Search() {
|
|||||||
if (item.type === 'direct') {
|
if (item.type === 'direct') {
|
||||||
imageSrc = item.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
imageSrc = item.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||||
} else {
|
} else {
|
||||||
const joinRuleToIconSrc = (joinRule) => ({
|
iconSrc = joinRuleToIconSrc(item.room.getJoinRule(), item.type === 'space');
|
||||||
restricted: () => (item.type === 'space' ? SpaceIC : HashIC),
|
|
||||||
invite: () => (item.type === 'space' ? SpaceLockIC : HashLockIC),
|
|
||||||
public: () => (item.type === 'space' ? SpaceGlobeIC : HashGlobeIC),
|
|
||||||
}[joinRule]?.() || null);
|
|
||||||
iconSrc = joinRuleToIconSrc(item.room.getJoinRule());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUnread = notifs.hasNoti(item.roomId);
|
const isUnread = notifs.hasNoti(item.roomId);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
padding: var(--sp-normal);
|
padding: var(--sp-normal);
|
||||||
|
|||||||