Compare commits
39 commits
db4ac6a2c2
...
384ab7e737
Author | SHA1 | Date | |
---|---|---|---|
384ab7e737 | |||
|
e54bb2e423 | ||
|
5058136737 | ||
|
74dc76e22e | ||
|
44161c4157 | ||
|
e8d04c0603 | ||
|
96415a8d2a | ||
|
2157f9a322 | ||
|
b387370aaf | ||
|
3110505b21 | ||
|
da536c8c3f | ||
|
98a378ad8a | ||
|
ab73225f00 | ||
|
cc4c222975 | ||
|
a32c8bf228 | ||
|
e6d6b0349e | ||
|
e2228a18c1 | ||
|
e046c59f7c | ||
|
fbe27d69c0 | ||
|
021a2c0e2e | ||
|
c243b6104c | ||
|
a1a822c5b6 | ||
|
c4abe39375 | ||
|
c52c4f7d32 | ||
|
653ddd9f11 | ||
|
e854b88394 | ||
|
66478143df | ||
|
4b461f87ff | ||
|
fc2b5744f4 | ||
|
65ad070878 | ||
|
f1668999a5 | ||
|
9db81d1913 | ||
|
7c795b800d | ||
|
e058a9ae6c | ||
|
4f09e6bbb5 | ||
|
60e022035f | ||
|
7a3e8dba92 | ||
|
c4615bd256 | ||
|
b6157707db |
267 changed files with 3457 additions and 16814 deletions
|
@ -61,4 +61,12 @@ module.exports = {
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
"@typescript-eslint/no-shadow": "error"
|
"@typescript-eslint/no-shadow": "error"
|
||||||
},
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.ts'],
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
8
.github/workflows/build-pull-request.yml
vendored
8
.github/workflows/build-pull-request.yml
vendored
|
@ -12,9 +12,9 @@ jobs:
|
||||||
PR_NUMBER: ${{github.event.number}}
|
PR_NUMBER: ${{github.event.number}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: 20.12.2
|
node-version: 20.12.2
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
@ -25,7 +25,7 @@ jobs:
|
||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.4
|
||||||
with:
|
with:
|
||||||
name: preview
|
name: preview
|
||||||
path: dist
|
path: dist
|
||||||
|
@ -33,7 +33,7 @@ jobs:
|
||||||
- name: Save pr number
|
- name: Save pr number
|
||||||
run: echo ${PR_NUMBER} > ./pr.txt
|
run: echo ${PR_NUMBER} > ./pr.txt
|
||||||
- name: Upload pr number
|
- name: Upload pr number
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.4
|
||||||
with:
|
with:
|
||||||
name: pr
|
name: pr
|
||||||
path: ./pr.txt
|
path: ./pr.txt
|
||||||
|
|
4
.github/workflows/deploy-pull-request.yml
vendored
4
.github/workflows/deploy-pull-request.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download pr number
|
- name: Download pr number
|
||||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe
|
uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11
|
||||||
with:
|
with:
|
||||||
workflow: ${{ github.event.workflow.id }}
|
workflow: ${{ github.event.workflow.id }}
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
|
@ -24,7 +24,7 @@ jobs:
|
||||||
id: pr
|
id: pr
|
||||||
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
|
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe
|
uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11
|
||||||
with:
|
with:
|
||||||
workflow: ${{ github.event.workflow.id }}
|
workflow: ${{ github.event.workflow.id }}
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
|
|
4
.github/workflows/docker-pr.yml
vendored
4
.github/workflows/docker-pr.yml
vendored
|
@ -11,9 +11,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v5.3.0
|
uses: docker/build-push-action@v6.5.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
|
|
2
.github/workflows/lockfile.yml
vendored
2
.github/workflows/lockfile.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.7
|
||||||
- name: NPM Lockfile Changes
|
- name: NPM Lockfile Changes
|
||||||
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
|
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
|
||||||
with:
|
with:
|
||||||
|
|
4
.github/workflows/netlify-dev.yml
vendored
4
.github/workflows/netlify-dev.yml
vendored
|
@ -11,9 +11,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: 20.12.2
|
node-version: 20.12.2
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
18
.github/workflows/prod-deploy.yml
vendored
18
.github/workflows/prod-deploy.yml
vendored
|
@ -10,9 +10,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.3
|
||||||
with:
|
with:
|
||||||
node-version: 20.12.2
|
node-version: 20.12.2
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
@ -52,7 +52,7 @@ jobs:
|
||||||
gpg --export | xxd -p
|
gpg --export | xxd -p
|
||||||
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
- name: Upload tagged release
|
- name: Upload tagged release
|
||||||
uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87
|
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||||
|
@ -66,18 +66,18 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.2.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.3.0
|
uses: docker/setup-buildx-action@v3.5.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3.2.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Login to the Container registry
|
- name: Login to the Container registry
|
||||||
uses: docker/login-action@v3.2.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
@ -90,7 +90,7 @@ jobs:
|
||||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5.3.0
|
uses: docker/build-push-action@v6.5.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
|
@ -14,6 +14,7 @@ RUN npm run build
|
||||||
FROM nginx:1.27.0-alpine
|
FROM nginx:1.27.0-alpine
|
||||||
|
|
||||||
COPY --from=builder /src/dist /app
|
COPY --from=builder /src/dist /app
|
||||||
|
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
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
|
||||||
|
|
42
README.md
42
README.md
|
@ -51,16 +51,16 @@ Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
|
||||||
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
|
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
|
||||||
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
|
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
|
||||||
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
|
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
|
||||||
AdQEEwEIAD4WIQSRri2MHidaaZv+vvuUMwx6UK/M8wUCYnD+DQIbAwUJA8JnAAUL
|
AdQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQSRri2MHidaaZv+
|
||||||
CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCUMwx6UK/M88ApC/9HAdbum1lYBC0s
|
vvuUMwx6UK/M8wUCZqEDwAUJFvwIswAKCRCUMwx6UK/M877qC/4lxXOQIoWnLLkK
|
||||||
1k7GwP2A7B4sQtBWjy771BzybWlHeaeG+BGJwg4YiuowXZMm5dubFJFoI/CfeY07
|
YiRCTkGsH6NdxgeYr6wpXT4xuQ45ZxCytwHpOGQmO/5up5961TxWW8D1frRIJHjj
|
||||||
B5aK40/bmT6Xcfkp0VA74c1wUpubBUEJN7tH5HG/OGd9BKeq9E/HHtVaJLVT1k3w
|
AZGoRCL3EKEuY8nt3D99fpf3DvZrs1uoVAhiyn737hRlZAg+QsJheeGCmdSJ0hX5
|
||||||
Rhv9VuHO6nR30EEp7IDthftotl5S4lio3+W0pKk4TAKV8vjaCNp3y/lAHzoP1BU9
|
Yud8SE+9zxLS1+CEjMrsUd/RGre/phme+wNXfaHfREAC9ewolgVChPIbMxG2f+vs
|
||||||
bUSao+7GXVeArKBjuqxN+t1uuiaxPH4L0oe2pMVjTig04zGJM5fTVoly859MEcC/
|
K8Xv52BFng7ta9fgsl1XuOjpuaSbQv6g+4ONk/lxKF0SmnhEGM3dmIYPONxW47Yf
|
||||||
R7Taq9RWGfXFmgCXy8Dviz3eOD90vqpCzhX4+ypK0cp2X0UwhMH4dpKUzExmdbhl
|
atnIjRra/YhPTNwrNBGMmG4IFKaOsMbjW/eakjWTWOVKKJNBMoDdRcYYWIMCpLy8
|
||||||
eBO5GcHB4VxvloRBNf9/Lr7YOTgWejMUw+MlhZE2RE8unfW1LnM/cjL4dhXzO/XB
|
AQUrMtQEsHSnqCwrw818S5A6rrhcfVGk36RGm0nOy6LS5g5jmqaYsvbCcBGY9B2c
|
||||||
FUHHNq8d6d4e02rfWqw7mZo2/NVJgFRcvzw2rgx7w7CKtCNwF4lNjUetB2waZzDb
|
SUAVNm17oo7TtEajk8hcSXoZod1t++pyjcVKEmSn3nFK7v5m3V+cPhNTxZMK459P
|
||||||
fAE0kwhK4Iuwvy12JOBzL0Yy9MxANtwUryr/LQz9AmdT4Rwnp0S5AY0EYnD+DQEM
|
3x1Ucqj/kTqrxKw6s2Uknuk0ajmw0ljV+BQwgL6maguo9BKgCNW5AY0EYnD+DQEM
|
||||||
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
|
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
|
||||||
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
|
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
|
||||||
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
|
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
|
||||||
|
@ -69,17 +69,17 @@ s1+eh00n/tVpi2Jj9pCm7S0csSXvXj8v2OTdK1jt4YjpzR0/rwh4+/xlOjDjZEqH
|
||||||
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
|
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
|
||||||
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
|
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
|
||||||
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
|
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
|
||||||
NwARAQABiQG8BBgBCAAmFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmJw/g0CGwwF
|
NwARAQABiQG8BBgBCAAmAhsMFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmahA9IF
|
||||||
CQPCZwAACgkQlDMMelCvzPPT7Qv8CjXUEhphZFLwpBfaNOzRNfIXJST9aDit8zHW
|
CRb8CMUACgkQlDMMelCvzPPQgQv/d5/z+fxgKqgfhQX+V49X4WgTVxZ/CzztDoJ1
|
||||||
IMmfSpORVfpU71IyIB3o/DtTUPwCeb8nvNJs7aj1QT1ZUSsqFa3yY2S16V/g8+WN
|
XAq1dzTNEy8AFguXIo6eVXPSpMxec7ZreN3+UPQBnCf3eR5YxWNYOYKmk0G4E8D2
|
||||||
sHca6oDSc1J+A0eEpEL1HbG1b5OPBC0AeGvvMOoqrbqThBZVKg1Jc/0SD3cvKElv
|
KGUJept7TSA42/8N2ov6tToXFg4CgzKZj0fYLwgutly7K8eiWmSU6ptaO8aEQBHB
|
||||||
aHeCZCNNmfcZ2Ib4HYhhc8//ZtC9TeI+5J/YesctY1M12EoWMxMrc27Y3P5Pa0BI
|
gTGIOO3h6vJMGVycmoeRnHjv4wV84YWSVFSoJ7cY0he4Z9UznJBbE/KHZjrkXsPo
|
||||||
Uc3qxWggPq1vOFYsEshL0w99HyJvREJmQA7Fa0crV+rICxyrBxJeNnEvjH/0KCBU
|
N+Gg5lDuOP5xjKzM5SogV9lhxBAhMWAg3URUF15yruZBiA8uV1FOK8sal/9C1G7V
|
||||||
LCkEonLY1QwrxyeeV3VpxGE3zHHE3azOdAjTIoAdzX5f/qhbgYlM68GL2f8xdDkp
|
M6ygA6uOZqXlZtcdA94RoSsW2pZ9eLVPsxz2B3Zko7tu11MpNP/wYmfGTI3KxZBj
|
||||||
O0igSGHWhO4F8BfmE7IOTx1Bi7daczp8nCFxh73cKpKB0RUsd9xxrqYpovjmEAlo
|
n/eodvwjJSgHpGOFSmbNzvPJo3to5nNlp7wH1KxIMc6Uuu9hgfDfwkFZgV2bnFIa
|
||||||
w7aHpdzt64NQcsrbK10OSVDF3gFa9Vz20/NQvdUrp8jGmAb/8+nYqI94Jsc28H36
|
Q6gyF548Ub48z7Dz83+WwLgbX19ve4oZx+dqSdczP6ILHRQomtrzrkkP2LU52oI5
|
||||||
UeGsouhyuITLwEhScounZDqop+Dx
|
mxFo+ioe/ABCufSmyqFye0psX3Sp
|
||||||
=Zg+6
|
=WtqZ
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -22,10 +22,11 @@
|
||||||
],
|
],
|
||||||
"rooms": [
|
"rooms": [
|
||||||
"#cinny:matrix.org",
|
"#cinny:matrix.org",
|
||||||
"#foundation-office:matrix.org",
|
"#freesoftware:matrix.org",
|
||||||
"#thisweekinmatrix:matrix.org",
|
"#pcapdroid:matrix.org",
|
||||||
"#matrix-dev:matrix.org",
|
"#gentoo:matrix.org",
|
||||||
"#matrix:matrix.org"
|
"#PrivSec.dev:arcticfoxes.net",
|
||||||
|
"#disroot:aria-net.org"
|
||||||
],
|
],
|
||||||
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,9 +19,16 @@ server {
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /opt/cinny/dist/;
|
root /opt/cinny/dist/;
|
||||||
index index.html;
|
|
||||||
}
|
rewrite ^/config.json$ /config.json break;
|
||||||
location ~* ^\/(login|register) {
|
rewrite ^/manifest.json$ /manifest.json break;
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
|
rewrite ^.*/olm.wasm$ /olm.wasm break;
|
||||||
|
rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break;
|
||||||
|
|
||||||
|
rewrite ^/public/(.*)$ /public/$1 break;
|
||||||
|
rewrite ^/assets/(.*)$ /assets/$1 break;
|
||||||
|
|
||||||
|
rewrite ^(.+)$ /index.html break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
docker-nginx.conf
Normal file
16
docker-nginx.conf
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
server {
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
rewrite ^/config.json$ /config.json break;
|
||||||
|
rewrite ^/manifest.json$ /manifest.json break;
|
||||||
|
|
||||||
|
rewrite ^.*/olm.wasm$ /olm.wasm break;
|
||||||
|
rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break;
|
||||||
|
|
||||||
|
rewrite ^/public/(.*)$ /public/$1 break;
|
||||||
|
rewrite ^/assets/(.*)$ /assets/$1 break;
|
||||||
|
|
||||||
|
rewrite ^(.+)$ /index.html break;
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,12 +90,6 @@
|
||||||
window.global ||= window;
|
window.global ||= window;
|
||||||
</script>
|
</script>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<audio id="notificationSound">
|
|
||||||
<source src="./public/sound/notification.ogg" type="audio/ogg" />
|
|
||||||
</audio>
|
|
||||||
<audio id="inviteSound">
|
|
||||||
<source src="./public/sound/invite.ogg" type="audio/ogg" />
|
|
||||||
</audio>
|
|
||||||
<script type="module" src="./src/index.tsx"></script>
|
<script type="module" src="./src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
235
package-lock.json
generated
235
package-lock.json
generated
|
@ -1,19 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "cinny",
|
"name": "cinny",
|
||||||
"version": "3.2.0",
|
"version": "4.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cinny",
|
"name": "cinny",
|
||||||
"version": "3.2.0",
|
"version": "4.0.3",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
||||||
"@fontsource/inter": "4.5.14",
|
"@fontsource/inter": "4.5.14",
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tanstack/react-query": "5.24.1",
|
"@tanstack/react-query": "5.24.1",
|
||||||
"@tanstack/react-query-devtools": "5.24.1",
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
|
@ -35,16 +34,14 @@
|
||||||
"flux": "4.0.3",
|
"flux": "4.0.3",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
"folds": "2.0.0",
|
"folds": "2.0.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.4.6",
|
||||||
"html-dom-parser": "4.0.0",
|
"html-dom-parser": "4.0.0",
|
||||||
"html-react-parser": "4.2.0",
|
"html-react-parser": "4.2.0",
|
||||||
"immer": "9.0.16",
|
"immer": "9.0.16",
|
||||||
"is-hotkey": "0.2.0",
|
"is-hotkey": "0.2.0",
|
||||||
"jotai": "2.6.0",
|
"jotai": "2.6.0",
|
||||||
"katex": "0.16.10",
|
"linkify-react": "4.1.3",
|
||||||
"linkify-html": "4.0.2",
|
"linkifyjs": "4.1.3",
|
||||||
"linkify-react": "4.1.1",
|
|
||||||
"linkifyjs": "4.0.2",
|
|
||||||
"matrix-js-sdk": "29.1.0",
|
"matrix-js-sdk": "29.1.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "4.2.67",
|
"pdfjs-dist": "4.2.67",
|
||||||
|
@ -54,10 +51,8 @@
|
||||||
"react-aria": "3.29.1",
|
"react-aria": "3.29.1",
|
||||||
"react-autosize-textarea": "7.1.0",
|
"react-autosize-textarea": "7.1.0",
|
||||||
"react-blurhash": "0.2.0",
|
"react-blurhash": "0.2.0",
|
||||||
"react-dnd": "16.0.1",
|
|
||||||
"react-dnd-html5-backend": "16.0.1",
|
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-error-boundary": "4.0.10",
|
"react-error-boundary": "4.0.13",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-modal": "3.16.1",
|
"react-modal": "3.16.1",
|
||||||
"react-range": "1.8.14",
|
"react-range": "1.8.14",
|
||||||
|
@ -67,7 +62,6 @@
|
||||||
"slate-history": "0.93.0",
|
"slate-history": "0.93.0",
|
||||||
"slate-react": "0.98.4",
|
"slate-react": "0.98.4",
|
||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"twemoji": "14.0.2",
|
|
||||||
"ua-parser-js": "1.0.35"
|
"ua-parser-js": "1.0.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1109,18 +1103,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
},
|
},
|
||||||
"node_modules/@khanacademy/simple-markdown": {
|
|
||||||
"version": "0.8.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz",
|
|
||||||
"integrity": "sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/react": ">=16.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "16.14.0",
|
|
||||||
"react-dom": "16.14.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mapbox/node-pre-gyp": {
|
"node_modules/@mapbox/node-pre-gyp": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||||
|
@ -1942,21 +1924,6 @@
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-dnd/asap": {
|
|
||||||
"version": "5.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
|
|
||||||
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
|
|
||||||
},
|
|
||||||
"node_modules/@react-dnd/invariant": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
|
|
||||||
},
|
|
||||||
"node_modules/@react-dnd/shallowequal": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
|
|
||||||
},
|
|
||||||
"node_modules/@react-stately/calendar": {
|
"node_modules/@react-stately/calendar": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.4.1.tgz",
|
||||||
|
@ -3270,6 +3237,15 @@
|
||||||
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
|
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
|
||||||
|
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/is-hotkey": {
|
"node_modules/@types/is-hotkey": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz",
|
||||||
|
@ -4419,14 +4395,6 @@
|
||||||
"color-support": "bin.js"
|
"color-support": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
|
||||||
"version": "8.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
|
||||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/compute-scroll-into-view": {
|
"node_modules/compute-scroll-into-view": {
|
||||||
"version": "1.0.20",
|
"version": "1.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
||||||
|
@ -4723,16 +4691,6 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dnd-core": {
|
|
||||||
"version": "16.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
|
|
||||||
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
|
|
||||||
"dependencies": {
|
|
||||||
"@react-dnd/asap": "^5.0.1",
|
|
||||||
"@react-dnd/invariant": "^4.0.1",
|
|
||||||
"redux": "^4.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
@ -5559,7 +5517,8 @@
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.2.12",
|
"version": "3.2.12",
|
||||||
|
@ -5774,9 +5733,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/formik": {
|
"node_modules/formik": {
|
||||||
"version": "2.2.9",
|
"version": "2.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
|
||||||
"integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==",
|
"integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
|
@ -5784,38 +5743,23 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
"deepmerge": "^2.1.1",
|
"deepmerge": "^2.1.1",
|
||||||
"hoist-non-react-statics": "^3.3.0",
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"react-fast-compare": "^2.0.1",
|
"react-fast-compare": "^2.0.1",
|
||||||
"tiny-warning": "^1.0.2",
|
"tiny-warning": "^1.0.2",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/formik/node_modules/tslib": {
|
||||||
"version": "8.1.0",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||||
"dependencies": {
|
|
||||||
"graceful-fs": "^4.2.0",
|
|
||||||
"jsonfile": "^4.0.0",
|
|
||||||
"universalify": "^0.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6 <7 || >=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fs-extra/node_modules/jsonfile": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"graceful-fs": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/fs-minipass": {
|
"node_modules/fs-minipass": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
@ -6060,7 +6004,8 @@
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.10",
|
"version": "4.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/grapheme-splitter": {
|
"node_modules/grapheme-splitter": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
|
@ -6749,17 +6694,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsonfile": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==",
|
|
||||||
"dependencies": {
|
|
||||||
"universalify": "^0.1.2"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"graceful-fs": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jsx-ast-utils": {
|
"node_modules/jsx-ast-utils": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
|
||||||
|
@ -6778,21 +6712,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
},
|
},
|
||||||
"node_modules/katex": {
|
|
||||||
"version": "0.16.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
|
|
||||||
"integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
|
|
||||||
"funding": [
|
|
||||||
"https://opencollective.com/katex",
|
|
||||||
"https://github.com/sponsors/katex"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^8.3.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"katex": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/language-subtag-registry": {
|
"node_modules/language-subtag-registry": {
|
||||||
"version": "0.3.22",
|
"version": "0.3.22",
|
||||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
|
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
|
||||||
|
@ -6854,27 +6773,19 @@
|
||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/linkify-html": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/linkify-html/-/linkify-html-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-YcN3tsyutK2Y/uSuoG0zne8FQdoqzrAgNU5ko0DWE7M2oQ3ms4z/202f2W4TvRm9uxKdrsWAullfynANLaVMqw==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"linkifyjs": "^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/linkify-react": {
|
"node_modules/linkify-react": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
|
||||||
"integrity": "sha512-2K9Y1cUdvq40dFWqCJ//X+WP19nlzIVITFGI93RjLnA0M7KbnxQ/ffC3AZIZaEIrLangF9Hjt3i0GQ9/anEG5A==",
|
"integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"linkifyjs": "^4.0.0",
|
"linkifyjs": "^4.0.0",
|
||||||
"react": ">= 15.0.0"
|
"react": ">= 15.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/linkifyjs": {
|
"node_modules/linkifyjs": {
|
||||||
"version": "4.0.2",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz",
|
||||||
"integrity": "sha512-/VSoCZiglX0VMsXmL5PN3lRg45M86lrD9PskdkA2abWaTKap1bIcJ11LS4EE55bcUl9ZOR4eZ792UtQ9E/5xLA=="
|
"integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg=="
|
||||||
},
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
|
@ -7766,43 +7677,6 @@
|
||||||
"react": ">=15"
|
"react": ">=15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dnd": {
|
|
||||||
"version": "16.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
|
|
||||||
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@react-dnd/invariant": "^4.0.1",
|
|
||||||
"@react-dnd/shallowequal": "^4.0.1",
|
|
||||||
"dnd-core": "^16.0.1",
|
|
||||||
"fast-deep-equal": "^3.1.3",
|
|
||||||
"hoist-non-react-statics": "^3.3.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/hoist-non-react-statics": ">= 3.3.1",
|
|
||||||
"@types/node": ">= 12",
|
|
||||||
"@types/react": ">= 16",
|
|
||||||
"react": ">= 16.14"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/hoist-non-react-statics": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/node": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-dnd-html5-backend": {
|
|
||||||
"version": "16.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
|
|
||||||
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
|
|
||||||
"dependencies": {
|
|
||||||
"dnd-core": "^16.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
|
@ -7816,9 +7690,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-error-boundary": {
|
"node_modules/react-error-boundary": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz",
|
||||||
"integrity": "sha512-pvVKdi77j2OoPHo+p3rorgE43OjDWiqFkaqkJz8sJKK6uf/u8xtzuaVfj5qJ2JnDLIgF1De3zY5AJDijp+LVPA==",
|
"integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5"
|
"@babel/runtime": "^7.12.5"
|
||||||
},
|
},
|
||||||
|
@ -7950,14 +7824,6 @@
|
||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/redux": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
|
||||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.9.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
|
||||||
|
@ -8694,7 +8560,8 @@
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
"node_modules/tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
|
@ -8711,22 +8578,6 @@
|
||||||
"typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
|
"typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/twemoji": {
|
|
||||||
"version": "14.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz",
|
|
||||||
"integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==",
|
|
||||||
"dependencies": {
|
|
||||||
"fs-extra": "^8.0.1",
|
|
||||||
"jsonfile": "^5.0.0",
|
|
||||||
"twemoji-parser": "14.0.0",
|
|
||||||
"universalify": "^0.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/twemoji-parser": {
|
|
||||||
"version": "14.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
|
|
||||||
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="
|
|
||||||
},
|
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
@ -8875,14 +8726,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
|
||||||
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
|
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.13",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||||
|
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cinny",
|
"name": "cinny",
|
||||||
"version": "3.2.0",
|
"version": "4.0.3",
|
||||||
"description": "Yet another matrix client",
|
"description": "Yet another matrix client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -24,7 +24,6 @@
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
||||||
"@fontsource/inter": "4.5.14",
|
"@fontsource/inter": "4.5.14",
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tanstack/react-query": "5.24.1",
|
"@tanstack/react-query": "5.24.1",
|
||||||
"@tanstack/react-query-devtools": "5.24.1",
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
|
@ -46,16 +45,14 @@
|
||||||
"flux": "4.0.3",
|
"flux": "4.0.3",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
"folds": "2.0.0",
|
"folds": "2.0.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.4.6",
|
||||||
"html-dom-parser": "4.0.0",
|
"html-dom-parser": "4.0.0",
|
||||||
"html-react-parser": "4.2.0",
|
"html-react-parser": "4.2.0",
|
||||||
"immer": "9.0.16",
|
"immer": "9.0.16",
|
||||||
"is-hotkey": "0.2.0",
|
"is-hotkey": "0.2.0",
|
||||||
"jotai": "2.6.0",
|
"jotai": "2.6.0",
|
||||||
"katex": "0.16.10",
|
"linkify-react": "4.1.3",
|
||||||
"linkify-html": "4.0.2",
|
"linkifyjs": "4.1.3",
|
||||||
"linkify-react": "4.1.1",
|
|
||||||
"linkifyjs": "4.0.2",
|
|
||||||
"matrix-js-sdk": "29.1.0",
|
"matrix-js-sdk": "29.1.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "4.2.67",
|
"pdfjs-dist": "4.2.67",
|
||||||
|
@ -65,10 +62,8 @@
|
||||||
"react-aria": "3.29.1",
|
"react-aria": "3.29.1",
|
||||||
"react-autosize-textarea": "7.1.0",
|
"react-autosize-textarea": "7.1.0",
|
||||||
"react-blurhash": "0.2.0",
|
"react-blurhash": "0.2.0",
|
||||||
"react-dnd": "16.0.1",
|
|
||||||
"react-dnd-html5-backend": "16.0.1",
|
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-error-boundary": "4.0.10",
|
"react-error-boundary": "4.0.13",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-modal": "3.16.1",
|
"react-modal": "3.16.1",
|
||||||
"react-range": "1.8.14",
|
"react-range": "1.8.14",
|
||||||
|
@ -78,7 +73,6 @@
|
||||||
"slate-history": "0.93.0",
|
"slate-history": "0.93.0",
|
||||||
"slate-react": "0.98.4",
|
"slate-react": "0.98.4",
|
||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"twemoji": "14.0.2",
|
|
||||||
"ua-parser-js": "1.0.35"
|
"ua-parser-js": "1.0.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -2,17 +2,13 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Avatar.scss';
|
import './Avatar.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../text/Text';
|
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';
|
import { avatarInitials } from '../../../util/common';
|
||||||
|
|
||||||
const Avatar = React.forwardRef(({
|
const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
|
||||||
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';
|
||||||
|
@ -20,34 +16,34 @@ const Avatar = React.forwardRef(({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
|
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
|
||||||
{
|
{imageSrc !== null ? (
|
||||||
imageSrc !== null
|
<img
|
||||||
? (
|
draggable="false"
|
||||||
<img
|
src={imageSrc}
|
||||||
draggable="false"
|
onLoad={(e) => {
|
||||||
src={imageSrc}
|
e.target.style.backgroundColor = 'transparent';
|
||||||
onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
|
}}
|
||||||
onError={(e) => { e.target.src = ImageBrokenSVG; }}
|
onError={(e) => {
|
||||||
alt=""
|
e.target.src = ImageBrokenSVG;
|
||||||
/>
|
}}
|
||||||
)
|
alt=""
|
||||||
: (
|
/>
|
||||||
<span
|
) : (
|
||||||
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
<span
|
||||||
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
|
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
||||||
>
|
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
|
||||||
{
|
>
|
||||||
iconSrc !== null
|
{iconSrc !== null ? (
|
||||||
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
<RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||||
: text !== null && (
|
) : (
|
||||||
<Text variant={textSize} primary>
|
text !== null && (
|
||||||
{twemojify(avatarInitials(text))}
|
<Text variant={textSize} primary>
|
||||||
</Text>
|
{avatarInitials(text)}
|
||||||
)
|
</Text>
|
||||||
}
|
)
|
||||||
</span>
|
)}
|
||||||
)
|
</span>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,8 +22,7 @@
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background-color: var(--tc-surface-low);
|
background-color: var(--tc-surface-low);
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
border-radius: calc(var(--bo-radius) / 2);
|
||||||
transition: transform 200ms ease-in-out,
|
transition: transform 200ms ease-in-out, opacity 200ms ease-in-out;
|
||||||
opacity 200ms ease-in-out;
|
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,8 +35,8 @@
|
||||||
@include dir.prop(transform, var(--ltr), var(--rtl));
|
@include dir.prop(transform, var(--ltr), var(--rtl));
|
||||||
|
|
||||||
transform: translateX(calc(125%));
|
transform: translateX(calc(125%));
|
||||||
background-color: white;
|
background-color: var(--bg-surface);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './Math.scss';
|
|
||||||
|
|
||||||
import katex from 'katex';
|
|
||||||
import 'katex/dist/katex.min.css';
|
|
||||||
|
|
||||||
import 'katex/dist/contrib/copy-tex';
|
|
||||||
|
|
||||||
const Math = React.memo(({
|
|
||||||
content, throwOnError, errorColor, displayMode,
|
|
||||||
}) => {
|
|
||||||
const ref = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
|
|
||||||
}, [content, throwOnError, errorColor, displayMode]);
|
|
||||||
|
|
||||||
return <span ref={ref} />;
|
|
||||||
});
|
|
||||||
Math.defaultProps = {
|
|
||||||
throwOnError: null,
|
|
||||||
errorColor: null,
|
|
||||||
displayMode: null,
|
|
||||||
};
|
|
||||||
Math.propTypes = {
|
|
||||||
content: PropTypes.string.isRequired,
|
|
||||||
throwOnError: PropTypes.bool,
|
|
||||||
errorColor: PropTypes.string,
|
|
||||||
displayMode: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Math;
|
|
|
@ -1,3 +0,0 @@
|
||||||
.katex-display {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
|
@ -26,6 +26,7 @@ import * as css from './PdfViewer.css';
|
||||||
import { AsyncStatus } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus } from '../../hooks/useAsyncCallback';
|
||||||
import { useZoom } from '../../hooks/useZoom';
|
import { useZoom } from '../../hooks/useZoom';
|
||||||
import { createPage, usePdfDocumentLoader, usePdfJSLoader } from '../../plugins/pdfjs-dist';
|
import { createPage, usePdfDocumentLoader, usePdfJSLoader } from '../../plugins/pdfjs-dist';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
export type PdfViewerProps = {
|
export type PdfViewerProps = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -201,6 +202,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setJumpAnchor(undefined),
|
onDeactivate: () => setJumpAnchor(undefined),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu variant="Surface">
|
<Menu variant="Surface">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MsgType } from 'matrix-js-sdk';
|
import { MsgType } from 'matrix-js-sdk';
|
||||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||||
|
import { Opts } from 'linkifyjs';
|
||||||
import {
|
import {
|
||||||
AudioContent,
|
AudioContent,
|
||||||
DownloadFile,
|
DownloadFile,
|
||||||
|
@ -27,6 +28,7 @@ import { Image, MediaControl, Video } from './media';
|
||||||
import { ImageViewer } from './image-viewer';
|
import { ImageViewer } from './image-viewer';
|
||||||
import { PdfViewer } from './Pdf-viewer';
|
import { PdfViewer } from './Pdf-viewer';
|
||||||
import { TextViewer } from './text-viewer';
|
import { TextViewer } from './text-viewer';
|
||||||
|
import { testMatrixTo } from '../plugins/matrix-to';
|
||||||
|
|
||||||
type RenderMessageContentProps = {
|
type RenderMessageContentProps = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
@ -38,6 +40,7 @@ type RenderMessageContentProps = {
|
||||||
urlPreview?: boolean;
|
urlPreview?: boolean;
|
||||||
highlightRegex?: RegExp;
|
highlightRegex?: RegExp;
|
||||||
htmlReactParserOptions: HTMLReactParserOptions;
|
htmlReactParserOptions: HTMLReactParserOptions;
|
||||||
|
linkifyOpts: Opts;
|
||||||
outlineAttachment?: boolean;
|
outlineAttachment?: boolean;
|
||||||
};
|
};
|
||||||
export function RenderMessageContent({
|
export function RenderMessageContent({
|
||||||
|
@ -50,8 +53,21 @@ export function RenderMessageContent({
|
||||||
urlPreview,
|
urlPreview,
|
||||||
highlightRegex,
|
highlightRegex,
|
||||||
htmlReactParserOptions,
|
htmlReactParserOptions,
|
||||||
|
linkifyOpts,
|
||||||
outlineAttachment,
|
outlineAttachment,
|
||||||
}: RenderMessageContentProps) {
|
}: RenderMessageContentProps) {
|
||||||
|
const renderUrlsPreview = (urls: string[]) => {
|
||||||
|
const filteredUrls = urls.filter((url) => !testMatrixTo(url));
|
||||||
|
if (filteredUrls.length === 0) return undefined;
|
||||||
|
return (
|
||||||
|
<UrlPreviewHolder>
|
||||||
|
{filteredUrls.map((url) => (
|
||||||
|
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||||
|
))}
|
||||||
|
</UrlPreviewHolder>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderFile = () => (
|
const renderFile = () => (
|
||||||
<MFile
|
<MFile
|
||||||
content={getContent()}
|
content={getContent()}
|
||||||
|
@ -95,19 +111,10 @@ export function RenderMessageContent({
|
||||||
{...props}
|
{...props}
|
||||||
highlightRegex={highlightRegex}
|
highlightRegex={highlightRegex}
|
||||||
htmlReactParserOptions={htmlReactParserOptions}
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
linkifyOpts={linkifyOpts}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderUrlsPreview={
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||||
urlPreview
|
|
||||||
? (urls) => (
|
|
||||||
<UrlPreviewHolder>
|
|
||||||
{urls.map((url) => (
|
|
||||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
|
||||||
))}
|
|
||||||
</UrlPreviewHolder>
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -123,19 +130,10 @@ export function RenderMessageContent({
|
||||||
{...props}
|
{...props}
|
||||||
highlightRegex={highlightRegex}
|
highlightRegex={highlightRegex}
|
||||||
htmlReactParserOptions={htmlReactParserOptions}
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
linkifyOpts={linkifyOpts}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderUrlsPreview={
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||||
urlPreview
|
|
||||||
? (urls) => (
|
|
||||||
<UrlPreviewHolder>
|
|
||||||
{urls.map((url) => (
|
|
||||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
|
||||||
))}
|
|
||||||
</UrlPreviewHolder>
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -150,19 +148,10 @@ export function RenderMessageContent({
|
||||||
{...props}
|
{...props}
|
||||||
highlightRegex={highlightRegex}
|
highlightRegex={highlightRegex}
|
||||||
htmlReactParserOptions={htmlReactParserOptions}
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
linkifyOpts={linkifyOpts}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderUrlsPreview={
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||||
urlPreview
|
|
||||||
? (urls) => (
|
|
||||||
<UrlPreviewHolder>
|
|
||||||
{urls.map((url) => (
|
|
||||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
|
||||||
))}
|
|
||||||
</UrlPreviewHolder>
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
IconButton,
|
IconButton,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { stopPropagation } from '../utils/keyboard';
|
||||||
|
|
||||||
export type UIAFlowOverlayProps = {
|
export type UIAFlowOverlayProps = {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
|
@ -28,7 +29,7 @@ export function UIAFlowOverlay({
|
||||||
}: UIAFlowOverlayProps) {
|
}: UIAFlowOverlayProps) {
|
||||||
return (
|
return (
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
<FocusTrap focusTrapOptions={{ initialFocus: false }}>
|
<FocusTrap focusTrapOptions={{ initialFocus: false, escapeDeactivates: stopPropagation }}>
|
||||||
<Box style={{ height: '100%' }} direction="Column" grow="Yes" gap="400">
|
<Box style={{ height: '100%' }} direction="Column" grow="Yes" gap="400">
|
||||||
<Box grow="Yes" direction="Column" alignItems="Center" justifyContent="Center">
|
<Box grow="Yes" direction="Column" alignItems="Center" justifyContent="Center">
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
|
|
||||||
import { CustomEditor, useEditor } from './Editor';
|
import { CustomEditor, useEditor } from './Editor';
|
||||||
import { Toolbar } from './Toolbar';
|
import { Toolbar } from './Toolbar';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
export function EditorPreview() {
|
export function EditorPreview() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
@ -32,6 +33,7 @@ export function EditorPreview() {
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setOpen(false),
|
onDeactivate: () => setOpen(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal size="500">
|
<Modal size="500">
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { isMacOS } from '../../utils/user-agent';
|
||||||
import { KeySymbol } from '../../utils/key-symbol';
|
import { KeySymbol } from '../../utils/key-symbol';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
|
function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
|
||||||
return (
|
return (
|
||||||
|
@ -151,6 +152,7 @@ export function HeadingBlockButton() {
|
||||||
isKeyForward: (evt: KeyboardEvent) =>
|
isKeyForward: (evt: KeyboardEvent) =>
|
||||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu style={{ padding: config.space.S100 }}>
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { Header, Menu, Scroll, config } from 'folds';
|
import { Header, Menu, Scroll, config } from 'folds';
|
||||||
|
|
||||||
import * as css from './AutocompleteMenu.css';
|
import * as css from './AutocompleteMenu.css';
|
||||||
import { preventScrollWithArrowKey } from '../../../utils/keyboard';
|
import { preventScrollWithArrowKey, stopPropagation } from '../../../utils/keyboard';
|
||||||
|
|
||||||
type AutocompleteMenuProps = {
|
type AutocompleteMenuProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
@ -24,6 +24,7 @@ export function AutocompleteMenu({ headerContent, requestClose, children }: Auto
|
||||||
allowOutsideClick: true,
|
allowOutsideClick: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
|
isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
|
||||||
isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
|
isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu className={css.AutocompleteMenu}>
|
<Menu className={css.AutocompleteMenu}>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { mDirectAtom } from '../../../state/mDirectList';
|
||||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||||
import { factoryRoomIdByActivity } from '../../../utils/sort';
|
import { factoryRoomIdByActivity } from '../../../utils/sort';
|
||||||
import { RoomAvatar, RoomIcon } from '../../room-avatar';
|
import { RoomAvatar, RoomIcon } from '../../room-avatar';
|
||||||
|
import { getViaServers } from '../../../plugins/via-servers';
|
||||||
|
|
||||||
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
|
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
|
||||||
|
|
||||||
|
@ -104,10 +105,14 @@ export function RoomMentionAutocomplete({
|
||||||
}, [query.text, search, resetSearch]);
|
}, [query.text, search, resetSearch]);
|
||||||
|
|
||||||
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
|
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
|
||||||
|
const mentionRoom = mx.getRoom(roomAliasOrId);
|
||||||
|
const viaServers = mentionRoom ? getViaServers(mentionRoom) : undefined;
|
||||||
const mentionEl = createMentionElement(
|
const mentionEl = createMentionElement(
|
||||||
roomAliasOrId,
|
roomAliasOrId,
|
||||||
name.startsWith('#') ? name : `#${name}`,
|
name.startsWith('#') ? name : `#${name}`,
|
||||||
roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId
|
roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId,
|
||||||
|
undefined,
|
||||||
|
viaServers
|
||||||
);
|
);
|
||||||
replaceWithElement(editor, query.range, mentionEl);
|
replaceWithElement(editor, query.range, mentionEl);
|
||||||
moveCursor(editor, true);
|
moveCursor(editor, true);
|
||||||
|
|
|
@ -18,8 +18,13 @@ import {
|
||||||
ParagraphElement,
|
ParagraphElement,
|
||||||
UnorderedListElement,
|
UnorderedListElement,
|
||||||
} from './slate';
|
} from './slate';
|
||||||
import { parseMatrixToUrl } from '../../utils/matrix';
|
|
||||||
import { createEmoticonElement, createMentionElement } from './utils';
|
import { createEmoticonElement, createMentionElement } from './utils';
|
||||||
|
import {
|
||||||
|
parseMatrixToRoom,
|
||||||
|
parseMatrixToRoomEvent,
|
||||||
|
parseMatrixToUser,
|
||||||
|
testMatrixTo,
|
||||||
|
} from '../../plugins/matrix-to';
|
||||||
|
|
||||||
const markNodeToType: Record<string, MarkType> = {
|
const markNodeToType: Record<string, MarkType> = {
|
||||||
b: MarkType.Bold,
|
b: MarkType.Bold,
|
||||||
|
@ -68,11 +73,33 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
|
||||||
return createEmoticonElement(src, alt || 'Unknown Emoji');
|
return createEmoticonElement(src, alt || 'Unknown Emoji');
|
||||||
}
|
}
|
||||||
if (node.name === 'a') {
|
if (node.name === 'a') {
|
||||||
const { href } = node.attribs;
|
const href = decodeURIComponent(node.attribs.href);
|
||||||
if (typeof href !== 'string') return undefined;
|
if (typeof href !== 'string') return undefined;
|
||||||
const [mxId] = parseMatrixToUrl(href);
|
if (testMatrixTo(href)) {
|
||||||
if (mxId) {
|
const userMention = parseMatrixToUser(href);
|
||||||
return createMentionElement(mxId, parseNodeText(node) || mxId, false);
|
if (userMention) {
|
||||||
|
return createMentionElement(userMention, parseNodeText(node) || userMention, false);
|
||||||
|
}
|
||||||
|
const roomMention = parseMatrixToRoom(href);
|
||||||
|
if (roomMention) {
|
||||||
|
return createMentionElement(
|
||||||
|
roomMention.roomIdOrAlias,
|
||||||
|
parseNodeText(node) || roomMention.roomIdOrAlias,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
roomMention.viaServers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const eventMention = parseMatrixToRoomEvent(href);
|
||||||
|
if (eventMention) {
|
||||||
|
return createMentionElement(
|
||||||
|
eventMention.roomIdOrAlias,
|
||||||
|
parseNodeText(node) || eventMention.roomIdOrAlias,
|
||||||
|
false,
|
||||||
|
eventMention.eventId,
|
||||||
|
eventMention.viaServers
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -51,10 +51,19 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
|
||||||
case BlockType.UnorderedList:
|
case BlockType.UnorderedList:
|
||||||
return `<ul>${children}</ul>`;
|
return `<ul>${children}</ul>`;
|
||||||
|
|
||||||
case BlockType.Mention:
|
case BlockType.Mention: {
|
||||||
return `<a href="https://matrix.to/#/${encodeURIComponent(node.id)}">${sanitizeText(
|
let fragment = node.id;
|
||||||
node.name
|
|
||||||
)}</a>`;
|
if (node.eventId) {
|
||||||
|
fragment += `/${node.eventId}`;
|
||||||
|
}
|
||||||
|
if (node.viaServers && node.viaServers.length > 0) {
|
||||||
|
fragment += `?${node.viaServers.map((server) => `via=${server}`).join('&')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matrixTo = `https://matrix.to/#/${fragment}`;
|
||||||
|
return `<a href="${encodeURIComponent(matrixTo)}">${sanitizeText(node.name)}</a>`;
|
||||||
|
}
|
||||||
case BlockType.Emoticon:
|
case BlockType.Emoticon:
|
||||||
return node.key.startsWith('mxc://')
|
return node.key.startsWith('mxc://')
|
||||||
? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText(
|
? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText(
|
||||||
|
|
2
src/app/components/editor/slate.d.ts
vendored
2
src/app/components/editor/slate.d.ts
vendored
|
@ -29,6 +29,8 @@ export type LinkElement = {
|
||||||
export type MentionElement = {
|
export type MentionElement = {
|
||||||
type: BlockType.Mention;
|
type: BlockType.Mention;
|
||||||
id: string;
|
id: string;
|
||||||
|
eventId?: string;
|
||||||
|
viaServers?: string[];
|
||||||
highlight: boolean;
|
highlight: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
children: Text[];
|
children: Text[];
|
||||||
|
|
|
@ -158,10 +158,14 @@ export const resetEditorHistory = (editor: Editor) => {
|
||||||
export const createMentionElement = (
|
export const createMentionElement = (
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
highlight: boolean
|
highlight: boolean,
|
||||||
|
eventId?: string,
|
||||||
|
viaServers?: string[]
|
||||||
): MentionElement => ({
|
): MentionElement => ({
|
||||||
type: BlockType.Mention,
|
type: BlockType.Mention,
|
||||||
id,
|
id,
|
||||||
|
eventId,
|
||||||
|
viaServers,
|
||||||
highlight,
|
highlight,
|
||||||
name,
|
name,
|
||||||
children: [{ text: '' }],
|
children: [{ text: '' }],
|
||||||
|
|
|
@ -45,7 +45,7 @@ import {
|
||||||
} from '../../plugins/emoji';
|
} from '../../plugins/emoji';
|
||||||
import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels';
|
import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels';
|
||||||
import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons';
|
import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons';
|
||||||
import { preventScrollWithArrowKey } from '../../utils/keyboard';
|
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
|
||||||
import { useRelevantImagePacks } from '../../hooks/useImagePacks';
|
import { useRelevantImagePacks } from '../../hooks/useImagePacks';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRecentEmoji } from '../../hooks/useRecentEmoji';
|
import { useRecentEmoji } from '../../hooks/useRecentEmoji';
|
||||||
|
@ -801,6 +801,7 @@ export function EmojiBoard({
|
||||||
!editableActiveElement() && isKeyHotkey(['arrowdown', 'arrowright'], evt),
|
!editableActiveElement() && isKeyHotkey(['arrowdown', 'arrowright'], evt),
|
||||||
isKeyBackward: (evt: KeyboardEvent) =>
|
isKeyBackward: (evt: KeyboardEvent) =>
|
||||||
!editableActiveElement() && isKeyHotkey(['arrowup', 'arrowleft'], evt),
|
!editableActiveElement() && isKeyHotkey(['arrowup', 'arrowleft'], evt),
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EmojiBoardLayout
|
<EmojiBoardLayout
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import { MatrixError } from 'matrix-js-sdk';
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
type LeaveRoomPromptProps = {
|
type LeaveRoomPromptProps = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -52,6 +53,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: onCancel,
|
onDeactivate: onCancel,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog variant="Surface">
|
<Dialog variant="Surface">
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import { MatrixError } from 'matrix-js-sdk';
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
type LeaveSpacePromptProps = {
|
type LeaveSpacePromptProps = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -52,6 +53,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: onCancel,
|
onDeactivate: onCancel,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog variant="Surface">
|
<Dialog variant="Surface">
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import parse, { HTMLReactParserOptions } from 'html-react-parser';
|
import parse, { HTMLReactParserOptions } from 'html-react-parser';
|
||||||
import Linkify from 'linkify-react';
|
import Linkify from 'linkify-react';
|
||||||
|
import { Opts } from 'linkifyjs';
|
||||||
import { MessageEmptyContent } from './content';
|
import { MessageEmptyContent } from './content';
|
||||||
import { sanitizeCustomHtml } from '../../utils/sanitize';
|
import { sanitizeCustomHtml } from '../../utils/sanitize';
|
||||||
import {
|
import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||||
LINKIFY_OPTS,
|
|
||||||
highlightText,
|
|
||||||
scaleSystemEmoji,
|
|
||||||
} from '../../plugins/react-custom-html-parser';
|
|
||||||
|
|
||||||
type RenderBodyProps = {
|
type RenderBodyProps = {
|
||||||
body: string;
|
body: string;
|
||||||
|
@ -15,12 +12,14 @@ type RenderBodyProps = {
|
||||||
|
|
||||||
highlightRegex?: RegExp;
|
highlightRegex?: RegExp;
|
||||||
htmlReactParserOptions: HTMLReactParserOptions;
|
htmlReactParserOptions: HTMLReactParserOptions;
|
||||||
|
linkifyOpts: Opts;
|
||||||
};
|
};
|
||||||
export function RenderBody({
|
export function RenderBody({
|
||||||
body,
|
body,
|
||||||
customBody,
|
customBody,
|
||||||
highlightRegex,
|
highlightRegex,
|
||||||
htmlReactParserOptions,
|
htmlReactParserOptions,
|
||||||
|
linkifyOpts,
|
||||||
}: RenderBodyProps) {
|
}: RenderBodyProps) {
|
||||||
if (body === '') <MessageEmptyContent />;
|
if (body === '') <MessageEmptyContent />;
|
||||||
if (customBody) {
|
if (customBody) {
|
||||||
|
@ -28,7 +27,7 @@ export function RenderBody({
|
||||||
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
|
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Linkify options={LINKIFY_OPTS}>
|
<Linkify options={linkifyOpts}>
|
||||||
{highlightRegex
|
{highlightRegex
|
||||||
? highlightText(highlightRegex, scaleSystemEmoji(body))
|
? highlightText(highlightRegex, scaleSystemEmoji(body))
|
||||||
: scaleSystemEmoji(body)}
|
: scaleSystemEmoji(body)}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
mimeTypeToExt,
|
mimeTypeToExt,
|
||||||
} from '../../../utils/mimeTypes';
|
} from '../../../utils/mimeTypes';
|
||||||
import * as css from './style.css';
|
import * as css from './style.css';
|
||||||
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
|
||||||
const renderErrorButton = (retry: () => void, text: string) => (
|
const renderErrorButton = (retry: () => void, text: string) => (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
|
@ -101,6 +102,7 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setTextViewer(false),
|
onDeactivate: () => setTextViewer(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -184,6 +186,7 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setPdfViewer(false),
|
onDeactivate: () => setPdfViewer(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { getFileSrcUrl } from './util';
|
||||||
import * as css from './style.css';
|
import * as css from './style.css';
|
||||||
import { bytesToSize } from '../../../utils/common';
|
import { bytesToSize } from '../../../utils/common';
|
||||||
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
|
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
|
||||||
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
|
||||||
type RenderViewerProps = {
|
type RenderViewerProps = {
|
||||||
src: string;
|
src: string;
|
||||||
|
@ -108,6 +109,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setViewer(false),
|
onDeactivate: () => setViewer(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
@ -5,6 +5,6 @@ import * as css from './styles.css';
|
||||||
|
|
||||||
export const NavItemContent = as<'p', ComponentProps<typeof Text>>(
|
export const NavItemContent = as<'p', ComponentProps<typeof Text>>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<Text className={classNames(css.NavItemContent, className)} size="T400" {...props} ref={ref} />
|
<Text className={classNames(css.NavItemContent, className)} size="T300" {...props} ref={ref} />
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -52,7 +52,7 @@ const NavItemBase = style({
|
||||||
backgroundColor: Container,
|
backgroundColor: Container,
|
||||||
color: OnContainer,
|
color: OnContainer,
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
minHeight: toRem(38),
|
minHeight: toRem(36),
|
||||||
|
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:hover, &:focus-visible': {
|
'&:hover, &:focus-visible': {
|
||||||
|
@ -111,6 +111,7 @@ export const NavItemContent = style({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
fontWeight: config.fontWeight.W500,
|
||||||
|
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { style } from '@vanilla-extract/css';
|
||||||
import { DefaultReset, color, config, toRem } from 'folds';
|
import { DefaultReset, color, config, toRem } from 'folds';
|
||||||
|
|
||||||
export const PageNav = style({
|
export const PageNav = style({
|
||||||
width: toRem(280),
|
width: toRem(256),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PageNavHeader = style({
|
export const PageNavHeader = style({
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { nameInitials } from '../../utils/common';
|
||||||
import { millify } from '../../plugins/millify';
|
import { millify } from '../../plugins/millify';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { onEnterOrSpace } from '../../utils/keyboard';
|
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
|
||||||
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
||||||
import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
|
import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
|
||||||
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
||||||
|
@ -107,6 +107,7 @@ function ErrorDialog({
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
onDeactivate: closeError,
|
onDeactivate: closeError,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog variant="Surface">
|
<Dialog variant="Surface">
|
||||||
|
@ -137,6 +138,7 @@ type RoomCardProps = {
|
||||||
topic?: string;
|
topic?: string;
|
||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
roomType?: string;
|
roomType?: string;
|
||||||
|
viaServers?: string[];
|
||||||
onView?: (roomId: string) => void;
|
onView?: (roomId: string) => void;
|
||||||
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
|
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
|
||||||
};
|
};
|
||||||
|
@ -151,6 +153,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||||
topic,
|
topic,
|
||||||
memberCount,
|
memberCount,
|
||||||
roomType,
|
roomType,
|
||||||
|
viaServers,
|
||||||
onView,
|
onView,
|
||||||
renderTopicViewer,
|
renderTopicViewer,
|
||||||
...props
|
...props
|
||||||
|
@ -193,7 +196,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||||
);
|
);
|
||||||
|
|
||||||
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
||||||
useCallback(() => mx.joinRoom(roomIdOrAlias), [mx, roomIdOrAlias])
|
useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
|
||||||
);
|
);
|
||||||
const joining =
|
const joining =
|
||||||
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
|
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
|
||||||
|
@ -236,6 +239,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
onDeactivate: closeTopic,
|
onDeactivate: closeTopic,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderTopicViewer(roomName, roomTopic, closeTopic)}
|
{renderTopicViewer(roomName, roomTopic, closeTopic)}
|
||||||
|
|
|
@ -22,9 +22,9 @@ export function UnreadBadge({ highlight, count }: UnreadBadgeProps) {
|
||||||
<Badge
|
<Badge
|
||||||
variant={highlight ? 'Success' : 'Secondary'}
|
variant={highlight ? 'Success' : 'Secondary'}
|
||||||
size={count > 0 ? '400' : '200'}
|
size={count > 0 ? '400' : '200'}
|
||||||
fill={count > 0 ? 'Solid' : 'Soft'}
|
fill="Solid"
|
||||||
radii="Pill"
|
radii="Pill"
|
||||||
outlined
|
outlined={false}
|
||||||
>
|
>
|
||||||
{count > 0 && (
|
{count > 0 && (
|
||||||
<Text as="span" size="L400">
|
<Text as="span" size="L400">
|
||||||
|
|
|
@ -9,8 +9,12 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||||
|
|
||||||
type JoinBeforeNavigateProps = { roomIdOrAlias: string };
|
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
|
||||||
export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
|
export function JoinBeforeNavigate({
|
||||||
|
roomIdOrAlias,
|
||||||
|
eventId,
|
||||||
|
viaServers,
|
||||||
|
}: JoinBeforeNavigateProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const allRooms = useAtomValue(allRoomsAtom);
|
const allRooms = useAtomValue(allRoomsAtom);
|
||||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||||
|
@ -20,7 +24,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
|
||||||
navigateSpace(roomId);
|
navigateSpace(roomId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigateRoom(roomId);
|
navigateRoom(roomId, eventId);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -46,6 +50,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
|
||||||
topic={summary?.topic}
|
topic={summary?.topic}
|
||||||
memberCount={summary?.num_joined_members}
|
memberCount={summary?.num_joined_members}
|
||||||
roomType={summary?.room_type}
|
roomType={summary?.room_type}
|
||||||
|
viaServers={viaServers}
|
||||||
renderTopicViewer={(name, topic, requestClose) => (
|
renderTopicViewer={(name, topic, requestClose) => (
|
||||||
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
|
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
type HierarchyItemWithParent = HierarchyItem & {
|
type HierarchyItemWithParent = HierarchyItem & {
|
||||||
parentId: string;
|
parentId: string;
|
||||||
|
@ -227,6 +228,7 @@ export function HierarchyItemMenu({
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu style={{ maxWidth: toRem(150), width: '100vw' }}>
|
<Menu style={{ maxWidth: toRem(150), width: '100vw' }}>
|
||||||
|
|
|
@ -47,6 +47,7 @@ import {
|
||||||
import { useOrphanSpaces } from '../../state/hooks/roomList';
|
import { useOrphanSpaces } from '../../state/hooks/roomList';
|
||||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||||
import { AccountDataEvent } from '../../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../../types/matrix/accountData';
|
||||||
|
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||||
|
|
||||||
export function Lobby() {
|
export function Lobby() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -57,6 +58,7 @@ export function Lobby() {
|
||||||
const space = useSpace();
|
const space = useSpace();
|
||||||
const spacePowerLevels = usePowerLevels(space);
|
const spacePowerLevels = usePowerLevels(space);
|
||||||
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
|
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
|
||||||
|
const members = useRoomMembers(mx, space.roomId);
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const heroSectionRef = useRef<HTMLDivElement>(null);
|
const heroSectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -519,7 +521,7 @@ export function Lobby() {
|
||||||
{screenSize === ScreenSize.Desktop && isDrawer && (
|
{screenSize === ScreenSize.Desktop && isDrawer && (
|
||||||
<>
|
<>
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
<MembersDrawer room={space} />
|
<MembersDrawer room={space} members={members} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { openInviteUser, openSpaceSettings } from '../../../client/action/naviga
|
||||||
import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
type LobbyMenuProps = {
|
type LobbyMenuProps = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -197,6 +198,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LobbyMenu
|
<LobbyMenu
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||||
import * as css from './LobbyHero.css';
|
import * as css from './LobbyHero.css';
|
||||||
import { PageHero } from '../../components/page';
|
import { PageHero } from '../../components/page';
|
||||||
import { onEnterOrSpace } from '../../utils/keyboard';
|
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
export function LobbyHero() {
|
export function LobbyHero() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
@ -46,6 +46,7 @@ export function LobbyHero() {
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
onDeactivate: () => setViewTopic(false),
|
onDeactivate: () => setViewTopic(false),
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RoomTopicViewer
|
<RoomTopicViewer
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {
|
||||||
} from '../../components/RoomSummaryLoader';
|
} from '../../components/RoomSummaryLoader';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||||
import { onEnterOrSpace } from '../../utils/keyboard';
|
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
|
||||||
import { Membership, RoomType } from '../../../types/matrix/room';
|
import { Membership, RoomType } from '../../../types/matrix/room';
|
||||||
import * as css from './RoomItem.css';
|
import * as css from './RoomItem.css';
|
||||||
import * as styleCss from './style.css';
|
import * as styleCss from './style.css';
|
||||||
|
@ -264,6 +264,7 @@ function RoomProfile({
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
onDeactivate: () => setView(false),
|
onDeactivate: () => setView(false),
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RoomTopicViewer
|
<RoomTopicViewer
|
||||||
|
|
|
@ -34,6 +34,7 @@ import * as styleCss from './style.css';
|
||||||
import { ErrorCode } from '../../cs-errorcode';
|
import { ErrorCode } from '../../cs-errorcode';
|
||||||
import { useDraggableItem } from './DnD';
|
import { useDraggableItem } from './DnD';
|
||||||
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
|
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
function SpaceProfileLoading() {
|
function SpaceProfileLoading() {
|
||||||
return (
|
return (
|
||||||
|
@ -277,6 +278,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu style={{ padding: config.space.S100 }}>
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
|
@ -338,6 +340,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu style={{ padding: config.space.S100 }}>
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
|
@ -479,7 +482,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{canEditChild && (
|
{canEditChild && (
|
||||||
<Box alignItems="Inherit" gap="200">
|
<Box shrink="No" alignItems="Inherit" gap="200">
|
||||||
<AddRoomButton item={item} />
|
<AddRoomButton item={item} />
|
||||||
{item.parentId === undefined && <AddSpaceButton item={item} />}
|
{item.parentId === undefined && <AddSpaceButton item={item} />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {
|
||||||
} from '../../hooks/useAsyncSearch';
|
} from '../../hooks/useAsyncSearch';
|
||||||
import { DebounceOptions, useDebounce } from '../../hooks/useDebounce';
|
import { DebounceOptions, useDebounce } from '../../hooks/useDebounce';
|
||||||
import { VirtualTile } from '../../components/virtualizer';
|
import { VirtualTile } from '../../components/virtualizer';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
type OrderButtonProps = {
|
type OrderButtonProps = {
|
||||||
order?: string;
|
order?: string;
|
||||||
|
@ -66,6 +67,7 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setMenuAnchor(undefined),
|
onDeactivate: () => setMenuAnchor(undefined),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu variant="Surface">
|
<Menu variant="Surface">
|
||||||
|
@ -202,6 +204,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setMenuAnchor(undefined),
|
onDeactivate: () => setMenuAnchor(undefined),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu variant="Surface" style={{ width: toRem(250) }}>
|
<Menu variant="Surface" style={{ width: toRem(250) }}>
|
||||||
|
|
|
@ -3,13 +3,17 @@ import React, { MouseEventHandler, useMemo } from 'react';
|
||||||
import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk';
|
import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk';
|
||||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||||
import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds';
|
import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds';
|
||||||
|
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import {
|
import {
|
||||||
|
factoryRenderLinkifyWithMention,
|
||||||
getReactCustomHtmlParser,
|
getReactCustomHtmlParser,
|
||||||
|
LINKIFY_OPTS,
|
||||||
makeHighlightRegex,
|
makeHighlightRegex,
|
||||||
|
makeMentionCustomProps,
|
||||||
|
renderMatrixMention,
|
||||||
} from '../../plugins/react-custom-html-parser';
|
} from '../../plugins/react-custom-html-parser';
|
||||||
import { getMxIdLocalPart, isRoomId, isUserId } from '../../utils/matrix';
|
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||||
import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
|
|
||||||
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
||||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
|
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
|
||||||
import {
|
import {
|
||||||
|
@ -31,8 +35,9 @@ import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../.
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { ResultItem } from './useMessageSearch';
|
import { ResultItem } from './useMessageSearch';
|
||||||
import { SequenceCard } from '../../components/sequence-card';
|
import { SequenceCard } from '../../components/sequence-card';
|
||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
|
||||||
import { UserAvatar } from '../../components/user-avatar';
|
import { UserAvatar } from '../../components/user-avatar';
|
||||||
|
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
|
||||||
|
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
|
||||||
|
|
||||||
type SearchResultGroupProps = {
|
type SearchResultGroupProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -51,38 +56,29 @@ export function SearchResultGroup({
|
||||||
onOpen,
|
onOpen,
|
||||||
}: SearchResultGroupProps) {
|
}: SearchResultGroupProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
|
||||||
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
|
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
|
||||||
|
|
||||||
|
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||||
|
const spoilerClickHandler = useSpoilerClickHandler();
|
||||||
|
|
||||||
|
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||||
|
() => ({
|
||||||
|
...LINKIFY_OPTS,
|
||||||
|
render: factoryRenderLinkifyWithMention((href) =>
|
||||||
|
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[mx, room, mentionClickHandler]
|
||||||
|
);
|
||||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||||
() =>
|
() =>
|
||||||
getReactCustomHtmlParser(mx, room, {
|
getReactCustomHtmlParser(mx, room.roomId, {
|
||||||
|
linkifyOpts,
|
||||||
highlightRegex,
|
highlightRegex,
|
||||||
handleSpoilerClick: (evt) => {
|
handleSpoilerClick: spoilerClickHandler,
|
||||||
const target = evt.currentTarget;
|
handleMentionClick: mentionClickHandler,
|
||||||
if (target.getAttribute('aria-pressed') === 'true') {
|
|
||||||
evt.stopPropagation();
|
|
||||||
target.setAttribute('aria-pressed', 'false');
|
|
||||||
target.style.cursor = 'initial';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleMentionClick: (evt) => {
|
|
||||||
const target = evt.currentTarget;
|
|
||||||
const mentionId = target.getAttribute('data-mention-id');
|
|
||||||
if (typeof mentionId !== 'string') return;
|
|
||||||
if (isUserId(mentionId)) {
|
|
||||||
openProfileViewer(mentionId, room.roomId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
|
||||||
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
|
||||||
else navigateRoom(mentionId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openJoinAlias(mentionId);
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
[mx, room, highlightRegex, navigateRoom, navigateSpace]
|
[mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
|
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
|
||||||
|
@ -101,6 +97,7 @@ export function SearchResultGroup({
|
||||||
mediaAutoLoad={mediaAutoLoad}
|
mediaAutoLoad={mediaAutoLoad}
|
||||||
urlPreview={urlPreview}
|
urlPreview={urlPreview}
|
||||||
htmlReactParserOptions={htmlReactParserOptions}
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
linkifyOpts={linkifyOpts}
|
||||||
highlightRegex={highlightRegex}
|
highlightRegex={highlightRegex}
|
||||||
outlineAttachment
|
outlineAttachment
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const RoomNavCategoryButton = as<'button', { closed?: boolean }>(
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<Text size="O400" priority="400" truncate>
|
<Text size="O400" priority="300" truncate>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|
|
@ -28,31 +28,31 @@ import { useRoomUnread } from '../../state/hooks/unread';
|
||||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||||
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||||
import { copyToClipboard } from '../../utils/dom';
|
import { copyToClipboard } from '../../utils/dom';
|
||||||
import { getOriginBaseUrl, withOriginBaseUrl } from '../../pages/pathUtils';
|
|
||||||
import { markAsRead } from '../../../client/action/notifications';
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
|
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
|
||||||
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
||||||
import { TypingIndicator } from '../../components/typing-indicator';
|
import { TypingIndicator } from '../../components/typing-indicator';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
||||||
|
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
|
||||||
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
|
|
||||||
type RoomNavItemMenuProps = {
|
type RoomNavItemMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
linkPath: string;
|
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
};
|
};
|
||||||
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||||
({ room, linkPath, requestClose }, ref) => {
|
({ room, requestClose }, ref) => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const { hashRouter } = useClientConfig();
|
|
||||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(room.roomId);
|
markAsRead(mx, room.roomId);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,7 +62,9 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath));
|
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||||
|
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||||
|
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -269,13 +271,10 @@ export function RoomNavItem({
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RoomNavItemMenu
|
<RoomNavItemMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
|
||||||
room={room}
|
|
||||||
linkPath={linkPath}
|
|
||||||
requestClose={() => setMenuAnchor(undefined)}
|
|
||||||
/>
|
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -35,7 +35,6 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
import * as css from './MembersDrawer.css';
|
import * as css from './MembersDrawer.css';
|
||||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { Membership } from '../../../types/matrix/room';
|
import { Membership } from '../../../types/matrix/room';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
|
@ -55,6 +54,7 @@ import { millify } from '../../plugins/millify';
|
||||||
import { ScrollTopContainer } from '../../components/scroll-top-container';
|
import { ScrollTopContainer } from '../../components/scroll-top-container';
|
||||||
import { UserAvatar } from '../../components/user-avatar';
|
import { UserAvatar } from '../../components/user-avatar';
|
||||||
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
export const MembershipFilters = {
|
export const MembershipFilters = {
|
||||||
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
|
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
|
||||||
|
@ -167,13 +167,13 @@ const getRoomMemberStr: SearchItemStrGetter<RoomMember> = (m, query) =>
|
||||||
|
|
||||||
type MembersDrawerProps = {
|
type MembersDrawerProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
members: RoomMember[];
|
||||||
};
|
};
|
||||||
export function MembersDrawer({ room }: MembersDrawerProps) {
|
export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||||
const members = useRoomMembers(mx, room.roomId);
|
|
||||||
const getPowerLevelTag = usePowerLevelTags();
|
const getPowerLevelTag = usePowerLevelTags();
|
||||||
const fetchingMembers = members.length < room.getJoinedMemberCount();
|
const fetchingMembers = members.length < room.getJoinedMemberCount();
|
||||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||||
|
@ -300,6 +300,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu style={{ padding: config.space.S100 }}>
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
|
@ -358,6 +359,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu style={{ padding: config.space.S100 }}>
|
<Menu style={{ padding: config.space.S100 }}>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Box, Line } from 'folds';
|
import { Box, Line } from 'folds';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { RoomView } from './RoomView';
|
import { RoomView } from './RoomView';
|
||||||
import { MembersDrawer } from './MembersDrawer';
|
import { MembersDrawer } from './MembersDrawer';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
|
@ -8,14 +9,32 @@ import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
||||||
import { useRoom } from '../../hooks/useRoom';
|
import { useRoom } from '../../hooks/useRoom';
|
||||||
|
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||||
|
|
||||||
export function Room() {
|
export function Room() {
|
||||||
const { eventId } = useParams();
|
const { eventId } = useParams();
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
|
const members = useRoomMembers(mx, room.roomId);
|
||||||
|
|
||||||
|
useKeyDown(
|
||||||
|
window,
|
||||||
|
useCallback(
|
||||||
|
(evt) => {
|
||||||
|
if (isKeyHotkey('escape', evt)) {
|
||||||
|
markAsRead(mx, room.roomId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mx, room.roomId]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PowerLevelsContextProvider value={powerLevels}>
|
<PowerLevelsContextProvider value={powerLevels}>
|
||||||
|
@ -24,7 +43,7 @@ export function Room() {
|
||||||
{screenSize === ScreenSize.Desktop && isDrawer && (
|
{screenSize === ScreenSize.Desktop && isDrawer && (
|
||||||
<>
|
<>
|
||||||
<Line variant="Background" direction="Vertical" size="300" />
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
<MembersDrawer key={room.roomId} room={room} />
|
<MembersDrawer key={room.roomId} room={room} members={members} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk';
|
import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
|
@ -56,7 +56,6 @@ import {
|
||||||
} from '../../components/editor';
|
} from '../../components/editor';
|
||||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart } from '../../utils/matrix';
|
import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart } from '../../utils/matrix';
|
||||||
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
|
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
|
||||||
import { useFilePicker } from '../../hooks/useFilePicker';
|
import { useFilePicker } from '../../hooks/useFilePicker';
|
||||||
|
@ -95,6 +94,7 @@ import {
|
||||||
} from './msgContent';
|
} from './msgContent';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import {
|
import {
|
||||||
|
getAllParents,
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
parseReplyBody,
|
parseReplyBody,
|
||||||
parseReplyFormattedBody,
|
parseReplyFormattedBody,
|
||||||
|
@ -107,6 +107,7 @@ import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
|
||||||
import { mobileOrTablet } from '../../utils/user-agent';
|
import { mobileOrTablet } from '../../utils/user-agent';
|
||||||
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
||||||
import { ReplyLayout } from '../../components/message';
|
import { ReplyLayout } from '../../components/message';
|
||||||
|
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||||
|
|
||||||
interface RoomInputProps {
|
interface RoomInputProps {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
|
@ -121,6 +122,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
|
||||||
const commands = useCommands(mx, room);
|
const commands = useCommands(mx, room);
|
||||||
const emojiBtnRef = useRef<HTMLButtonElement>(null);
|
const emojiBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
|
|
||||||
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
||||||
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
||||||
|
@ -133,13 +135,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
const uploadBoardHandlers = useRef<UploadBoardImperativeHandlers>();
|
const uploadBoardHandlers = useRef<UploadBoardImperativeHandlers>();
|
||||||
|
|
||||||
const imagePackRooms: Room[] = useMemo(() => {
|
const imagePackRooms: Room[] = useMemo(() => {
|
||||||
const allParentSpaces = [roomId, ...(initMatrix.roomList?.getAllParentSpaces(roomId) ?? [])];
|
const allParentSpaces = [roomId].concat(Array.from(getAllParents(roomToParents, roomId)));
|
||||||
return allParentSpaces.reduce<Room[]>((list, rId) => {
|
return allParentSpaces.reduce<Room[]>((list, rId) => {
|
||||||
const r = mx.getRoom(rId);
|
const r = mx.getRoom(rId);
|
||||||
if (r) list.push(r);
|
if (r) list.push(r);
|
||||||
return list;
|
return list;
|
||||||
}, []);
|
}, []);
|
||||||
}, [mx, roomId]);
|
}, [mx, roomId, roomToParents]);
|
||||||
|
|
||||||
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||||
const [autocompleteQuery, setAutocompleteQuery] =
|
const [autocompleteQuery, setAutocompleteQuery] =
|
||||||
|
|
|
@ -28,7 +28,7 @@ import classNames from 'classnames';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import to from 'await-to-js';
|
import to from 'await-to-js';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
|
@ -45,13 +45,12 @@ import {
|
||||||
toRem,
|
toRem,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
|
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||||
import {
|
import {
|
||||||
decryptFile,
|
decryptFile,
|
||||||
eventWithShortcode,
|
eventWithShortcode,
|
||||||
factoryEventSentBy,
|
factoryEventSentBy,
|
||||||
getMxIdLocalPart,
|
getMxIdLocalPart,
|
||||||
isRoomId,
|
|
||||||
isUserId,
|
|
||||||
} from '../../utils/matrix';
|
} from '../../utils/matrix';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
|
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
|
||||||
|
@ -70,10 +69,17 @@ import {
|
||||||
ImageContent,
|
ImageContent,
|
||||||
EventContent,
|
EventContent,
|
||||||
} from '../../components/message';
|
} from '../../components/message';
|
||||||
import { getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser';
|
import {
|
||||||
|
factoryRenderLinkifyWithMention,
|
||||||
|
getReactCustomHtmlParser,
|
||||||
|
LINKIFY_OPTS,
|
||||||
|
makeMentionCustomProps,
|
||||||
|
renderMatrixMention,
|
||||||
|
} from '../../plugins/react-custom-html-parser';
|
||||||
import {
|
import {
|
||||||
canEditEvent,
|
canEditEvent,
|
||||||
decryptAllTimelineEvent,
|
decryptAllTimelineEvent,
|
||||||
|
getAllParents,
|
||||||
getEditedEvent,
|
getEditedEvent,
|
||||||
getEventReactions,
|
getEventReactions,
|
||||||
getLatestEditableEvt,
|
getLatestEditableEvt,
|
||||||
|
@ -84,7 +90,7 @@ import {
|
||||||
} from '../../utils/room';
|
} from '../../utils/room';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
||||||
import { Reactions, Message, Event, EncryptedContent } from './message';
|
import { Reactions, Message, Event, EncryptedContent } from './message';
|
||||||
import { useMemberEventParser } from '../../hooks/useMemberEventParser';
|
import { useMemberEventParser } from '../../hooks/useMemberEventParser';
|
||||||
|
@ -103,13 +109,16 @@ import { createMentionElement, isEmptyEditor, moveCursor } from '../../component
|
||||||
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
||||||
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
|
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
import cons from '../../../client/state/cons';
|
|
||||||
import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
|
import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
|
||||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||||
import { Image } from '../../components/media';
|
import { Image } from '../../components/media';
|
||||||
import { ImageViewer } from '../../components/image-viewer';
|
import { ImageViewer } from '../../components/image-viewer';
|
||||||
|
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||||
|
import { useRoomUnread } from '../../state/hooks/unread';
|
||||||
|
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||||
|
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
|
||||||
|
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
|
||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
|
|
||||||
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
|
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
|
||||||
|
@ -443,19 +452,22 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
const canRedact = canDoAction('redact', myPowerLevel);
|
const canRedact = canDoAction('redact', myPowerLevel);
|
||||||
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
|
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
|
||||||
const [editId, setEditId] = useState<string>();
|
const [editId, setEditId] = useState<string>();
|
||||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
|
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||||
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||||
|
const spoilerClickHandler = useSpoilerClickHandler();
|
||||||
|
|
||||||
const imagePackRooms: Room[] = useMemo(() => {
|
const imagePackRooms: Room[] = useMemo(() => {
|
||||||
const allParentSpaces = [
|
const allParentSpaces = [room.roomId].concat(
|
||||||
room.roomId,
|
Array.from(getAllParents(roomToParents, room.roomId))
|
||||||
...(initMatrix.roomList?.getAllParentSpaces(room.roomId) ?? []),
|
);
|
||||||
];
|
|
||||||
return allParentSpaces.reduce<Room[]>((list, rId) => {
|
return allParentSpaces.reduce<Room[]>((list, rId) => {
|
||||||
const r = mx.getRoom(rId);
|
const r = mx.getRoom(rId);
|
||||||
if (r) list.push(r);
|
if (r) list.push(r);
|
||||||
return list;
|
return list;
|
||||||
}, []);
|
}, []);
|
||||||
}, [mx, room]);
|
}, [mx, room, roomToParents]);
|
||||||
|
|
||||||
const [unreadInfo, setUnreadInfo] = useState(() => getRoomUnreadInfo(room, true));
|
const [unreadInfo, setUnreadInfo] = useState(() => getRoomUnreadInfo(room, true));
|
||||||
const readUptoEventIdRef = useRef<string>();
|
const readUptoEventIdRef = useRef<string>();
|
||||||
|
@ -484,34 +496,23 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
>();
|
>();
|
||||||
const alive = useAlive();
|
const alive = useAlive();
|
||||||
|
|
||||||
|
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||||
|
() => ({
|
||||||
|
...LINKIFY_OPTS,
|
||||||
|
render: factoryRenderLinkifyWithMention((href) =>
|
||||||
|
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[mx, room, mentionClickHandler]
|
||||||
|
);
|
||||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||||
() =>
|
() =>
|
||||||
getReactCustomHtmlParser(mx, room, {
|
getReactCustomHtmlParser(mx, room.roomId, {
|
||||||
handleSpoilerClick: (evt) => {
|
linkifyOpts,
|
||||||
const target = evt.currentTarget;
|
handleSpoilerClick: spoilerClickHandler,
|
||||||
if (target.getAttribute('aria-pressed') === 'true') {
|
handleMentionClick: mentionClickHandler,
|
||||||
evt.stopPropagation();
|
|
||||||
target.setAttribute('aria-pressed', 'false');
|
|
||||||
target.style.cursor = 'initial';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleMentionClick: (evt) => {
|
|
||||||
const target = evt.currentTarget;
|
|
||||||
const mentionId = target.getAttribute('data-mention-id');
|
|
||||||
if (typeof mentionId !== 'string') return;
|
|
||||||
if (isUserId(mentionId)) {
|
|
||||||
openProfileViewer(mentionId, room.roomId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
|
||||||
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
|
||||||
else navigateRoom(mentionId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openJoinAlias(mentionId);
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
[mx, room, navigateRoom, navigateSpace]
|
[mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler]
|
||||||
);
|
);
|
||||||
const parseMemberEvent = useMemberEventParser();
|
const parseMemberEvent = useMemberEventParser();
|
||||||
|
|
||||||
|
@ -594,7 +595,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
// so timeline can be updated with evt like: edits, reactions etc
|
// so timeline can be updated with evt like: edits, reactions etc
|
||||||
if (atBottomRef.current) {
|
if (atBottomRef.current) {
|
||||||
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
|
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
|
||||||
requestAnimationFrame(() => markAsRead(mEvt.getRoomId()));
|
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.hasFocus()) {
|
if (document.hasFocus()) {
|
||||||
|
@ -655,15 +656,15 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
|
|
||||||
const tryAutoMarkAsRead = useCallback(() => {
|
const tryAutoMarkAsRead = useCallback(() => {
|
||||||
if (!unreadInfo) {
|
if (!unreadInfo) {
|
||||||
requestAnimationFrame(() => markAsRead(room.roomId));
|
requestAnimationFrame(() => markAsRead(mx, room.roomId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const evtTimeline = getEventTimeline(room, unreadInfo.readUptoEventId);
|
const evtTimeline = getEventTimeline(room, unreadInfo.readUptoEventId);
|
||||||
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
||||||
if (latestTimeline === room.getLiveTimeline()) {
|
if (latestTimeline === room.getLiveTimeline()) {
|
||||||
requestAnimationFrame(() => markAsRead(room.roomId));
|
requestAnimationFrame(() => markAsRead(mx, room.roomId));
|
||||||
}
|
}
|
||||||
}, [room, unreadInfo]);
|
}, [mx, room, unreadInfo]);
|
||||||
|
|
||||||
const debounceSetAtBottom = useDebounce(
|
const debounceSetAtBottom = useDebounce(
|
||||||
useCallback((entry: IntersectionObserverEntry) => {
|
useCallback((entry: IntersectionObserverEntry) => {
|
||||||
|
@ -794,15 +795,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
|
|
||||||
// Remove unreadInfo on mark as read
|
// Remove unreadInfo on mark as read
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFullRead = (rId: string) => {
|
if (!unread) {
|
||||||
if (rId !== room.roomId) return;
|
|
||||||
setUnreadInfo(undefined);
|
setUnreadInfo(undefined);
|
||||||
};
|
}
|
||||||
initMatrix.notifications?.on(cons.events.notifications.FULL_READ, handleFullRead);
|
}, [unread]);
|
||||||
return () => {
|
|
||||||
initMatrix.notifications?.removeListener(cons.events.notifications.FULL_READ, handleFullRead);
|
|
||||||
};
|
|
||||||
}, [room]);
|
|
||||||
|
|
||||||
// scroll out of view msg editor in view.
|
// scroll out of view msg editor in view.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -821,6 +817,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
}, [scrollToElement, editId]);
|
}, [scrollToElement, editId]);
|
||||||
|
|
||||||
const handleJumpToLatest = () => {
|
const handleJumpToLatest = () => {
|
||||||
|
if (eventId) {
|
||||||
|
navigateRoom(room.roomId, undefined, { replace: true });
|
||||||
|
}
|
||||||
setTimeline(getInitialTimeline(room));
|
setTimeline(getInitialTimeline(room));
|
||||||
scrollToBottomRef.current.count += 1;
|
scrollToBottomRef.current.count += 1;
|
||||||
scrollToBottomRef.current.smooth = false;
|
scrollToBottomRef.current.smooth = false;
|
||||||
|
@ -834,7 +833,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(room.roomId);
|
markAsRead(mx, room.roomId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenReply: MouseEventHandler<HTMLButtonElement> = useCallback(
|
const handleOpenReply: MouseEventHandler<HTMLButtonElement> = useCallback(
|
||||||
|
@ -1038,6 +1037,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
mediaAutoLoad={mediaAutoLoad}
|
mediaAutoLoad={mediaAutoLoad}
|
||||||
urlPreview={showUrlPreview}
|
urlPreview={showUrlPreview}
|
||||||
htmlReactParserOptions={htmlReactParserOptions}
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
linkifyOpts={linkifyOpts}
|
||||||
outlineAttachment={messageLayout === 2}
|
outlineAttachment={messageLayout === 2}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -1134,6 +1134,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
mediaAutoLoad={mediaAutoLoad}
|
mediaAutoLoad={mediaAutoLoad}
|
||||||
urlPreview={showUrlPreview}
|
urlPreview={showUrlPreview}
|
||||||
htmlReactParserOptions={htmlReactParserOptions}
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
linkifyOpts={linkifyOpts}
|
||||||
outlineAttachment={messageLayout === 2}
|
outlineAttachment={messageLayout === 2}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { Box, Button, Spinner, Text, color } from 'folds';
|
||||||
|
|
||||||
import * as css from './RoomTombstone.css';
|
import * as css from './RoomTombstone.css';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { genRoomVia } from '../../../util/matrixUtil';
|
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { Membership } from '../../../types/matrix/room';
|
import { Membership } from '../../../types/matrix/room';
|
||||||
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
|
|
||||||
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
|
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
|
||||||
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
|
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
|
||||||
|
@ -17,7 +17,7 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
|
||||||
const [joinState, handleJoin] = useAsyncCallback(
|
const [joinState, handleJoin] = useAsyncCallback(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const currentRoom = mx.getRoom(roomId);
|
const currentRoom = mx.getRoom(roomId);
|
||||||
const via = currentRoom ? genRoomVia(currentRoom) : [];
|
const via = currentRoom ? getViaServers(currentRoom) : [];
|
||||||
return mx.joinRoom(replacementRoomId, {
|
return mx.joinRoom(replacementRoomId, {
|
||||||
viaServers: via,
|
viaServers: via,
|
||||||
});
|
});
|
||||||
|
@ -40,28 +40,30 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{replacementRoom?.getMyMembership() === Membership.Join ||
|
<Box shrink="No">
|
||||||
joinState.status === AsyncStatus.Success ? (
|
{replacementRoom?.getMyMembership() === Membership.Join ||
|
||||||
<Button onClick={handleOpen} size="300" variant="Success" fill="Solid" radii="300">
|
joinState.status === AsyncStatus.Success ? (
|
||||||
<Text size="B300">Open New Room</Text>
|
<Button onClick={handleOpen} size="300" variant="Success" fill="Solid" radii="300">
|
||||||
</Button>
|
<Text size="B300">Open New Room</Text>
|
||||||
) : (
|
</Button>
|
||||||
<Button
|
) : (
|
||||||
onClick={handleJoin}
|
<Button
|
||||||
size="300"
|
onClick={handleJoin}
|
||||||
variant="Primary"
|
size="300"
|
||||||
fill="Solid"
|
variant="Primary"
|
||||||
radii="300"
|
fill="Solid"
|
||||||
before={
|
radii="300"
|
||||||
joinState.status === AsyncStatus.Loading && (
|
before={
|
||||||
<Spinner size="100" variant="Primary" fill="Solid" />
|
joinState.status === AsyncStatus.Loading && (
|
||||||
)
|
<Spinner size="100" variant="Primary" fill="Solid" />
|
||||||
}
|
)
|
||||||
disabled={joinState.status === AsyncStatus.Loading}
|
}
|
||||||
>
|
disabled={joinState.status === AsyncStatus.Loading}
|
||||||
<Text size="B300">Join New Room</Text>
|
>
|
||||||
</Button>
|
<Text size="B300">Join New Room</Text>
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</RoomInputPlaceholder>
|
</RoomInputPlaceholder>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useCallback, useRef } from 'react';
|
||||||
import { Box, Text, config } from 'folds';
|
import { Box, Text, config } from 'folds';
|
||||||
import { EventType, Room } from 'matrix-js-sdk';
|
import { EventType, Room } from 'matrix-js-sdk';
|
||||||
|
import { ReactEditor } from 'slate-react';
|
||||||
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||||
import { StateEvent } from '../../../types/matrix/room';
|
import { StateEvent } from '../../../types/matrix/room';
|
||||||
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||||
|
@ -15,10 +16,41 @@ import { RoomInput } from './RoomInput';
|
||||||
import { RoomViewFollowing } from './RoomViewFollowing';
|
import { RoomViewFollowing } from './RoomViewFollowing';
|
||||||
import { Page } from '../../components/page';
|
import { Page } from '../../components/page';
|
||||||
import { RoomViewHeader } from './RoomViewHeader';
|
import { RoomViewHeader } from './RoomViewHeader';
|
||||||
|
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
|
import { editableActiveElement } from '../../utils/dom';
|
||||||
|
import navigation from '../../../client/state/navigation';
|
||||||
|
|
||||||
|
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||||
|
const { code } = evt;
|
||||||
|
if (evt.metaKey || evt.altKey || evt.ctrlKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// do not focus on F keys
|
||||||
|
if (/^F\d+$/.test(code)) return false;
|
||||||
|
|
||||||
|
// do not focus on numlock/scroll lock
|
||||||
|
if (
|
||||||
|
code.startsWith('OS') ||
|
||||||
|
code.startsWith('Meta') ||
|
||||||
|
code.startsWith('Shift') ||
|
||||||
|
code.startsWith('Alt') ||
|
||||||
|
code.startsWith('Control') ||
|
||||||
|
code.startsWith('Arrow') ||
|
||||||
|
code === 'Tab' ||
|
||||||
|
code === 'Space' ||
|
||||||
|
code === 'Enter' ||
|
||||||
|
code === 'NumLock' ||
|
||||||
|
code === 'ScrollLock'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
||||||
const roomInputRef = useRef(null);
|
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||||
const roomViewRef = useRef(null);
|
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { roomId } = room;
|
const { roomId } = room;
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
|
@ -33,6 +65,25 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
|
||||||
? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))
|
? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
useKeyDown(
|
||||||
|
window,
|
||||||
|
useCallback(
|
||||||
|
(evt) => {
|
||||||
|
if (editableActiveElement()) return;
|
||||||
|
if (
|
||||||
|
document.body.lastElementChild?.className !== 'ReactModalPortal' ||
|
||||||
|
navigation.isRawModalVisible
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (shouldFocusMessageField(evt) || isKeyHotkey('mod+v', evt)) {
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page ref={roomViewRef}>
|
<Page ref={roomViewRef}>
|
||||||
<RoomViewHeader />
|
<RoomViewHeader />
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
|
import { useRoomLatestRenderedEvent } from '../../hooks/useRoomLatestRenderedEvent';
|
||||||
import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
|
import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
|
||||||
import { EventReaders } from '../../components/event-readers';
|
import { EventReaders } from '../../components/event-readers';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
export type RoomViewFollowingProps = {
|
export type RoomViewFollowingProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -50,6 +51,7 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setOpen(false),
|
onDeactivate: () => setOpen(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal variant="Surface" size="300">
|
<Modal variant="Surface" size="300">
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
PopOut,
|
PopOut,
|
||||||
RectCords,
|
RectCords,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { JoinRule, Room } from 'matrix-js-sdk';
|
import { JoinRule, Room } from 'matrix-js-sdk';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
|
||||||
|
@ -35,15 +35,8 @@ import { useRoom } from '../../hooks/useRoom';
|
||||||
import { useSetSetting } from '../../state/hooks/settings';
|
import { useSetSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||||
import {
|
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
|
||||||
getHomeSearchPath,
|
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
|
||||||
getOriginBaseUrl,
|
|
||||||
getSpaceSearchPath,
|
|
||||||
joinPathComponent,
|
|
||||||
withOriginBaseUrl,
|
|
||||||
withSearchParam,
|
|
||||||
} from '../../pages/pathUtils';
|
|
||||||
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
|
|
||||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||||
import * as css from './RoomViewHeader.css';
|
import * as css from './RoomViewHeader.css';
|
||||||
import { useRoomUnread } from '../../state/hooks/unread';
|
import { useRoomUnread } from '../../state/hooks/unread';
|
||||||
|
@ -55,127 +48,127 @@ import { copyToClipboard } from '../../utils/dom';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||||
import { mDirectAtom } from '../../state/mDirectList';
|
import { mDirectAtom } from '../../state/mDirectList';
|
||||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
||||||
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
|
|
||||||
type RoomMenuProps = {
|
type RoomMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
linkPath: string;
|
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
};
|
};
|
||||||
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
|
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose }, ref) => {
|
||||||
({ room, linkPath, requestClose }, ref) => {
|
const mx = useMatrixClient();
|
||||||
const mx = useMatrixClient();
|
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||||
const { hashRouter } = useClientConfig();
|
const powerLevels = usePowerLevelsContext();
|
||||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||||
const powerLevels = usePowerLevelsContext();
|
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
|
||||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(room.roomId);
|
markAsRead(mx, room.roomId);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(room.roomId);
|
openInviteUser(room.roomId);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath));
|
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||||
requestClose();
|
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||||
};
|
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
||||||
|
requestClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleRoomSettings = () => {
|
const handleRoomSettings = () => {
|
||||||
toggleRoomSettings(room.roomId);
|
toggleRoomSettings(room.roomId);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleMarkAsRead}
|
onClick={handleMarkAsRead}
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.CheckTwice} />}
|
after={<Icon size="100" src={Icons.CheckTwice} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
disabled={!unread}
|
disabled={!unread}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Mark as Read
|
Mark as Read
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Box>
|
</Box>
|
||||||
<Line variant="Surface" size="300" />
|
<Line variant="Surface" size="300" />
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleInvite}
|
onClick={handleInvite}
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
fill="None"
|
fill="None"
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Invite
|
Invite
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleCopyLink}
|
onClick={handleCopyLink}
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.Link} />}
|
after={<Icon size="100" src={Icons.Link} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Copy Link
|
Copy Link
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleRoomSettings}
|
onClick={handleRoomSettings}
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.Setting} />}
|
after={<Icon size="100" src={Icons.Setting} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Room Settings
|
Room Settings
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Box>
|
</Box>
|
||||||
<Line variant="Surface" size="300" />
|
<Line variant="Surface" size="300" />
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<UseStateProvider initial={false}>
|
<UseStateProvider initial={false}>
|
||||||
{(promptLeave, setPromptLeave) => (
|
{(promptLeave, setPromptLeave) => (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => setPromptLeave(true)}
|
onClick={() => setPromptLeave(true)}
|
||||||
variant="Critical"
|
variant="Critical"
|
||||||
fill="None"
|
fill="None"
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-pressed={promptLeave}
|
aria-pressed={promptLeave}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Leave Room
|
Leave Room
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{promptLeave && (
|
{promptLeave && (
|
||||||
<LeaveRoomPrompt
|
<LeaveRoomPrompt
|
||||||
roomId={room.roomId}
|
roomId={room.roomId}
|
||||||
onDone={requestClose}
|
onDone={requestClose}
|
||||||
onCancel={() => setPromptLeave(false)}
|
onCancel={() => setPromptLeave(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</UseStateProvider>
|
</UseStateProvider>
|
||||||
</Box>
|
</Box>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export function RoomViewHeader() {
|
export function RoomViewHeader() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -194,8 +187,6 @@ export function RoomViewHeader() {
|
||||||
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
|
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
|
||||||
|
|
||||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||||
const location = useLocation();
|
|
||||||
const currentPath = joinPathComponent(location);
|
|
||||||
|
|
||||||
const handleSearchClick = () => {
|
const handleSearchClick = () => {
|
||||||
const searchParams: _SearchPathSearchParams = {
|
const searchParams: _SearchPathSearchParams = {
|
||||||
|
@ -240,6 +231,7 @@ export function RoomViewHeader() {
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
onDeactivate: () => setViewTopic(false),
|
onDeactivate: () => setViewTopic(false),
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RoomTopicViewer
|
<RoomTopicViewer
|
||||||
|
@ -331,13 +323,10 @@ export function RoomViewHeader() {
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RoomMenu
|
<RoomMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
|
||||||
room={room}
|
|
||||||
linkPath={currentPath}
|
|
||||||
requestClose={() => setMenuAnchor(undefined)}
|
|
||||||
/>
|
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -51,7 +51,7 @@ import {
|
||||||
getMemberAvatarMxc,
|
getMemberAvatarMxc,
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
} from '../../../utils/room';
|
} from '../../../utils/room';
|
||||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../../utils/matrix';
|
import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias } from '../../../utils/matrix';
|
||||||
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||||
|
@ -63,17 +63,10 @@ import { EmojiBoard } from '../../../components/emoji-board';
|
||||||
import { ReactionViewer } from '../reaction-viewer';
|
import { ReactionViewer } from '../reaction-viewer';
|
||||||
import { MessageEditor } from './MessageEditor';
|
import { MessageEditor } from './MessageEditor';
|
||||||
import { UserAvatar } from '../../../components/user-avatar';
|
import { UserAvatar } from '../../../components/user-avatar';
|
||||||
import { useSpaceOptionally } from '../../../hooks/useSpace';
|
|
||||||
import { useDirectSelected } from '../../../hooks/router/useDirectSelected';
|
|
||||||
import {
|
|
||||||
getDirectRoomPath,
|
|
||||||
getHomeRoomPath,
|
|
||||||
getOriginBaseUrl,
|
|
||||||
getSpaceRoomPath,
|
|
||||||
withOriginBaseUrl,
|
|
||||||
} from '../../../pages/pathUtils';
|
|
||||||
import { copyToClipboard } from '../../../utils/dom';
|
import { copyToClipboard } from '../../../utils/dom';
|
||||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
import { getMatrixToRoomEvent } from '../../../plugins/matrix-to';
|
||||||
|
import { getViaServers } from '../../../plugins/via-servers';
|
||||||
|
|
||||||
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
||||||
|
|
||||||
|
@ -148,6 +141,7 @@ export const MessageAllReactionItem = as<
|
||||||
returnFocusOnDeactivate: false,
|
returnFocusOnDeactivate: false,
|
||||||
onDeactivate: () => handleClose(),
|
onDeactivate: () => handleClose(),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal variant="Surface" size="300">
|
<Modal variant="Surface" size="300">
|
||||||
|
@ -201,6 +195,7 @@ export const MessageReadReceiptItem = as<
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: handleClose,
|
onDeactivate: handleClose,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal variant="Surface" size="300">
|
<Modal variant="Surface" size="300">
|
||||||
|
@ -278,6 +273,7 @@ export const MessageSourceCodeItem = as<
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: handleClose,
|
onDeactivate: handleClose,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal variant="Surface" size="500">
|
<Modal variant="Surface" size="500">
|
||||||
|
@ -317,23 +313,13 @@ export const MessageCopyLinkItem = as<
|
||||||
}
|
}
|
||||||
>(({ room, mEvent, onClose, ...props }, ref) => {
|
>(({ room, mEvent, onClose, ...props }, ref) => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const { hashRouter } = useClientConfig();
|
|
||||||
const space = useSpaceOptionally();
|
|
||||||
const directSelected = useDirectSelected();
|
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||||
let eventPath = getHomeRoomPath(roomIdOrAlias, mEvent.getId());
|
const eventId = mEvent.getId();
|
||||||
if (space) {
|
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||||
eventPath = getSpaceRoomPath(
|
if (!eventId) return;
|
||||||
getCanonicalAliasOrRoomId(mx, space.roomId),
|
copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
|
||||||
roomIdOrAlias,
|
|
||||||
mEvent.getId()
|
|
||||||
);
|
|
||||||
} else if (directSelected) {
|
|
||||||
eventPath = getDirectRoomPath(roomIdOrAlias, mEvent.getId());
|
|
||||||
}
|
|
||||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), eventPath));
|
|
||||||
onClose?.();
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -401,6 +387,7 @@ export const MessageDeleteItem = as<
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: handleClose,
|
onDeactivate: handleClose,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog variant="Surface">
|
<Dialog variant="Surface">
|
||||||
|
@ -530,6 +517,7 @@ export const MessageReportItem = as<
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: handleClose,
|
onDeactivate: handleClose,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog variant="Surface">
|
<Dialog variant="Surface">
|
||||||
|
@ -875,6 +863,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu>
|
<Menu>
|
||||||
|
@ -1089,6 +1078,7 @@ export const Event = as<'div', EventProps>(
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu {...props} ref={ref}>
|
<Menu {...props} ref={ref}>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { Reaction, ReactionTooltipMsg } from '../../../components/message';
|
||||||
import { useRelations } from '../../../hooks/useRelations';
|
import { useRelations } from '../../../hooks/useRelations';
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
import { ReactionViewer } from '../reaction-viewer';
|
import { ReactionViewer } from '../reaction-viewer';
|
||||||
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
|
||||||
export type ReactionsProps = {
|
export type ReactionsProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -105,6 +106,7 @@ export const Reactions = as<'div', ReactionsProps>(
|
||||||
returnFocusOnDeactivate: false,
|
returnFocusOnDeactivate: false,
|
||||||
onDeactivate: () => setViewer(false),
|
onDeactivate: () => setViewer(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal variant="Surface" size="300">
|
<Modal variant="Surface" size="300">
|
||||||
|
|
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal file
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { getRoomSearchParams } from '../../pages/pathSearchParam';
|
||||||
|
import { decodeSearchParamValueArray } from '../../pages/pathUtils';
|
||||||
|
|
||||||
|
export const useSearchParamsViaServers = (): string[] | undefined => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const roomSearchParams = useMemo(() => getRoomSearchParams(searchParams), [searchParams]);
|
||||||
|
const viaServers = roomSearchParams.viaServers
|
||||||
|
? decodeSearchParamValueArray(roomSearchParams.viaServers)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return viaServers;
|
||||||
|
};
|
|
@ -1,10 +1,9 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
import initMatrix from '../../client/initMatrix';
|
|
||||||
|
|
||||||
export function useAccountData(eventType) {
|
export function useAccountData(eventType) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const [event, setEvent] = useState(mx.getAccountData(eventType));
|
const [event, setEvent] = useState(mx.getAccountData(eventType));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -16,7 +15,7 @@ export function useAccountData(eventType) {
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener('accountData', handleChange);
|
mx.removeListener('accountData', handleChange);
|
||||||
};
|
};
|
||||||
}, [eventType]);
|
}, [mx, eventType]);
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
/* 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];
|
|
||||||
}
|
|
|
@ -92,9 +92,9 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const devices = await Promise.all(userIds.map(hasDevices));
|
const devices = await Promise.all(userIds.map(uid => hasDevices(mx, uid)));
|
||||||
const isEncrypt = devices.every((hasDevice) => hasDevice);
|
const isEncrypt = devices.every((hasDevice) => hasDevice);
|
||||||
const result = await roomActions.createDM(userIds, isEncrypt);
|
const result = await roomActions.createDM(mx, userIds, isEncrypt);
|
||||||
navigateRoom(result.room_id);
|
navigateRoom(result.room_id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -106,7 +106,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
const roomIds = rawIds.filter(
|
const roomIds = rawIds.filter(
|
||||||
(idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias)
|
(idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias)
|
||||||
);
|
);
|
||||||
roomIds.map((id) => roomActions.join(id));
|
roomIds.map((id) => roomActions.join(mx, id));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.Leave]: {
|
[Command.Leave]: {
|
||||||
|
@ -114,12 +114,12 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
description: 'Leave current room.',
|
description: 'Leave current room.',
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
if (payload.trim() === '') {
|
if (payload.trim() === '') {
|
||||||
roomActions.leave(room.roomId);
|
mx.leave(room.roomId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rawIds = payload.split(' ');
|
const rawIds = payload.split(' ');
|
||||||
const roomIds = rawIds.filter((id) => isRoomId(id));
|
const roomIds = rawIds.filter((id) => isRoomId(id));
|
||||||
roomIds.map((id) => roomActions.leave(id));
|
roomIds.map((id) => mx.leave(id));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.Invite]: {
|
[Command.Invite]: {
|
||||||
|
@ -127,7 +127,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
|
description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const { users, reason } = parseUsersAndReason(payload);
|
const { users, reason } = parseUsersAndReason(payload);
|
||||||
users.map((id) => roomActions.invite(room.roomId, id, reason));
|
users.map((id) => mx.invite(room.roomId, id, reason));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.DisInvite]: {
|
[Command.DisInvite]: {
|
||||||
|
@ -135,7 +135,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
|
description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const { users, reason } = parseUsersAndReason(payload);
|
const { users, reason } = parseUsersAndReason(payload);
|
||||||
users.map((id) => roomActions.kick(room.roomId, id, reason));
|
users.map((id) => mx.kick(room.roomId, id, reason));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.Kick]: {
|
[Command.Kick]: {
|
||||||
|
@ -143,7 +143,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
|
description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const { users, reason } = parseUsersAndReason(payload);
|
const { users, reason } = parseUsersAndReason(payload);
|
||||||
users.map((id) => roomActions.kick(room.roomId, id, reason));
|
users.map((id) => mx.kick(room.roomId, id, reason));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.Ban]: {
|
[Command.Ban]: {
|
||||||
|
@ -151,7 +151,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
|
description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const { users, reason } = parseUsersAndReason(payload);
|
const { users, reason } = parseUsersAndReason(payload);
|
||||||
users.map((id) => roomActions.ban(room.roomId, id, reason));
|
users.map((id) => mx.ban(room.roomId, id, reason));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.UnBan]: {
|
[Command.UnBan]: {
|
||||||
|
@ -160,7 +160,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const rawIds = payload.split(' ');
|
const rawIds = payload.split(' ');
|
||||||
const users = rawIds.filter((id) => isUserId(id));
|
const users = rawIds.filter((id) => isUserId(id));
|
||||||
users.map((id) => roomActions.unban(room.roomId, id));
|
users.map((id) => mx.unban(room.roomId, id));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.Ignore]: {
|
[Command.Ignore]: {
|
||||||
|
@ -169,7 +169,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const rawIds = payload.split(' ');
|
const rawIds = payload.split(' ');
|
||||||
const userIds = rawIds.filter((id) => isUserId(id));
|
const userIds = rawIds.filter((id) => isUserId(id));
|
||||||
if (userIds.length > 0) roomActions.ignore(userIds);
|
if (userIds.length > 0) roomActions.ignore(mx, userIds);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.UnIgnore]: {
|
[Command.UnIgnore]: {
|
||||||
|
@ -178,7 +178,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const rawIds = payload.split(' ');
|
const rawIds = payload.split(' ');
|
||||||
const userIds = rawIds.filter((id) => isUserId(id));
|
const userIds = rawIds.filter((id) => isUserId(id));
|
||||||
if (userIds.length > 0) roomActions.unignore(userIds);
|
if (userIds.length > 0) roomActions.unignore(mx, userIds);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.MyRoomNick]: {
|
[Command.MyRoomNick]: {
|
||||||
|
@ -187,7 +187,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
const nick = payload.trim();
|
const nick = payload.trim();
|
||||||
if (nick === '') return;
|
if (nick === '') return;
|
||||||
roomActions.setMyRoomNick(room.roomId, nick);
|
roomActions.setMyRoomNick(mx, room.roomId, nick);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.MyRoomAvatar]: {
|
[Command.MyRoomAvatar]: {
|
||||||
|
@ -195,7 +195,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
description: 'Change profile picture in current room. Example /myroomavatar mxc://xyzabc',
|
description: 'Change profile picture in current room. Example /myroomavatar mxc://xyzabc',
|
||||||
exe: async (payload) => {
|
exe: async (payload) => {
|
||||||
if (payload.match(/^mxc:\/\/\S+$/)) {
|
if (payload.match(/^mxc:\/\/\S+$/)) {
|
||||||
roomActions.setMyRoomAvatar(room.roomId, payload);
|
roomActions.setMyRoomAvatar(mx, room.roomId, payload);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -203,14 +203,14 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
name: Command.ConvertToDm,
|
name: Command.ConvertToDm,
|
||||||
description: 'Convert room to direct message',
|
description: 'Convert room to direct message',
|
||||||
exe: async () => {
|
exe: async () => {
|
||||||
roomActions.convertToDm(room.roomId);
|
roomActions.convertToDm(mx, room.roomId);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[Command.ConvertToRoom]: {
|
[Command.ConvertToRoom]: {
|
||||||
name: Command.ConvertToRoom,
|
name: Command.ConvertToRoom,
|
||||||
description: 'Convert direct message to room',
|
description: 'Convert direct message to room',
|
||||||
exe: async () => {
|
exe: async () => {
|
||||||
roomActions.convertToRoom(room.roomId);
|
roomActions.convertToRoom(mx, room.roomId);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import initMatrix from '../../client/initMatrix';
|
|
||||||
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
|
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
|
||||||
export function useCrossSigningStatus() {
|
export function useCrossSigningStatus() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData());
|
const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData(mx));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCSEnabled) return undefined;
|
if (isCSEnabled) return undefined;
|
||||||
|
@ -20,6 +20,6 @@ export function useCrossSigningStatus() {
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener('accountData', handleAccountData);
|
mx.removeListener('accountData', handleAccountData);
|
||||||
};
|
};
|
||||||
}, [isCSEnabled === false]);
|
}, [mx, isCSEnabled]);
|
||||||
return isCSEnabled;
|
return isCSEnabled;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
import initMatrix from '../../client/initMatrix';
|
|
||||||
|
|
||||||
export function useDeviceList() {
|
export function useDeviceList() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const [deviceList, setDeviceList] = useState(null);
|
const [deviceList, setDeviceList] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -27,6 +26,6 @@ export function useDeviceList() {
|
||||||
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [mx]);
|
||||||
return deviceList;
|
return deviceList;
|
||||||
}
|
}
|
||||||
|
|
43
src/app/hooks/useMentionClickHandler.ts
Normal file
43
src/app/hooks/useMentionClickHandler.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { ReactEventHandler, useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useRoomNavigate } from './useRoomNavigate';
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
import { isRoomId, isUserId } from '../utils/matrix';
|
||||||
|
import { openProfileViewer } from '../../client/action/navigation';
|
||||||
|
import { getHomeRoomPath, withSearchParam } from '../pages/pathUtils';
|
||||||
|
import { _RoomSearchParams } from '../pages/paths';
|
||||||
|
|
||||||
|
export const useMentionClickHandler = (roomId: string): ReactEventHandler<HTMLElement> => {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleClick: ReactEventHandler<HTMLElement> = useCallback(
|
||||||
|
(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const target = evt.currentTarget;
|
||||||
|
const mentionId = target.getAttribute('data-mention-id');
|
||||||
|
if (typeof mentionId !== 'string') return;
|
||||||
|
|
||||||
|
if (isUserId(mentionId)) {
|
||||||
|
openProfileViewer(mentionId, roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = target.getAttribute('data-mention-event-id') || undefined;
|
||||||
|
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
||||||
|
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
||||||
|
else navigateRoom(mentionId, eventId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viaServers = target.getAttribute('data-mention-via') || undefined;
|
||||||
|
const path = getHomeRoomPath(mentionId, eventId);
|
||||||
|
|
||||||
|
navigate(viaServers ? withSearchParam<_RoomSearchParams>(path, { viaServers }) : path);
|
||||||
|
},
|
||||||
|
[mx, navigate, navigateRoom, navigateSpace, roomId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return handleClick;
|
||||||
|
};
|
|
@ -1,28 +0,0 @@
|
||||||
/* 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];
|
|
||||||
}
|
|
30
src/app/hooks/usePermission.ts
Normal file
30
src/app/hooks/usePermission.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function usePermissionState(name: PermissionName, initialValue: PermissionState = 'prompt') {
|
||||||
|
const [permissionState, setPermissionState] = useState<PermissionState>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let permissionStatus: PermissionStatus;
|
||||||
|
|
||||||
|
function handlePermissionChange(this: PermissionStatus) {
|
||||||
|
setPermissionState(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.permissions
|
||||||
|
.query({ name })
|
||||||
|
.then((permStatus: PermissionStatus) => {
|
||||||
|
permissionStatus = permStatus;
|
||||||
|
handlePermissionChange.apply(permStatus);
|
||||||
|
permStatus.addEventListener("change", handlePermissionChange);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Silence error since FF doesn't support microphone permission
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
permissionStatus?.removeEventListener("change", handlePermissionChange);
|
||||||
|
};
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
return permissionState;
|
||||||
|
}
|
11
src/app/hooks/usePreviousValue.ts
Normal file
11
src/app/hooks/usePreviousValue.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const usePreviousValue = <T>(currentValue: T, initialValue: T) => {
|
||||||
|
const valueRef = useRef(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
valueRef.current = currentValue;
|
||||||
|
}, [currentValue]);
|
||||||
|
|
||||||
|
return valueRef.current;
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { NavigateOptions, useNavigate } from 'react-router-dom';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
||||||
import {
|
import {
|
||||||
|
@ -12,12 +12,14 @@ import { useMatrixClient } from './useMatrixClient';
|
||||||
import { getOrphanParents } from '../utils/room';
|
import { getOrphanParents } from '../utils/room';
|
||||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||||
import { mDirectAtom } from '../state/mDirectList';
|
import { mDirectAtom } from '../state/mDirectList';
|
||||||
|
import { useSelectedSpace } from './router/useSelectedSpace';
|
||||||
|
|
||||||
export const useRoomNavigate = () => {
|
export const useRoomNavigate = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
const mDirects = useAtomValue(mDirectAtom);
|
const mDirects = useAtomValue(mDirectAtom);
|
||||||
|
const spaceSelectedId = useSelectedSpace();
|
||||||
|
|
||||||
const navigateSpace = useCallback(
|
const navigateSpace = useCallback(
|
||||||
(roomId: string) => {
|
(roomId: string) => {
|
||||||
|
@ -28,24 +30,29 @@ export const useRoomNavigate = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateRoom = useCallback(
|
const navigateRoom = useCallback(
|
||||||
(roomId: string, eventId?: string) => {
|
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
||||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||||
|
|
||||||
const orphanParents = getOrphanParents(roomToParents, roomId);
|
const orphanParents = getOrphanParents(roomToParents, roomId);
|
||||||
if (orphanParents.length > 0) {
|
if (orphanParents.length > 0) {
|
||||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, orphanParents[0]);
|
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
|
||||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId));
|
mx,
|
||||||
|
spaceSelectedId && orphanParents.includes(spaceSelectedId)
|
||||||
|
? spaceSelectedId
|
||||||
|
: orphanParents[0]
|
||||||
|
);
|
||||||
|
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mDirects.has(roomId)) {
|
if (mDirects.has(roomId)) {
|
||||||
navigate(getDirectRoomPath(roomIdOrAlias, eventId));
|
navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId));
|
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
|
||||||
},
|
},
|
||||||
[mx, navigate, roomToParents, mDirects]
|
[mx, navigate, spaceSelectedId, roomToParents, mDirects]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,10 +1,26 @@
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo } from 'react';
|
import { selectAtom } from 'jotai/utils';
|
||||||
import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../state/typingMembers';
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
IRoomIdToTypingMembers,
|
||||||
|
TypingReceipt,
|
||||||
|
roomIdToTypingMembersAtom,
|
||||||
|
} from '../state/typingMembers';
|
||||||
|
|
||||||
|
const typingReceiptEqual = (a: TypingReceipt, b: TypingReceipt): boolean =>
|
||||||
|
a.userId === b.userId && a.ts === b.ts;
|
||||||
|
|
||||||
|
const equalTypingMembers = (x: TypingReceipt[], y: TypingReceipt[]): boolean => {
|
||||||
|
if (x.length !== y.length) return false;
|
||||||
|
return x.every((a, i) => typingReceiptEqual(a, y[i]));
|
||||||
|
};
|
||||||
|
|
||||||
export const useRoomTypingMember = (roomId: string) => {
|
export const useRoomTypingMember = (roomId: string) => {
|
||||||
const typing = useAtomValue(
|
const selector = useCallback(
|
||||||
useMemo(() => selectRoomTypingMembersAtom(roomId, roomIdToTypingMembersAtom), [roomId])
|
(roomToTyping: IRoomIdToTypingMembers) => roomToTyping.get(roomId) ?? [],
|
||||||
|
[roomId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const typing = useAtomValue(selectAtom(roomIdToTypingMembersAtom, selector, equalTypingMembers));
|
||||||
return typing;
|
return typing;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
import cons from '../../client/state/cons';
|
|
||||||
import navigation from '../../client/state/navigation';
|
|
||||||
|
|
||||||
export function useSelectedSpace() {
|
|
||||||
const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onSpaceSelected = (roomId) => {
|
|
||||||
setSpaceId(roomId);
|
|
||||||
};
|
|
||||||
navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
|
|
||||||
return () => {
|
|
||||||
navigation.removeListener(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [spaceId];
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
import cons from '../../client/state/cons';
|
|
||||||
import navigation from '../../client/state/navigation';
|
|
||||||
|
|
||||||
export function useSelectedTab() {
|
|
||||||
const [selectedTab, setSelectedTab] = useState(navigation.selectedTab);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onTabSelected = (tabId) => {
|
|
||||||
setSelectedTab(tabId);
|
|
||||||
};
|
|
||||||
navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
|
|
||||||
return () => {
|
|
||||||
navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [selectedTab];
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
import initMatrix from '../../client/initMatrix';
|
|
||||||
import cons from '../../client/state/cons';
|
|
||||||
|
|
||||||
export function useSpaceShortcut() {
|
|
||||||
const { accountData } = initMatrix;
|
|
||||||
const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onSpaceShortcutUpdated = () => {
|
|
||||||
setSpaceShortcut([...accountData.spaceShortcut]);
|
|
||||||
};
|
|
||||||
accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
|
|
||||||
return () => {
|
|
||||||
accountData.removeListener(
|
|
||||||
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
|
|
||||||
onSpaceShortcutUpdated,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [spaceShortcut];
|
|
||||||
}
|
|
14
src/app/hooks/useSpoilerClickHandler.ts
Normal file
14
src/app/hooks/useSpoilerClickHandler.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { ReactEventHandler, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const useSpoilerClickHandler = (): ReactEventHandler<HTMLElement> => {
|
||||||
|
const handleClick: ReactEventHandler<HTMLElement> = useCallback((evt) => {
|
||||||
|
const target = evt.currentTarget;
|
||||||
|
if (target.getAttribute('aria-pressed') === 'true') {
|
||||||
|
evt.stopPropagation();
|
||||||
|
target.setAttribute('aria-pressed', 'false');
|
||||||
|
target.style.cursor = 'initial';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return handleClick;
|
||||||
|
};
|
|
@ -2,13 +2,13 @@ import { ClientEvent, ClientEventHandlerMap, MatrixClient } from 'matrix-js-sdk'
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const useSyncState = (
|
export const useSyncState = (
|
||||||
mx: MatrixClient,
|
mx: MatrixClient | undefined,
|
||||||
onChange: ClientEventHandlerMap[ClientEvent.Sync]
|
onChange: ClientEventHandlerMap[ClientEvent.Sync]
|
||||||
): void => {
|
): void => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mx.on(ClientEvent.Sync, onChange);
|
mx?.on(ClientEvent.Sync, onChange);
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener(ClientEvent.Sync, onChange);
|
mx?.removeListener(ClientEvent.Sync, onChange);
|
||||||
};
|
};
|
||||||
}, [mx, onChange]);
|
}, [mx, onChange]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,16 +2,21 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Dialog.scss';
|
import './Dialog.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import RawModal from '../../atoms/modal/RawModal';
|
import RawModal from '../../atoms/modal/RawModal';
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
className, isOpen, title, onAfterOpen, onAfterClose,
|
className,
|
||||||
contentOptions, onRequestClose, closeFromOutside, children,
|
isOpen,
|
||||||
|
title,
|
||||||
|
onAfterOpen,
|
||||||
|
onAfterClose,
|
||||||
|
contentOptions,
|
||||||
|
onRequestClose,
|
||||||
|
closeFromOutside,
|
||||||
|
children,
|
||||||
invisibleScroll,
|
invisibleScroll,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -28,19 +33,19 @@ function Dialog({
|
||||||
<div className="dialog__content">
|
<div className="dialog__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
{
|
{typeof title === 'string' ? (
|
||||||
typeof title === 'string'
|
<Text variant="h2" weight="medium" primary>
|
||||||
? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
|
{title}
|
||||||
: title
|
</Text>
|
||||||
}
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="dialog__content__wrapper">
|
<div className="dialog__content__wrapper">
|
||||||
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
||||||
<div className="dialog__content-container">
|
<div className="dialog__content-container">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
/* eslint-disable react/prop-types */
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './FollowingMembers.scss';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import cons from '../../../client/state/cons';
|
|
||||||
import { openReadReceipts } from '../../../client/action/navigation';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
|
||||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
|
||||||
|
|
||||||
import { getUsersActionJsx } from '../../organisms/room/common';
|
|
||||||
|
|
||||||
function FollowingMembers({ roomTimeline }) {
|
|
||||||
const [followingMembers, setFollowingMembers] = useState([]);
|
|
||||||
const { roomId } = roomTimeline;
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const myUserId = mx.getUserId();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateFollowingMembers = () => {
|
|
||||||
setFollowingMembers(roomTimeline.getLiveReaders());
|
|
||||||
};
|
|
||||||
const updateOnEvent = (event, room) => {
|
|
||||||
if (room.roomId !== roomId) return;
|
|
||||||
setFollowingMembers(roomTimeline.getLiveReaders());
|
|
||||||
};
|
|
||||||
updateFollowingMembers();
|
|
||||||
roomTimeline.on(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
|
||||||
mx.on('Room.timeline', updateOnEvent);
|
|
||||||
return () => {
|
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
|
||||||
mx.removeListener('Room.timeline', updateOnEvent);
|
|
||||||
};
|
|
||||||
}, [roomTimeline, roomId]);
|
|
||||||
|
|
||||||
const filteredM = followingMembers.filter((userId) => userId !== myUserId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
filteredM.length !== 0 && (
|
|
||||||
<button
|
|
||||||
className="following-members"
|
|
||||||
onClick={() => openReadReceipts(roomId, followingMembers)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<RawIcon size="extra-small" src={TickMarkIC} />
|
|
||||||
<Text variant="b2">
|
|
||||||
{getUsersActionJsx(roomId, filteredM, 'following the conversation.')}
|
|
||||||
</Text>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FollowingMembers.propTypes = {
|
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FollowingMembers;
|
|
|
@ -1,31 +0,0 @@
|
||||||
@use '../../partials/text';
|
|
||||||
|
|
||||||
.following-members {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 var(--sp-normal);
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
& .ic-raw {
|
|
||||||
min-width: var(--ic-extra-small);
|
|
||||||
opacity: 0.4;
|
|
||||||
margin: 0 var(--sp-extra-tight);
|
|
||||||
}
|
|
||||||
& .text {
|
|
||||||
@extend .cp-txt__ellipsis;
|
|
||||||
color: var(--tc-surface-low);
|
|
||||||
b {
|
|
||||||
color: var(--tc-surface-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: var(--bg-surface-active);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
|
@ -14,6 +13,7 @@ import NotificationSelector from './NotificationSelector';
|
||||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
|
|
||||||
import { useAccountData } from '../../hooks/useAccountData';
|
import { useAccountData } from '../../hooks/useAccountData';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
export const notifType = {
|
export const notifType = {
|
||||||
ON: 'on',
|
ON: 'on',
|
||||||
|
@ -52,7 +52,7 @@ export function getTypeActions(type, highlightValue = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGlobalNotif() {
|
function useGlobalNotif() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const pushRules = useAccountData('m.push_rules')?.getContent();
|
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||||
const underride = pushRules?.global?.underride ?? [];
|
const underride = pushRules?.global?.underride ?? [];
|
||||||
const rulesToType = {
|
const rulesToType = {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './IgnoreUserList.scss';
|
import './IgnoreUserList.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
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';
|
||||||
|
@ -14,10 +13,12 @@ import SettingTile from '../setting-tile/SettingTile';
|
||||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||||
|
|
||||||
import { useAccountData } from '../../hooks/useAccountData';
|
import { useAccountData } from '../../hooks/useAccountData';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
function IgnoreUserList() {
|
function IgnoreUserList() {
|
||||||
useAccountData('m.ignored_user_list');
|
useAccountData('m.ignored_user_list');
|
||||||
const ignoredUsers = initMatrix.matrixClient.getIgnoredUsers();
|
const mx = useMatrixClient();
|
||||||
|
const ignoredUsers = mx.getIgnoredUsers();
|
||||||
|
|
||||||
const handleSubmit = (evt) => {
|
const handleSubmit = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
@ -26,7 +27,7 @@ function IgnoreUserList() {
|
||||||
const userIds = value.split(' ').filter((v) => v.match(/^@\S+:\S+$/));
|
const userIds = value.split(' ').filter((v) => v.match(/^@\S+:\S+$/));
|
||||||
if (userIds.length === 0) return;
|
if (userIds.length === 0) return;
|
||||||
ignoreInput.value = '';
|
ignoreInput.value = '';
|
||||||
roomActions.ignore(userIds);
|
roomActions.ignore(mx, userIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -49,7 +50,7 @@ function IgnoreUserList() {
|
||||||
key={uId}
|
key={uId}
|
||||||
text={uId}
|
text={uId}
|
||||||
iconColor={CrossIC}
|
iconColor={CrossIC}
|
||||||
onClick={() => roomActions.unignore([uId])}
|
onClick={() => roomActions.unignore(mx, [uId])}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './KeywordNotification.scss';
|
import './KeywordNotification.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
|
@ -21,6 +20,7 @@ import { useAccountData } from '../../hooks/useAccountData';
|
||||||
import {
|
import {
|
||||||
notifType, typeToLabel, getActionType, getTypeActions,
|
notifType, typeToLabel, getActionType, getTypeActions,
|
||||||
} from './GlobalNotification';
|
} from './GlobalNotification';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
const DISPLAY_NAME = '.m.rule.contains_display_name';
|
const DISPLAY_NAME = '.m.rule.contains_display_name';
|
||||||
const ROOM_PING = '.m.rule.roomnotif';
|
const ROOM_PING = '.m.rule.roomnotif';
|
||||||
|
@ -28,7 +28,7 @@ const USERNAME = '.m.rule.contains_user_name';
|
||||||
const KEYWORD = 'keyword';
|
const KEYWORD = 'keyword';
|
||||||
|
|
||||||
function useKeywordNotif() {
|
function useKeywordNotif() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const pushRules = useAccountData('m.push_rules')?.getContent();
|
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||||
const override = pushRules?.global?.override ?? [];
|
const override = pushRules?.global?.override ?? [];
|
||||||
const content = pushRules?.global?.content ?? [];
|
const content = pushRules?.global?.content ?? [];
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './ImageLightbox.scss';
|
|
||||||
import FileSaver from 'file-saver';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import RawModal from '../../atoms/modal/RawModal';
|
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
|
||||||
|
|
||||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
|
||||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
|
||||||
|
|
||||||
function ImageLightbox({
|
|
||||||
url, alt, isOpen, onRequestClose,
|
|
||||||
}) {
|
|
||||||
const handleDownload = () => {
|
|
||||||
FileSaver.saveAs(url, alt);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RawModal
|
|
||||||
className="image-lightbox__modal"
|
|
||||||
overlayClassName="image-lightbox__overlay"
|
|
||||||
isOpen={isOpen}
|
|
||||||
onRequestClose={onRequestClose}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<div className="image-lightbox__header">
|
|
||||||
<Text variant="b2" weight="medium">{alt}</Text>
|
|
||||||
<IconButton onClick={() => window.open(url)} size="small" src={ExternalSVG} />
|
|
||||||
<IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
|
|
||||||
</div>
|
|
||||||
<div className="image-lightbox__content">
|
|
||||||
<img src={url} alt={alt} />
|
|
||||||
</div>
|
|
||||||
</RawModal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageLightbox.propTypes = {
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
alt: PropTypes.string.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onRequestClose: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImageLightbox;
|
|
|
@ -1,50 +0,0 @@
|
||||||
@use '../../partials/flex';
|
|
||||||
@use '../../partials/text';
|
|
||||||
|
|
||||||
.image-lightbox__modal {
|
|
||||||
box-shadow: none;
|
|
||||||
width: unset;
|
|
||||||
gap: var(--sp-normal);
|
|
||||||
|
|
||||||
border-radius: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
& .text {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
& .ic-raw {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-lightbox__overlay {
|
|
||||||
background-color: var(--bg-overlay-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.image-lightbox__header > *,
|
|
||||||
.image-lightbox__content > * {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
.image-lightbox__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
& > .text {
|
|
||||||
@extend .cp-fx__item-one;
|
|
||||||
@extend .cp-txt__ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.image-lightbox__content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
max-height: 80vh;
|
|
||||||
|
|
||||||
& img {
|
|
||||||
background-color: var(--bg-surface-low);
|
|
||||||
object-fit: contain;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
border-radius: var(--bo-radius);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import React, {
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './ImagePack.scss';
|
import './ImagePack.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import { openReusableDialog } from '../../../client/action/navigation';
|
import { openReusableDialog } from '../../../client/action/navigation';
|
||||||
import { suffixRename } from '../../../util/common';
|
import { suffixRename } from '../../../util/common';
|
||||||
|
|
||||||
|
@ -19,6 +18,7 @@ import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||||
import ImagePackProfile from './ImagePackProfile';
|
import ImagePackProfile from './ImagePackProfile';
|
||||||
import ImagePackItem from './ImagePackItem';
|
import ImagePackItem from './ImagePackItem';
|
||||||
import ImagePackUpload from './ImagePackUpload';
|
import ImagePackUpload from './ImagePackUpload';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
|
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
|
||||||
let isCompleted = false;
|
let isCompleted = false;
|
||||||
|
@ -63,8 +63,7 @@ function getUsage(usage) {
|
||||||
return 'both';
|
return 'both';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGlobalPack(roomId, stateKey) {
|
function isGlobalPack(mx, roomId, stateKey) {
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||||
if (typeof globalContent !== 'object') return false;
|
if (typeof globalContent !== 'object') return false;
|
||||||
|
|
||||||
|
@ -75,13 +74,13 @@ function isGlobalPack(roomId, stateKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useRoomImagePack(roomId, stateKey) {
|
function useRoomImagePack(roomId, stateKey) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
const pack = useMemo(() => {
|
||||||
const pack = useMemo(() => (
|
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||||
ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent())
|
return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent())
|
||||||
), [room, stateKey]);
|
}, [room, stateKey]);
|
||||||
|
|
||||||
const sendPackContent = (content) => {
|
const sendPackContent = (content) => {
|
||||||
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
||||||
|
@ -94,14 +93,14 @@ function useRoomImagePack(roomId, stateKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useUserImagePack() {
|
function useUserImagePack() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
const pack = useMemo(() => {
|
||||||
const pack = useMemo(() => (
|
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
||||||
ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? {
|
return ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? {
|
||||||
pack: { display_name: 'Personal' },
|
pack: { display_name: 'Personal' },
|
||||||
images: {},
|
images: {},
|
||||||
})
|
})
|
||||||
), []);
|
}, [mx]);
|
||||||
|
|
||||||
const sendPackContent = (content) => {
|
const sendPackContent = (content) => {
|
||||||
mx.setAccountData('im.ponies.user_emotes', content);
|
mx.setAccountData('im.ponies.user_emotes', content);
|
||||||
|
@ -223,10 +222,10 @@ function removeGlobalImagePack(mx, roomId, stateKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const [viewMore, setViewMore] = useState(false);
|
const [viewMore, setViewMore] = useState(false);
|
||||||
const [isGlobal, setIsGlobal] = useState(isGlobalPack(roomId, stateKey));
|
const [isGlobal, setIsGlobal] = useState(isGlobalPack(mx, roomId, stateKey));
|
||||||
|
|
||||||
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
|
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
|
||||||
|
|
||||||
|
@ -331,7 +330,7 @@ ImagePack.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function ImagePackUser() {
|
function ImagePackUser() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const [viewMore, setViewMore] = useState(false);
|
const [viewMore, setViewMore] = useState(false);
|
||||||
|
|
||||||
const { pack, sendPackContent } = useUserImagePack();
|
const { pack, sendPackContent } = useUserImagePack();
|
||||||
|
@ -397,7 +396,7 @@ function ImagePackUser() {
|
||||||
|
|
||||||
function useGlobalImagePack() {
|
function useGlobalImagePack() {
|
||||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
const roomIdToStateKeys = new Map();
|
const roomIdToStateKeys = new Map();
|
||||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
||||||
|
@ -419,13 +418,13 @@ function useGlobalImagePack() {
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener('accountData', handleEvent);
|
mx.removeListener('accountData', handleEvent);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [mx]);
|
||||||
|
|
||||||
return roomIdToStateKeys;
|
return roomIdToStateKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImagePackGlobal() {
|
function ImagePackGlobal() {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const roomIdToStateKeys = useGlobalImagePack();
|
const roomIdToStateKeys = useGlobalImagePack();
|
||||||
|
|
||||||
const handleChange = (roomId, stateKey) => {
|
const handleChange = (roomId, stateKey) => {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './ImagePackUpload.scss';
|
import './ImagePackUpload.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import { scaleDownImage } from '../../../util/common';
|
import { scaleDownImage } from '../../../util/common';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
@ -10,9 +9,10 @@ import Button from '../../atoms/button/Button';
|
||||||
import Input from '../../atoms/input/Input';
|
import Input from '../../atoms/input/Input';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
function ImagePackUpload({ onUpload }) {
|
function ImagePackUpload({ onUpload }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = useMatrixClient();
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const shortcodeRef = useRef(null);
|
const shortcodeRef = useRef(null);
|
||||||
const [imgFile, setImgFile] = useState(null);
|
const [imgFile, setImgFile] = useState(null);
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './ImageUpload.scss';
|
import './ImageUpload.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
|
@ -10,6 +9,7 @@ import Spinner from '../../atoms/spinner/Spinner';
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
|
|
||||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
function ImageUpload({
|
function ImageUpload({
|
||||||
text, bgColor, imageSrc, onUpload, onRequestRemove,
|
text, bgColor, imageSrc, onUpload, onRequestRemove,
|
||||||
|
@ -17,12 +17,13 @@ function ImageUpload({
|
||||||
}) {
|
}) {
|
||||||
const [uploadPromise, setUploadPromise] = useState(null);
|
const [uploadPromise, setUploadPromise] = useState(null);
|
||||||
const uploadImageRef = useRef(null);
|
const uploadImageRef = useRef(null);
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
async function uploadImage(e) {
|
async function uploadImage(e) {
|
||||||
const file = e.target.files.item(0);
|
const file = e.target.files.item(0);
|
||||||
if (file === null) return;
|
if (file === null) return;
|
||||||
try {
|
try {
|
||||||
const uPromise = initMatrix.matrixClient.uploadContent(file);
|
const uPromise = mx.uploadContent(file);
|
||||||
setUploadPromise(uPromise);
|
setUploadPromise(uPromise);
|
||||||
|
|
||||||
const res = await uPromise;
|
const res = await uPromise;
|
||||||
|
@ -35,7 +36,7 @@ function ImageUpload({
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelUpload() {
|
function cancelUpload() {
|
||||||
initMatrix.matrixClient.cancelUpload(uploadPromise);
|
mx.cancelUpload(uploadPromise);
|
||||||
setUploadPromise(null);
|
setUploadPromise(null);
|
||||||
uploadImageRef.current.value = null;
|
uploadImageRef.current.value = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import './ExportE2ERoomKeys.scss';
|
||||||
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import { encryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
|
import { encryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
|
||||||
|
|
||||||
|
@ -13,8 +12,10 @@ import Input from '../../atoms/input/Input';
|
||||||
import Spinner from '../../atoms/spinner/Spinner';
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
function ExportE2ERoomKeys() {
|
function ExportE2ERoomKeys() {
|
||||||
|
const mx = useMatrixClient();
|
||||||
const isMountStore = useStore();
|
const isMountStore = useStore();
|
||||||
const [status, setStatus] = useState({
|
const [status, setStatus] = useState({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
|
@ -40,7 +41,7 @@ function ExportE2ERoomKeys() {
|
||||||
type: cons.status.IN_FLIGHT,
|
type: cons.status.IN_FLIGHT,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const keys = await initMatrix.matrixClient.exportRoomKeys();
|
const keys = await mx.exportRoomKeys();
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: true,
|
isOngoing: true,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import './ImportE2ERoomKeys.scss';
|
import './ImportE2ERoomKeys.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import { decryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
|
import { decryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
|
||||||
|
|
||||||
|
@ -14,8 +13,10 @@ import Spinner from '../../atoms/spinner/Spinner';
|
||||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
function ImportE2ERoomKeys() {
|
function ImportE2ERoomKeys() {
|
||||||
|
const mx = useMatrixClient();
|
||||||
const isMountStore = useStore();
|
const isMountStore = useStore();
|
||||||
const [keyFile, setKeyFile] = useState(null);
|
const [keyFile, setKeyFile] = useState(null);
|
||||||
const [status, setStatus] = useState({
|
const [status, setStatus] = useState({
|
||||||
|
@ -45,7 +46,7 @@ function ImportE2ERoomKeys() {
|
||||||
type: cons.status.IN_FLIGHT,
|
type: cons.status.IN_FLIGHT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await initMatrix.matrixClient.importRoomKeys(JSON.parse(keys));
|
await mx.importRoomKeys(JSON.parse(keys));
|
||||||
if (isMountStore.getItem()) {
|
if (isMountStore.getItem()) {
|
||||||
setStatus({
|
setStatus({
|
||||||
isOngoing: false,
|
isOngoing: false,
|
||||||
|
|
|
@ -1,366 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './Media.scss';
|
|
||||||
|
|
||||||
import encrypt from 'browser-encrypt-attachment';
|
|
||||||
|
|
||||||
import { BlurhashCanvas } from 'react-blurhash';
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
|
||||||
import Spinner from '../../atoms/spinner/Spinner';
|
|
||||||
import ImageLightbox from '../image-lightbox/ImageLightbox';
|
|
||||||
|
|
||||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
|
||||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
|
||||||
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
|
|
||||||
|
|
||||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
|
||||||
|
|
||||||
async function getDecryptedBlob(response, type, decryptData) {
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const dataArray = await encrypt.decryptAttachment(arrayBuffer, decryptData);
|
|
||||||
const blob = new Blob([dataArray], { type: getBlobSafeMimeType(type) });
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUrl(link, type, decryptData) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(link, { method: 'GET' });
|
|
||||||
if (decryptData !== null) {
|
|
||||||
return URL.createObjectURL(await getDecryptedBlob(response, type, decryptData));
|
|
||||||
}
|
|
||||||
const blob = await response.blob();
|
|
||||||
return URL.createObjectURL(blob);
|
|
||||||
} catch (e) {
|
|
||||||
return link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNativeHeight(width, height, maxWidth = 296) {
|
|
||||||
const scale = maxWidth / width;
|
|
||||||
return scale * height;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FileHeader({
|
|
||||||
name, link, external,
|
|
||||||
file, type,
|
|
||||||
}) {
|
|
||||||
const [url, setUrl] = useState(null);
|
|
||||||
|
|
||||||
async function getFile() {
|
|
||||||
const myUrl = await getUrl(link, type, file);
|
|
||||||
setUrl(myUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDownload(e) {
|
|
||||||
if (file !== null && url === null) {
|
|
||||||
e.preventDefault();
|
|
||||||
await getFile();
|
|
||||||
e.target.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="file-header">
|
|
||||||
<Text className="file-name" variant="b3">{name}</Text>
|
|
||||||
{ link !== null && (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
external && (
|
|
||||||
<IconButton
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Open in new tab"
|
|
||||||
src={ExternalSVG}
|
|
||||||
onClick={() => window.open(url || link)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<a href={url || link} download={name} target="_blank" rel="noreferrer">
|
|
||||||
<IconButton
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Download"
|
|
||||||
src={DownloadSVG}
|
|
||||||
onClick={handleDownload}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
FileHeader.defaultProps = {
|
|
||||||
external: false,
|
|
||||||
file: null,
|
|
||||||
link: null,
|
|
||||||
};
|
|
||||||
FileHeader.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
link: PropTypes.string,
|
|
||||||
external: PropTypes.bool,
|
|
||||||
file: PropTypes.shape({}),
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function File({
|
|
||||||
name, link, file, type,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="file-container">
|
|
||||||
<FileHeader name={name} link={link} file={file} type={type} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File.defaultProps = {
|
|
||||||
file: null,
|
|
||||||
type: '',
|
|
||||||
};
|
|
||||||
File.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
type: PropTypes.string,
|
|
||||||
file: PropTypes.shape({}),
|
|
||||||
};
|
|
||||||
|
|
||||||
function Image({
|
|
||||||
name, width, height, link, file, type, blurhash,
|
|
||||||
}) {
|
|
||||||
const [url, setUrl] = useState(null);
|
|
||||||
const [blur, setBlur] = useState(true);
|
|
||||||
const [lightbox, setLightbox] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let unmounted = false;
|
|
||||||
async function fetchUrl() {
|
|
||||||
const myUrl = await getUrl(link, type, file);
|
|
||||||
if (unmounted) return;
|
|
||||||
setUrl(myUrl);
|
|
||||||
}
|
|
||||||
fetchUrl();
|
|
||||||
return () => {
|
|
||||||
unmounted = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleLightbox = () => {
|
|
||||||
if (!url) return;
|
|
||||||
setLightbox(!lightbox);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="file-container">
|
|
||||||
<div
|
|
||||||
style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }}
|
|
||||||
className="image-container"
|
|
||||||
role="button"
|
|
||||||
tabIndex="0"
|
|
||||||
onClick={toggleLightbox}
|
|
||||||
onKeyDown={toggleLightbox}
|
|
||||||
>
|
|
||||||
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
|
||||||
{ url !== null && (
|
|
||||||
<img
|
|
||||||
style={{ display: blur ? 'none' : 'unset' }}
|
|
||||||
onLoad={() => setBlur(false)}
|
|
||||||
src={url || link}
|
|
||||||
alt={name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{url && (
|
|
||||||
<ImageLightbox
|
|
||||||
url={url}
|
|
||||||
alt={name}
|
|
||||||
isOpen={lightbox}
|
|
||||||
onRequestClose={toggleLightbox}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Image.defaultProps = {
|
|
||||||
file: null,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
type: '',
|
|
||||||
blurhash: '',
|
|
||||||
};
|
|
||||||
Image.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
file: PropTypes.shape({}),
|
|
||||||
type: PropTypes.string,
|
|
||||||
blurhash: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
function Sticker({
|
|
||||||
name, height, width, link, file, type,
|
|
||||||
}) {
|
|
||||||
const [url, setUrl] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let unmounted = false;
|
|
||||||
async function fetchUrl() {
|
|
||||||
const myUrl = await getUrl(link, type, file);
|
|
||||||
if (unmounted) return;
|
|
||||||
setUrl(myUrl);
|
|
||||||
}
|
|
||||||
fetchUrl();
|
|
||||||
return () => {
|
|
||||||
unmounted = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sticker-container" style={{ height: width !== null ? getNativeHeight(width, height, 128) : 'unset' }}>
|
|
||||||
{ url !== null && <img src={url || link} title={name} alt={name} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Sticker.defaultProps = {
|
|
||||||
file: null,
|
|
||||||
type: '',
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
};
|
|
||||||
Sticker.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
file: PropTypes.shape({}),
|
|
||||||
type: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
function Audio({
|
|
||||||
name, link, type, file,
|
|
||||||
}) {
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [url, setUrl] = useState(null);
|
|
||||||
|
|
||||||
async function loadAudio() {
|
|
||||||
const myUrl = await getUrl(link, type, file);
|
|
||||||
setUrl(myUrl);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
function handlePlayAudio() {
|
|
||||||
setIsLoading(true);
|
|
||||||
loadAudio();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="file-container">
|
|
||||||
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
|
|
||||||
<div className="audio-container">
|
|
||||||
{ url === null && isLoading && <Spinner size="small" /> }
|
|
||||||
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
|
|
||||||
{ url !== null && (
|
|
||||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
|
||||||
<audio autoPlay controls>
|
|
||||||
<source src={url} type={getBlobSafeMimeType(type)} />
|
|
||||||
</audio>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Audio.defaultProps = {
|
|
||||||
file: null,
|
|
||||||
type: '',
|
|
||||||
};
|
|
||||||
Audio.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
type: PropTypes.string,
|
|
||||||
file: PropTypes.shape({}),
|
|
||||||
};
|
|
||||||
|
|
||||||
function Video({
|
|
||||||
name, link, thumbnail, thumbnailFile, thumbnailType,
|
|
||||||
width, height, file, type, blurhash,
|
|
||||||
}) {
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [url, setUrl] = useState(null);
|
|
||||||
const [thumbUrl, setThumbUrl] = useState(null);
|
|
||||||
const [blur, setBlur] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let unmounted = false;
|
|
||||||
async function fetchUrl() {
|
|
||||||
const myThumbUrl = await getUrl(thumbnail, thumbnailType, thumbnailFile);
|
|
||||||
if (unmounted) return;
|
|
||||||
setThumbUrl(myThumbUrl);
|
|
||||||
}
|
|
||||||
if (thumbnail !== null) fetchUrl();
|
|
||||||
return () => {
|
|
||||||
unmounted = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadVideo = async () => {
|
|
||||||
const myUrl = await getUrl(link, type, file);
|
|
||||||
setUrl(myUrl);
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlayVideo = () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
loadVideo();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="file-container">
|
|
||||||
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: width !== null ? getNativeHeight(width, height) : 'unset',
|
|
||||||
}}
|
|
||||||
className="video-container"
|
|
||||||
>
|
|
||||||
{ url === null ? (
|
|
||||||
<>
|
|
||||||
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
|
||||||
{ thumbUrl !== null && (
|
|
||||||
<img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
|
|
||||||
)}
|
|
||||||
{isLoading && <Spinner size="small" />}
|
|
||||||
{!isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
|
||||||
<video autoPlay controls poster={thumbUrl}>
|
|
||||||
<source src={url} type={getBlobSafeMimeType(type)} />
|
|
||||||
</video>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Video.defaultProps = {
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
file: null,
|
|
||||||
thumbnail: null,
|
|
||||||
thumbnailType: null,
|
|
||||||
thumbnailFile: null,
|
|
||||||
type: '',
|
|
||||||
blurhash: null,
|
|
||||||
};
|
|
||||||
Video.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
link: PropTypes.string.isRequired,
|
|
||||||
thumbnail: PropTypes.string,
|
|
||||||
thumbnailFile: PropTypes.shape({}),
|
|
||||||
thumbnailType: PropTypes.string,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
file: PropTypes.shape({}),
|
|
||||||
type: PropTypes.string,
|
|
||||||
blurhash: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
File, Image, Sticker, Audio, Video,
|
|
||||||
};
|
|
|
@ -1,90 +0,0 @@
|
||||||
@use '../../partials/text';
|
|
||||||
|
|
||||||
.file-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--sp-ultra-tight) var(--sp-tight);
|
|
||||||
min-height: 42px;
|
|
||||||
|
|
||||||
& .file-name {
|
|
||||||
@extend .cp-txt__ellipsis;
|
|
||||||
flex: 1;
|
|
||||||
color: var(--tc-surface-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
& a {
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-container {
|
|
||||||
--media-max-width: 296px;
|
|
||||||
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: var(--media-max-width);
|
|
||||||
white-space: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticker-container {
|
|
||||||
display: inline-flex;
|
|
||||||
max-width: 128px;
|
|
||||||
width: 100%;
|
|
||||||
& img {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container,
|
|
||||||
.video-container,
|
|
||||||
.audio-container {
|
|
||||||
font-size: 0;
|
|
||||||
line-height: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container,
|
|
||||||
.video-container {
|
|
||||||
& img,
|
|
||||||
& canvas {
|
|
||||||
max-width: unset !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.image-container {
|
|
||||||
max-height: 460px;
|
|
||||||
img {
|
|
||||||
cursor: pointer;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container {
|
|
||||||
position: relative;
|
|
||||||
& .ic-btn-surface {
|
|
||||||
background-color: var(--bg-surface-low);
|
|
||||||
}
|
|
||||||
& .ic-btn-surface,
|
|
||||||
& .donut-spinner {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.audio-container {
|
|
||||||
audio {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,853 +0,0 @@
|
||||||
/* eslint-disable react/prop-types */
|
|
||||||
import React, {
|
|
||||||
useState, useEffect, useCallback, useRef,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './Message.scss';
|
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
|
||||||
import {
|
|
||||||
getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply,
|
|
||||||
} from '../../../util/matrixUtil';
|
|
||||||
import colorMXID from '../../../util/colorMXID';
|
|
||||||
import { getEventCords } from '../../../util/common';
|
|
||||||
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
|
||||||
import {
|
|
||||||
openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
|
|
||||||
} from '../../../client/action/navigation';
|
|
||||||
import { sanitizeCustomHtml } from '../../../util/sanitize';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
|
||||||
import Button from '../../atoms/button/Button';
|
|
||||||
import Tooltip from '../../atoms/tooltip/Tooltip';
|
|
||||||
import Input from '../../atoms/input/Input';
|
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
|
||||||
import Time from '../../atoms/time/Time';
|
|
||||||
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
|
||||||
import * as Media from '../media/Media';
|
|
||||||
|
|
||||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
|
||||||
import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
|
|
||||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
|
||||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.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 { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
|
||||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
|
||||||
import { html, plain } from '../../../util/markdown';
|
|
||||||
|
|
||||||
function PlaceholderMessage() {
|
|
||||||
return (
|
|
||||||
<div className="ph-msg">
|
|
||||||
<div className="ph-msg__avatar-container">
|
|
||||||
<div className="ph-msg__avatar" />
|
|
||||||
</div>
|
|
||||||
<div className="ph-msg__main-container">
|
|
||||||
<div className="ph-msg__header" />
|
|
||||||
<div className="ph-msg__body">
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageAvatar = React.memo(({
|
|
||||||
roomId, avatarSrc, userId, username,
|
|
||||||
}) => (
|
|
||||||
<div className="message__avatar-container">
|
|
||||||
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
|
||||||
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
const MessageHeader = React.memo(({
|
|
||||||
userId, username, timestamp, fullTime,
|
|
||||||
}) => (
|
|
||||||
<div className="message__header">
|
|
||||||
<Text
|
|
||||||
style={{ color: colorMXID(userId) }}
|
|
||||||
className="message__profile"
|
|
||||||
variant="b1"
|
|
||||||
weight="medium"
|
|
||||||
span
|
|
||||||
>
|
|
||||||
<span>{twemojify(username)}</span>
|
|
||||||
<span>{twemojify(userId)}</span>
|
|
||||||
</Text>
|
|
||||||
<div className="message__time">
|
|
||||||
<Text variant="b3">
|
|
||||||
<Time timestamp={timestamp} fullTime={fullTime} />
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
MessageHeader.defaultProps = {
|
|
||||||
fullTime: false,
|
|
||||||
};
|
|
||||||
MessageHeader.propTypes = {
|
|
||||||
userId: PropTypes.string.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
timestamp: PropTypes.number.isRequired,
|
|
||||||
fullTime: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
function MessageReply({ name, color, body }) {
|
|
||||||
return (
|
|
||||||
<div className="message__reply">
|
|
||||||
<Text variant="b2">
|
|
||||||
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
|
||||||
<span style={{ color }}>{twemojify(name)}</span>
|
|
||||||
{' '}
|
|
||||||
{twemojify(body)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageReply.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
color: PropTypes.string.isRequired,
|
|
||||||
body: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
|
||||||
const [reply, setReply] = useState(null);
|
|
||||||
const isMountedRef = useRef(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const timelineSet = roomTimeline.getUnfilteredTimelineSet();
|
|
||||||
const loadReply = async () => {
|
|
||||||
try {
|
|
||||||
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
|
|
||||||
await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
|
|
||||||
|
|
||||||
let mEvent = eTimeline.getTimelineSet().findEventById(eventId);
|
|
||||||
const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
|
|
||||||
if (editedList) {
|
|
||||||
mEvent = editedList[editedList.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawBody = mEvent.getContent().body;
|
|
||||||
const username = getUsernameOfRoomMember(mEvent.sender);
|
|
||||||
|
|
||||||
if (isMountedRef.current === false) return;
|
|
||||||
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
|
|
||||||
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
|
|
||||||
if (editedList && parsedBody.startsWith(' * ')) {
|
|
||||||
parsedBody = parsedBody.slice(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
setReply({
|
|
||||||
to: username,
|
|
||||||
color: colorMXID(mEvent.getSender()),
|
|
||||||
body: parsedBody,
|
|
||||||
event: mEvent,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
setReply({
|
|
||||||
to: '** Unknown user **',
|
|
||||||
color: 'var(--tc-danger-normal)',
|
|
||||||
body: '*** Unable to load reply ***',
|
|
||||||
event: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadReply();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMountedRef.current = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const focusReply = (ev) => {
|
|
||||||
if (!ev.key || ev.key === ' ' || ev.key === 'Enter') {
|
|
||||||
if (ev.key) ev.preventDefault();
|
|
||||||
if (reply?.event === null) return;
|
|
||||||
if (reply?.event.isRedacted()) return;
|
|
||||||
roomTimeline.loadEventTimeline(eventId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="message__reply-wrapper"
|
|
||||||
onClick={focusReply}
|
|
||||||
onKeyDown={focusReply}
|
|
||||||
role="button"
|
|
||||||
tabIndex="0"
|
|
||||||
>
|
|
||||||
{reply !== null && <MessageReply name={reply.to} color={reply.color} body={reply.body} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
MessageReplyWrapper.propTypes = {
|
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
|
||||||
eventId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MessageBody = React.memo(({
|
|
||||||
senderName,
|
|
||||||
body,
|
|
||||||
isCustomHTML,
|
|
||||||
isEdited,
|
|
||||||
msgType,
|
|
||||||
}) => {
|
|
||||||
// if body is not string it is a React element.
|
|
||||||
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
|
|
||||||
|
|
||||||
let content = null;
|
|
||||||
if (isCustomHTML) {
|
|
||||||
try {
|
|
||||||
content = twemojify(
|
|
||||||
sanitizeCustomHtml(initMatrix.matrixClient, body),
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
console.error('Malformed custom html: ', body);
|
|
||||||
content = twemojify(body, undefined);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content = twemojify(body, undefined, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if this message should render with large emojis
|
|
||||||
// Criteria:
|
|
||||||
// - Contains only emoji
|
|
||||||
// - Contains no more than 10 emoji
|
|
||||||
let emojiOnly = false;
|
|
||||||
if (content.type === 'img') {
|
|
||||||
// If this messages contains only a single (inline) image
|
|
||||||
emojiOnly = true;
|
|
||||||
} else if (content.constructor.name === 'Array') {
|
|
||||||
// Otherwise, it might be an array of images / texb
|
|
||||||
|
|
||||||
// Count the number of emojis
|
|
||||||
const nEmojis = content.filter((e) => e.type === 'img').length;
|
|
||||||
|
|
||||||
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
|
||||||
if (nEmojis <= 10 && content.every((element) => (
|
|
||||||
(typeof element === 'object' && element.type === 'img')
|
|
||||||
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
|
||||||
))) {
|
|
||||||
emojiOnly = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCustomHTML) {
|
|
||||||
// If this is a plaintext message, wrap it in a <p> element (automatically applying
|
|
||||||
// white-space: pre-wrap) in order to preserve newlines
|
|
||||||
content = (<p className="message__body-plain">{content}</p>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="message__body">
|
|
||||||
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
|
||||||
{ msgType === 'm.emote' && (
|
|
||||||
<>
|
|
||||||
{'* '}
|
|
||||||
{twemojify(senderName)}
|
|
||||||
{' '}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{ content }
|
|
||||||
</div>
|
|
||||||
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
MessageBody.defaultProps = {
|
|
||||||
isCustomHTML: false,
|
|
||||||
isEdited: false,
|
|
||||||
msgType: null,
|
|
||||||
};
|
|
||||||
MessageBody.propTypes = {
|
|
||||||
senderName: PropTypes.string.isRequired,
|
|
||||||
body: PropTypes.node.isRequired,
|
|
||||||
isCustomHTML: PropTypes.bool,
|
|
||||||
isEdited: PropTypes.bool,
|
|
||||||
msgType: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
function MessageEdit({ body, onSave, onCancel }) {
|
|
||||||
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) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
|
||||||
e.preventDefault();
|
|
||||||
onSave(editInputRef.current.value, body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value, body); }}>
|
|
||||||
<Input
|
|
||||||
forwardRef={editInputRef}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
value={body}
|
|
||||||
placeholder="Edit message"
|
|
||||||
required
|
|
||||||
resizable
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<div className="message__edit-btns">
|
|
||||||
<Button type="submit" variant="primary">Save</Button>
|
|
||||||
<Button onClick={onCancel}>Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
MessageEdit.propTypes = {
|
|
||||||
body: PropTypes.string.isRequired,
|
|
||||||
onSave: PropTypes.func.isRequired,
|
|
||||||
onCancel: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const rEvents = roomTimeline.reactionTimeline.get(eventId);
|
|
||||||
let rEvent = null;
|
|
||||||
rEvents?.find((rE) => {
|
|
||||||
if (rE.getRelation() === null) return false;
|
|
||||||
if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
|
|
||||||
rEvent = rE;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
return rEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
|
|
||||||
const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
|
|
||||||
if (myAlreadyReactEvent) {
|
|
||||||
const rId = myAlreadyReactEvent.getId();
|
|
||||||
if (rId.startsWith('~')) return;
|
|
||||||
redactEvent(roomId, rId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendReaction(roomId, eventId, emojiKey, shortcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickEmoji(e, roomId, eventId, roomTimeline) {
|
|
||||||
openEmojiBoard(getEventCords(e), (emoji) => {
|
|
||||||
toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
|
|
||||||
e.target.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function genReactionMsg(userIds, reaction, shortcode) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{userIds.map((userId, index) => (
|
|
||||||
<React.Fragment key={userId}>
|
|
||||||
{twemojify(getUsername(userId))}
|
|
||||||
{index < userIds.length - 1 && (
|
|
||||||
<span style={{ opacity: '.6' }}>
|
|
||||||
{index === userIds.length - 2 ? ' and ' : ', '}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
|
||||||
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MessageReaction({
|
|
||||||
reaction, shortcode, count, users, isActive, onClick,
|
|
||||||
}) {
|
|
||||||
let customEmojiUrl = null;
|
|
||||||
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
|
||||||
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
className="msg__reaction-tooltip"
|
|
||||||
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
type="button"
|
|
||||||
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
customEmojiUrl
|
|
||||||
? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
|
|
||||||
: twemojify(reaction, { className: 'react-emoji' })
|
|
||||||
}
|
|
||||||
<Text variant="b3" className="msg__reaction-count">{count}</Text>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
MessageReaction.defaultProps = {
|
|
||||||
shortcode: undefined,
|
|
||||||
};
|
|
||||||
MessageReaction.propTypes = {
|
|
||||||
reaction: PropTypes.node.isRequired,
|
|
||||||
shortcode: PropTypes.string,
|
|
||||||
count: PropTypes.number.isRequired,
|
|
||||||
users: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
isActive: PropTypes.bool.isRequired,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function MessageReactionGroup({ roomTimeline, mEvent }) {
|
|
||||||
const { roomId, room, reactionTimeline } = roomTimeline;
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const reactions = {};
|
|
||||||
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
|
||||||
|
|
||||||
const eventReactions = reactionTimeline.get(mEvent.getId());
|
|
||||||
const addReaction = (key, shortcode, count, senderId, isActive) => {
|
|
||||||
let reaction = reactions[key];
|
|
||||||
if (reaction === undefined) {
|
|
||||||
reaction = {
|
|
||||||
count: 0,
|
|
||||||
users: [],
|
|
||||||
isActive: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (shortcode) reaction.shortcode = shortcode;
|
|
||||||
if (count) {
|
|
||||||
reaction.count = count;
|
|
||||||
} else {
|
|
||||||
reaction.users.push(senderId);
|
|
||||||
reaction.count = reaction.users.length;
|
|
||||||
if (isActive) reaction.isActive = isActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
reactions[key] = reaction;
|
|
||||||
};
|
|
||||||
if (eventReactions) {
|
|
||||||
eventReactions.forEach((rEvent) => {
|
|
||||||
if (rEvent.getRelation() === null) return;
|
|
||||||
const reaction = rEvent.getRelation();
|
|
||||||
const senderId = rEvent.getSender();
|
|
||||||
const { shortcode } = rEvent.getContent();
|
|
||||||
const isActive = senderId === mx.getUserId();
|
|
||||||
|
|
||||||
addReaction(reaction.key, shortcode, undefined, senderId, isActive);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Use aggregated reactions
|
|
||||||
const aggregatedReaction = mEvent.getServerAggregatedRelation('m.annotation')?.chunk;
|
|
||||||
if (!aggregatedReaction) return null;
|
|
||||||
aggregatedReaction.forEach((reaction) => {
|
|
||||||
if (reaction.type !== 'm.reaction') return;
|
|
||||||
addReaction(reaction.key, undefined, reaction.count, undefined, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="message__reactions text text-b3 noselect">
|
|
||||||
{
|
|
||||||
Object.keys(reactions).map((key) => (
|
|
||||||
<MessageReaction
|
|
||||||
key={key}
|
|
||||||
reaction={key}
|
|
||||||
shortcode={reactions[key].shortcode}
|
|
||||||
count={reactions[key].count}
|
|
||||||
users={reactions[key].users}
|
|
||||||
isActive={reactions[key].isActive}
|
|
||||||
onClick={() => {
|
|
||||||
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
{canSendReaction && (
|
|
||||||
<IconButton
|
|
||||||
onClick={(e) => {
|
|
||||||
pickEmoji(e, roomId, mEvent.getId(), roomTimeline);
|
|
||||||
}}
|
|
||||||
src={EmojiAddIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Add reaction"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
MessageReactionGroup.propTypes = {
|
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
|
||||||
mEvent: PropTypes.shape({}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function isMedia(mE) {
|
|
||||||
return (
|
|
||||||
mE.getContent()?.msgtype === 'm.file'
|
|
||||||
|| mE.getContent()?.msgtype === 'm.image'
|
|
||||||
|| mE.getContent()?.msgtype === 'm.audio'
|
|
||||||
|| mE.getContent()?.msgtype === 'm.video'
|
|
||||||
|| mE.getType() === 'm.sticker'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if editedTimeline has mEventId then pass editedMEvent else pass mEvent to openViewSource
|
|
||||||
function handleOpenViewSource(mEvent, roomTimeline) {
|
|
||||||
const eventId = mEvent.getId();
|
|
||||||
const { editedTimeline } = roomTimeline ?? {};
|
|
||||||
let editedMEvent;
|
|
||||||
if (editedTimeline?.has(eventId)) {
|
|
||||||
const editedList = editedTimeline.get(eventId);
|
|
||||||
editedMEvent = editedList[editedList.length - 1];
|
|
||||||
}
|
|
||||||
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageOptions = React.memo(({
|
|
||||||
roomTimeline, mEvent, edit, reply,
|
|
||||||
}) => {
|
|
||||||
const { roomId, room } = roomTimeline;
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const senderId = mEvent.getSender();
|
|
||||||
|
|
||||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
|
|
||||||
const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
|
|
||||||
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="message__options">
|
|
||||||
{canSendReaction && (
|
|
||||||
<IconButton
|
|
||||||
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
|
|
||||||
src={EmojiAddIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Add reaction"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
onClick={() => reply()}
|
|
||||||
src={ReplyArrowIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Reply"
|
|
||||||
/>
|
|
||||||
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => edit(true)}
|
|
||||||
src={PencilIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Edit"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ContextMenu
|
|
||||||
content={() => (
|
|
||||||
<>
|
|
||||||
<MenuHeader>Options</MenuHeader>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={TickMarkIC}
|
|
||||||
onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))}
|
|
||||||
>
|
|
||||||
Read receipts
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={CmdIC}
|
|
||||||
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
|
|
||||||
>
|
|
||||||
View source
|
|
||||||
</MenuItem>
|
|
||||||
{(canIRedact || senderId === mx.getUserId()) && (
|
|
||||||
<>
|
|
||||||
<MenuBorder />
|
|
||||||
<MenuItem
|
|
||||||
variant="danger"
|
|
||||||
iconSrc={BinIC}
|
|
||||||
onClick={async () => {
|
|
||||||
const isConfirmed = await confirmDialog(
|
|
||||||
'Delete message',
|
|
||||||
'Are you sure that you want to delete this message?',
|
|
||||||
'Delete',
|
|
||||||
'danger',
|
|
||||||
);
|
|
||||||
if (!isConfirmed) return;
|
|
||||||
redactEvent(roomId, mEvent.getId());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
render={(toggleMenu) => (
|
|
||||||
<IconButton
|
|
||||||
onClick={toggleMenu}
|
|
||||||
src={VerticalMenuIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Options"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
MessageOptions.propTypes = {
|
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
|
||||||
mEvent: PropTypes.shape({}).isRequired,
|
|
||||||
edit: PropTypes.func.isRequired,
|
|
||||||
reply: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function genMediaContent(mE) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const mContent = mE.getContent();
|
|
||||||
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
|
||||||
|
|
||||||
let mediaMXC = mContent?.url;
|
|
||||||
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
|
||||||
if (isEncryptedFile) mediaMXC = mContent?.file?.url;
|
|
||||||
|
|
||||||
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
|
||||||
|
|
||||||
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
|
||||||
|
|
||||||
let msgType = mE.getContent()?.msgtype;
|
|
||||||
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
|
|
||||||
if (mE.getType() === 'm.sticker') {
|
|
||||||
msgType = 'm.sticker';
|
|
||||||
} else if (safeMimetype === 'application/octet-stream') {
|
|
||||||
msgType = 'm.file';
|
|
||||||
}
|
|
||||||
|
|
||||||
const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
|
|
||||||
|
|
||||||
switch (msgType) {
|
|
||||||
case 'm.file':
|
|
||||||
return (
|
|
||||||
<Media.File
|
|
||||||
name={mContent.body}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
type={mContent.info?.mimetype}
|
|
||||||
file={mContent.file || null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'm.image':
|
|
||||||
return (
|
|
||||||
<Media.Image
|
|
||||||
name={mContent.body}
|
|
||||||
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
|
||||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
|
||||||
type={mContent.info?.mimetype}
|
|
||||||
blurhash={blurhash}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'm.sticker':
|
|
||||||
return (
|
|
||||||
<Media.Sticker
|
|
||||||
name={mContent.body}
|
|
||||||
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
|
||||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
|
||||||
type={mContent.info?.mimetype}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'm.audio':
|
|
||||||
return (
|
|
||||||
<Media.Audio
|
|
||||||
name={mContent.body}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
type={mContent.info?.mimetype}
|
|
||||||
file={mContent.file || null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'm.video':
|
|
||||||
if (typeof thumbnailMXC === 'undefined') {
|
|
||||||
thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Media.Video
|
|
||||||
name={mContent.body}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
|
|
||||||
thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
|
|
||||||
thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
|
|
||||||
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
|
||||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
|
||||||
type={mContent.info?.mimetype}
|
|
||||||
blurhash={blurhash}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEditedBody(editedMEvent) {
|
|
||||||
const newContent = editedMEvent.getContent()['m.new_content'];
|
|
||||||
if (typeof newContent === 'undefined') return [null, false, null];
|
|
||||||
|
|
||||||
const isCustomHTML = newContent.format === 'org.matrix.custom.html';
|
|
||||||
const parsedContent = parseReply(newContent.body);
|
|
||||||
if (parsedContent === null) {
|
|
||||||
return [newContent.body, isCustomHTML, newContent.formatted_body ?? null];
|
|
||||||
}
|
|
||||||
return [parsedContent.body, isCustomHTML, newContent.formatted_body ?? null];
|
|
||||||
}
|
|
||||||
|
|
||||||
function Message({
|
|
||||||
mEvent, isBodyOnly, roomTimeline,
|
|
||||||
focus, fullTime, isEdit, setEdit, cancelEdit,
|
|
||||||
}) {
|
|
||||||
const roomId = mEvent.getRoomId();
|
|
||||||
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
|
|
||||||
|
|
||||||
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
|
|
||||||
if (focus) className.push('message--focus');
|
|
||||||
const content = mEvent.getContent();
|
|
||||||
const eventId = mEvent.getId();
|
|
||||||
const msgType = content?.msgtype;
|
|
||||||
const senderId = mEvent.getSender();
|
|
||||||
let { body } = content;
|
|
||||||
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
|
|
||||||
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
|
|
||||||
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
|
||||||
let customHTML = isCustomHTML ? content.formatted_body : null;
|
|
||||||
|
|
||||||
const edit = useCallback(() => {
|
|
||||||
setEdit(eventId);
|
|
||||||
}, []);
|
|
||||||
const reply = useCallback(() => {
|
|
||||||
replyTo(senderId, mEvent.getId(), body, customHTML);
|
|
||||||
}, [body, customHTML]);
|
|
||||||
|
|
||||||
if (msgType === 'm.emote') className.push('message--type-emote');
|
|
||||||
|
|
||||||
const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
|
|
||||||
const haveReactions = roomTimeline
|
|
||||||
? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
|
|
||||||
: false;
|
|
||||||
const isReply = !!mEvent.replyEventId;
|
|
||||||
|
|
||||||
if (isEdited) {
|
|
||||||
const editedList = editedTimeline.get(eventId);
|
|
||||||
const editedMEvent = editedList[editedList.length - 1];
|
|
||||||
[body, isCustomHTML, customHTML] = getEditedBody(editedMEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReply) {
|
|
||||||
body = parseReply(body)?.body ?? body;
|
|
||||||
customHTML = trimHTMLReply(customHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof body !== 'string') body = '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className.join(' ')}>
|
|
||||||
{
|
|
||||||
isBodyOnly
|
|
||||||
? <div className="message__avatar-container" />
|
|
||||||
: (
|
|
||||||
<MessageAvatar
|
|
||||||
roomId={roomId}
|
|
||||||
avatarSrc={avatarSrc}
|
|
||||||
userId={senderId}
|
|
||||||
username={username}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div className="message__main-container">
|
|
||||||
{!isBodyOnly && (
|
|
||||||
<MessageHeader
|
|
||||||
userId={senderId}
|
|
||||||
username={username}
|
|
||||||
timestamp={mEvent.getTs()}
|
|
||||||
fullTime={fullTime}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{roomTimeline && isReply && (
|
|
||||||
<MessageReplyWrapper
|
|
||||||
roomTimeline={roomTimeline}
|
|
||||||
eventId={mEvent.replyEventId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isEdit && (
|
|
||||||
<MessageBody
|
|
||||||
senderName={username}
|
|
||||||
isCustomHTML={isCustomHTML}
|
|
||||||
body={isMedia(mEvent) ? genMediaContent(mEvent) : customHTML ?? body}
|
|
||||||
msgType={msgType}
|
|
||||||
isEdited={isEdited}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isEdit && (
|
|
||||||
<MessageEdit
|
|
||||||
body={(customHTML
|
|
||||||
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
|
|
||||||
: plain(body, { kind: 'edit', onlyPlain: true }).plain)}
|
|
||||||
onSave={(newBody, oldBody) => {
|
|
||||||
if (newBody !== oldBody) {
|
|
||||||
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
|
||||||
}
|
|
||||||
cancelEdit();
|
|
||||||
}}
|
|
||||||
onCancel={cancelEdit}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{haveReactions && (
|
|
||||||
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
|
|
||||||
)}
|
|
||||||
{roomTimeline && !isEdit && (
|
|
||||||
<MessageOptions
|
|
||||||
roomTimeline={roomTimeline}
|
|
||||||
mEvent={mEvent}
|
|
||||||
edit={edit}
|
|
||||||
reply={reply}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Message.defaultProps = {
|
|
||||||
isBodyOnly: false,
|
|
||||||
focus: false,
|
|
||||||
roomTimeline: null,
|
|
||||||
fullTime: false,
|
|
||||||
isEdit: false,
|
|
||||||
setEdit: null,
|
|
||||||
cancelEdit: null,
|
|
||||||
};
|
|
||||||
Message.propTypes = {
|
|
||||||
mEvent: PropTypes.shape({}).isRequired,
|
|
||||||
isBodyOnly: PropTypes.bool,
|
|
||||||
roomTimeline: PropTypes.shape({}),
|
|
||||||
focus: PropTypes.bool,
|
|
||||||
fullTime: PropTypes.bool,
|
|
||||||
isEdit: PropTypes.bool,
|
|
||||||
setEdit: PropTypes.func,
|
|
||||||
cancelEdit: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Message, MessageReply, PlaceholderMessage };
|
|
|
@ -1,479 +0,0 @@
|
||||||
@use '../../atoms/scroll/scrollbar';
|
|
||||||
@use '../../partials/text';
|
|
||||||
@use '../../partials/dir';
|
|
||||||
@use '../../partials/screen';
|
|
||||||
|
|
||||||
.message,
|
|
||||||
.ph-msg {
|
|
||||||
padding: var(--sp-ultra-tight);
|
|
||||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
& .message__options {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar-container {
|
|
||||||
padding-top: 6px;
|
|
||||||
@include dir.side(margin, 0, var(--sp-tight));
|
|
||||||
|
|
||||||
& .avatar-container {
|
|
||||||
transition: transform 200ms var(--fluid-push);
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& button {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main-container {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
&--full + &--full,
|
|
||||||
&--body-only + &--full,
|
|
||||||
& + .timeline-change,
|
|
||||||
.timeline-change + & {
|
|
||||||
margin-top: var(--sp-normal);
|
|
||||||
}
|
|
||||||
&__avatar-container {
|
|
||||||
width: var(--av-small);
|
|
||||||
}
|
|
||||||
&--focus {
|
|
||||||
--ltr: inset 2px 0 0 var(--bg-caution);
|
|
||||||
--rtl: inset -2px 0 0 var(--bg-caution);
|
|
||||||
@include dir.prop(box-shadow, var(--ltr), var(--rtl));
|
|
||||||
background-color: var(--bg-caution-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ph-msg {
|
|
||||||
&__avatar {
|
|
||||||
width: var(--av-small);
|
|
||||||
height: var(--av-small);
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
border-radius: var(--bo-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header,
|
|
||||||
&__body > div {
|
|
||||||
margin: var(--sp-ultra-tight);
|
|
||||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
|
||||||
height: var(--fs-b1);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100px;
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
|
||||||
}
|
|
||||||
&__body {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
&__body > div:nth-child(1n) {
|
|
||||||
max-width: 10%;
|
|
||||||
}
|
|
||||||
&__body > div:nth-child(2n) {
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message__reply,
|
|
||||||
.message__body,
|
|
||||||
.message__body__wrapper,
|
|
||||||
.message__edit,
|
|
||||||
.message__reactions {
|
|
||||||
max-width: calc(100% - 88px);
|
|
||||||
min-width: 0;
|
|
||||||
@include screen.smallerThan(tabletBreakpoint) {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
|
|
||||||
& .message__profile {
|
|
||||||
min-width: 0;
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
@include dir.side(margin, 0, var(--sp-tight));
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
@extend .cp-txt__ellipsis;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
& > span:last-child {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
& > span:first-child {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
& > span:last-child {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .message__time {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
& > .text {
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--tc-surface-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.message__reply {
|
|
||||||
&-wrapper {
|
|
||||||
min-height: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
&:empty {
|
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
max-width: 200px;
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
&:hover .text {
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
@extend .cp-txt__ellipsis;
|
|
||||||
color: var(--tc-surface-low);
|
|
||||||
}
|
|
||||||
.ic-raw {
|
|
||||||
width: 16px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.message__body {
|
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
& > .text > .message__body-plain {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
& a {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
& > .text > a {
|
|
||||||
white-space: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .text > p + p {
|
|
||||||
margin-top: var(--sp-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
& span[data-mx-pill] {
|
|
||||||
background-color: hsla(0, 0%, 64%, 0.15);
|
|
||||||
padding: 0 2px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: var(--fw-medium);
|
|
||||||
&:hover {
|
|
||||||
background-color: hsla(0, 0%, 64%, 0.3);
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-mx-ping] {
|
|
||||||
background-color: var(--bg-ping);
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-ping-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& span[data-mx-spoiler] {
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: rgba(124, 124, 124, 0.5);
|
|
||||||
color: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
& > * {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-mx-spoiler--visible {
|
|
||||||
background-color: var(--bg-surface-active) !important;
|
|
||||||
color: inherit !important;
|
|
||||||
user-select: initial !important;
|
|
||||||
& > * {
|
|
||||||
opacity: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-edited {
|
|
||||||
color: var(--tc-surface-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.message__edit {
|
|
||||||
padding: var(--sp-extra-tight) 0;
|
|
||||||
&-btns button {
|
|
||||||
margin: var(--sp-tight) 0 0 0;
|
|
||||||
padding: var(--sp-ultra-tight) var(--sp-tight);
|
|
||||||
min-width: 0;
|
|
||||||
@include dir.side(margin, 0, var(--sp-tight));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.message__reactions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
& .ic-btn-surface {
|
|
||||||
display: none;
|
|
||||||
padding: var(--sp-ultra-tight);
|
|
||||||
margin-top: var(--sp-extra-tight);
|
|
||||||
}
|
|
||||||
&:hover .ic-btn-surface {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.msg__reaction {
|
|
||||||
margin: var(--sp-extra-tight) 0 0 0;
|
|
||||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
|
||||||
padding: 0 var(--sp-ultra-tight);
|
|
||||||
min-height: 26px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--tc-surface-normal);
|
|
||||||
background-color: var(--bg-surface-low);
|
|
||||||
border: 1px solid var(--bg-surface-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
& .react-emoji {
|
|
||||||
height: 16px;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
&-count {
|
|
||||||
margin: 0 var(--sp-ultra-tight);
|
|
||||||
color: var(--tc-surface-normal);
|
|
||||||
}
|
|
||||||
&-tooltip .react-emoji {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin: 0 var(--sp-ultra-tight);
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: var(--bg-surface-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
background-color: var(--bg-caution-active);
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-caution-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: var(--bg-caution-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.message__options {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
@include dir.prop(right, 60px, unset);
|
|
||||||
@include dir.prop(left, unset, 60px);
|
|
||||||
|
|
||||||
z-index: 99;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
|
|
||||||
border-radius: var(--bo-radius);
|
|
||||||
box-shadow: var(--bs-surface-border);
|
|
||||||
background-color: var(--bg-surface-low);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// markdown formating
|
|
||||||
.message__body {
|
|
||||||
& h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: var(--sp-ultra-tight);
|
|
||||||
font-weight: var(--fw-medium);
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& h1,
|
|
||||||
& h2 {
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
margin-top: var(--sp-normal);
|
|
||||||
font-size: var(--fs-h2);
|
|
||||||
line-height: var(--lh-h2);
|
|
||||||
letter-spacing: var(--ls-h2);
|
|
||||||
}
|
|
||||||
& h3,
|
|
||||||
& h4 {
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
margin-top: var(--sp-tight);
|
|
||||||
font-size: var(--fs-s1);
|
|
||||||
line-height: var(--lh-s1);
|
|
||||||
letter-spacing: var(--ls-s1);
|
|
||||||
}
|
|
||||||
& h5,
|
|
||||||
& h6 {
|
|
||||||
color: var(--tc-surface-high);
|
|
||||||
margin-top: var(--sp-extra-tight);
|
|
||||||
font-size: var(--fs-b1);
|
|
||||||
line-height: var(--lh-b1);
|
|
||||||
letter-spacing: var(--ls-b1);
|
|
||||||
}
|
|
||||||
& hr {
|
|
||||||
border-color: var(--bg-divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text img {
|
|
||||||
margin: var(--sp-ultra-tight) 0;
|
|
||||||
max-width: 296px;
|
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
& p,
|
|
||||||
& pre,
|
|
||||||
& blockquote {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
& pre,
|
|
||||||
& blockquote {
|
|
||||||
margin: var(--sp-ultra-tight) 0;
|
|
||||||
padding: var(--sp-extra-tight);
|
|
||||||
background-color: var(--bg-surface-hover) !important;
|
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
|
||||||
}
|
|
||||||
& pre {
|
|
||||||
div {
|
|
||||||
background: none !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
.linenumber {
|
|
||||||
min-width: 2.25em !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& code {
|
|
||||||
padding: 0 !important;
|
|
||||||
color: var(--tc-code) !important;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
@include scrollbar.scroll;
|
|
||||||
@include scrollbar.scroll__h;
|
|
||||||
@include scrollbar.scroll--auto-hide;
|
|
||||||
}
|
|
||||||
& pre {
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
@include scrollbar.scroll;
|
|
||||||
@include scrollbar.scroll__h;
|
|
||||||
@include scrollbar.scroll--auto-hide;
|
|
||||||
& code {
|
|
||||||
color: var(--tc-surface-normal) !important;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& blockquote {
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
@include dir.side(border, 4px solid var(--bg-surface-active), 0);
|
|
||||||
white-space: initial !important;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& ul,
|
|
||||||
& ol {
|
|
||||||
margin: var(--sp-ultra-tight) 0;
|
|
||||||
@include dir.side(padding, 24px, 0);
|
|
||||||
white-space: initial !important;
|
|
||||||
}
|
|
||||||
& ul.contains-task-list {
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
& table {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 100%;
|
|
||||||
white-space: normal !important;
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
border-radius: calc(var(--bo-radius) / 2);
|
|
||||||
border-spacing: 0;
|
|
||||||
border: 1px solid var(--bg-surface-border);
|
|
||||||
@include scrollbar.scroll;
|
|
||||||
@include scrollbar.scroll__h;
|
|
||||||
@include scrollbar.scroll--auto-hide;
|
|
||||||
|
|
||||||
& td,
|
|
||||||
& th {
|
|
||||||
padding: var(--sp-extra-tight);
|
|
||||||
border: 1px solid var(--bg-surface-border);
|
|
||||||
border-width: 0 1px 1px 0;
|
|
||||||
white-space: pre;
|
|
||||||
&:last-child {
|
|
||||||
border-width: 0;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
[dir='rtl'] & {
|
|
||||||
border-width: 0 1px 1px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[dir='rtl'] &:first-child {
|
|
||||||
border-width: 0;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& tbody tr:nth-child(2n + 1) {
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
}
|
|
||||||
& tr:last-child td {
|
|
||||||
border-bottom-width: 0px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.message--type-emote {
|
|
||||||
.message__body {
|
|
||||||
font-style: italic;
|
|
||||||
|
|
||||||
// Remove blockness of first `<p>` so that markdown emotes stay on one line.
|
|
||||||
p:first-of-type {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './TimelineChange.scss';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
|
||||||
import Time from '../../atoms/time/Time';
|
|
||||||
|
|
||||||
import JoinArraowIC from '../../../../public/res/ic/outlined/join-arrow.svg';
|
|
||||||
import LeaveArraowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
|
||||||
import InviteArraowIC from '../../../../public/res/ic/outlined/invite-arrow.svg';
|
|
||||||
import InviteCancelArraowIC from '../../../../public/res/ic/outlined/invite-cancel-arrow.svg';
|
|
||||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
|
||||||
|
|
||||||
function TimelineChange({
|
|
||||||
variant, content, timestamp, onClick,
|
|
||||||
}) {
|
|
||||||
let iconSrc;
|
|
||||||
|
|
||||||
switch (variant) {
|
|
||||||
case 'join':
|
|
||||||
iconSrc = JoinArraowIC;
|
|
||||||
break;
|
|
||||||
case 'leave':
|
|
||||||
iconSrc = LeaveArraowIC;
|
|
||||||
break;
|
|
||||||
case 'invite':
|
|
||||||
iconSrc = InviteArraowIC;
|
|
||||||
break;
|
|
||||||
case 'invite-cancel':
|
|
||||||
iconSrc = InviteCancelArraowIC;
|
|
||||||
break;
|
|
||||||
case 'avatar':
|
|
||||||
iconSrc = UserIC;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
iconSrc = JoinArraowIC;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button style={{ cursor: onClick === null ? 'default' : 'pointer' }} onClick={onClick} type="button" className="timeline-change">
|
|
||||||
<div className="timeline-change__avatar-container">
|
|
||||||
<RawIcon src={iconSrc} size="extra-small" />
|
|
||||||
</div>
|
|
||||||
<div className="timeline-change__content">
|
|
||||||
<Text variant="b2">
|
|
||||||
{content}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="timeline-change__time">
|
|
||||||
<Text variant="b3">
|
|
||||||
<Time timestamp={timestamp} />
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimelineChange.defaultProps = {
|
|
||||||
variant: 'other',
|
|
||||||
onClick: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
TimelineChange.propTypes = {
|
|
||||||
variant: PropTypes.oneOf([
|
|
||||||
'join', 'leave', 'invite',
|
|
||||||
'invite-cancel', 'avatar', 'other',
|
|
||||||
]),
|
|
||||||
content: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.node,
|
|
||||||
]).isRequired,
|
|
||||||
timestamp: PropTypes.number.isRequired,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimelineChange;
|
|
|
@ -1,37 +0,0 @@
|
||||||
@use '../../partials/dir';
|
|
||||||
|
|
||||||
.timeline-change {
|
|
||||||
padding: var(--sp-ultra-tight);
|
|
||||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar-container {
|
|
||||||
width: var(--av-small);
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
opacity: 0.38;
|
|
||||||
.ic-raw {
|
|
||||||
background-color: var(--tc-surface-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .text {
|
|
||||||
color: var(--tc-surface-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
margin: 0 var(--sp-tight);
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,16 +2,12 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './PeopleSelector.scss';
|
import './PeopleSelector.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
|
|
||||||
function PeopleSelector({
|
function PeopleSelector({ avatarSrc, name, color, peopleRole, onClick }) {
|
||||||
avatarSrc, name, color, peopleRole, onClick,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="people-selector__container">
|
<div className="people-selector__container">
|
||||||
<button
|
<button
|
||||||
|
@ -21,8 +17,14 @@ function PeopleSelector({
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
|
<Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
|
||||||
<Text className="people-selector__name" variant="b1">{twemojify(name)}</Text>
|
<Text className="people-selector__name" variant="b1">
|
||||||
{peopleRole !== null && <Text className="people-selector__role" variant="b3">{peopleRole}</Text>}
|
{name}
|
||||||
|
</Text>
|
||||||
|
{peopleRole !== null && (
|
||||||
|
<Text className="people-selector__role" variant="b3">
|
||||||
|
{peopleRole}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './PopupWindow.scss';
|
import './PopupWindow.scss';
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
@ -13,19 +11,11 @@ import RawModal from '../../atoms/modal/RawModal';
|
||||||
|
|
||||||
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
||||||
|
|
||||||
function PWContentSelector({
|
function PWContentSelector({ selected, variant, iconSrc, type, onClick, children }) {
|
||||||
selected, variant, iconSrc,
|
|
||||||
type, onClick, children,
|
|
||||||
}) {
|
|
||||||
const pwcsClass = selected ? ' pw-content-selector--selected' : '';
|
const pwcsClass = selected ? ' pw-content-selector--selected' : '';
|
||||||
return (
|
return (
|
||||||
<div className={`pw-content-selector${pwcsClass}`}>
|
<div className={`pw-content-selector${pwcsClass}`}>
|
||||||
<MenuItem
|
<MenuItem variant={variant} iconSrc={iconSrc} type={type} onClick={onClick}>
|
||||||
variant={variant}
|
|
||||||
iconSrc={iconSrc}
|
|
||||||
type={type}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,9 +39,16 @@ PWContentSelector.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function PopupWindow({
|
function PopupWindow({
|
||||||
className, isOpen, title, contentTitle,
|
className,
|
||||||
drawer, drawerOptions, contentOptions,
|
isOpen,
|
||||||
onAfterClose, onRequestClose, children,
|
title,
|
||||||
|
contentTitle,
|
||||||
|
drawer,
|
||||||
|
drawerOptions,
|
||||||
|
contentOptions,
|
||||||
|
onAfterClose,
|
||||||
|
onRequestClose,
|
||||||
|
children,
|
||||||
}) {
|
}) {
|
||||||
const haveDrawer = drawer !== null;
|
const haveDrawer = drawer !== null;
|
||||||
const cTitle = contentTitle !== null ? contentTitle : title;
|
const cTitle = contentTitle !== null ? contentTitle : title;
|
||||||
|
@ -69,21 +66,26 @@ function PopupWindow({
|
||||||
{haveDrawer && (
|
{haveDrawer && (
|
||||||
<div className="pw__drawer">
|
<div className="pw__drawer">
|
||||||
<Header>
|
<Header>
|
||||||
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
src={ChevronLeftIC}
|
||||||
|
onClick={onRequestClose}
|
||||||
|
tooltip="Back"
|
||||||
|
/>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
{
|
{typeof title === 'string' ? (
|
||||||
typeof title === 'string'
|
<Text variant="s1" weight="medium" primary>
|
||||||
? <Text variant="s1" weight="medium" primary>{twemojify(title)}</Text>
|
{title}
|
||||||
: title
|
</Text>
|
||||||
}
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{drawerOptions}
|
{drawerOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="pw__drawer__content__wrapper">
|
<div className="pw__drawer__content__wrapper">
|
||||||
<ScrollView invisible>
|
<ScrollView invisible>
|
||||||
<div className="pw__drawer__content">
|
<div className="pw__drawer__content">{drawer}</div>
|
||||||
{drawer}
|
|
||||||
</div>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,19 +93,19 @@ function PopupWindow({
|
||||||
<div className="pw__content">
|
<div className="pw__content">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
{
|
{typeof cTitle === 'string' ? (
|
||||||
typeof cTitle === 'string'
|
<Text variant="h2" weight="medium" primary>
|
||||||
? <Text variant="h2" weight="medium" primary>{twemojify(cTitle)}</Text>
|
{cTitle}
|
||||||
: cTitle
|
</Text>
|
||||||
}
|
) : (
|
||||||
|
cTitle
|
||||||
|
)}
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
{contentOptions}
|
{contentOptions}
|
||||||
</Header>
|
</Header>
|
||||||
<div className="pw__content__wrapper">
|
<div className="pw__content__wrapper">
|
||||||
<ScrollView autoHide>
|
<ScrollView autoHide>
|
||||||
<div className="pw__content-container">
|
<div className="pw__content-container">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue