Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc423fcc96 |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
||||
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
|
||||
|
||||
### Description
|
||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
- name: Build app
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
path: dist
|
||||
retention-days: 1
|
||||
- name: Get PR info
|
||||
uses: actions/github-script@v6.2.0
|
||||
uses: actions/github-script@v6.1.0
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
|
||||
# so instead we get this mess:
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v6.2.0
|
||||
uses: actions/github-script@v6.1.0
|
||||
with:
|
||||
script: |
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
|
||||
- name: Read PR Info
|
||||
id: readctx
|
||||
uses: actions/github-script@v6.2.0
|
||||
uses: actions/github-script@v6.1.0
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
- name: Build and deploy to Netlify
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
- name: Build
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3.3.0
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
- name: Build and deploy to Netlify
|
||||
@@ -64,11 +64,10 @@ jobs:
|
||||
NETLIFY_DEPLOY_TO_PROD: true
|
||||
|
||||
push-to-dockerhub:
|
||||
name: Push Docker image to Docker Hub, ghcr
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
@@ -81,21 +80,13 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to the Container registry
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4.0.1
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||
ghcr.io/${{ github.repository }}
|
||||
images: ajbura/cinny
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
+1
-6
@@ -36,9 +36,4 @@ It is not always possible to phrase every change in such a manner, but it is des
|
||||
|
||||
Also, we use [ESLint](https://eslint.org/) for clean and stylistically consistent code syntax, so make sure your pull request follow it.
|
||||
|
||||
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
||||
|
||||
## Helpful links
|
||||
- [BEM methodology](http://getbem.com/introduction/)
|
||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/19.2.0/index.html)
|
||||
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
||||
+2
-2
@@ -3,14 +3,14 @@ FROM node:17.9.0-alpine3.15 as builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY .npmrc package.json package-lock.json /src/
|
||||
COPY package.json package-lock.json /src/
|
||||
RUN npm ci
|
||||
COPY . /src/
|
||||
RUN npm run build
|
||||
|
||||
|
||||
## App
|
||||
FROM nginx:1.23.1-alpine
|
||||
FROM nginx:1.23.0-alpine
|
||||
|
||||
COPY --from=builder /src/dist /app
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-present Ajay Bura (ajbura)
|
||||
Copyright (c) 2021 Ajay Bura (ajbura)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Cinny
|
||||
<p>
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/ajbura/cinny/dev/public/res/svg/cinny.svg?sanitize=true"
|
||||
height="16">
|
||||
<span><b>Cinny</b></span>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/ajbura/cinny/releases">
|
||||
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
|
||||
<a href="https://hub.docker.com/r/ajbura/cinny">
|
||||
@@ -12,30 +16,21 @@
|
||||
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
|
||||
</p>
|
||||
|
||||
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
|
||||
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
||||
**Cinny** is a Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have a client that is easy on end user
|
||||
and feels a modern chat application.
|
||||
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
||||
|
||||
## Getting started
|
||||
Web app is available at https://app.cinny.in and gets updated on each new release. The `dev` branch is continuously deployed at https://dev.cinny.in but keep in mind that it could have things broken.
|
||||
## Building and Running
|
||||
|
||||
You can also download our desktop app from [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||
### Running pre-compiled
|
||||
|
||||
To host Cinny on your own, download tarball of the app from [GitHub release](https://github.com/cinnyapp/cinny/releases/latest).
|
||||
You can serve the application with a webserver of your choice by simply copying `dist/` directory to the webroot.
|
||||
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
|
||||
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
|
||||
```
|
||||
docker pull ajbura/cinny
|
||||
```
|
||||
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
|
||||
```
|
||||
docker pull ghcr.io/cinnyapp/cinny:latest
|
||||
```
|
||||
A tarball of pre-compiled version of the app is provided with each [release](https://github.com/ajbura/cinny/releases).
|
||||
You can serve the application with a webserver of your choosing by simply copying `dist/` directory to the webroot.
|
||||
|
||||
<details>
|
||||
<summary>PGP Public Key to verify tarball</summary>
|
||||
<summary>PGP Public Key to verify pre-compiled tarball</summary>
|
||||
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
@@ -82,39 +77,48 @@ UeGsouhyuITLwEhScounZDqop+Dx
|
||||
```
|
||||
</details>
|
||||
|
||||
## Local development
|
||||
### Building from source
|
||||
> We recommend using a version manager as versions change very quickly. You will likely need to switch
|
||||
between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Also recommended nodejs version is 16.15.0 LTS.
|
||||
|
||||
Execute the following commands to start a development server:
|
||||
Execute the following commands to compile the app from its source code:
|
||||
|
||||
```sh
|
||||
npm ci # Installs all dependencies
|
||||
npm start # Serve a development version
|
||||
```
|
||||
|
||||
To build the app:
|
||||
```sh
|
||||
npm run build # Compiles the app into the dist/ directory
|
||||
```
|
||||
|
||||
You can then copy the files to a webserver's webroot of your choice.
|
||||
|
||||
To serve a development version of the app locally for testing, you need to use the command `npm start`.
|
||||
|
||||
### Running with Docker
|
||||
|
||||
This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
|
||||
use this locally, you can build the container like so:
|
||||
|
||||
```
|
||||
docker build -t cinny:latest .
|
||||
```
|
||||
|
||||
You can then run the container you've built with a command similar to this:
|
||||
|
||||
```
|
||||
docker run -p 8080:80 cinny:latest
|
||||
```
|
||||
|
||||
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
|
||||
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by
|
||||
navigating to `http://localhost:8080`.
|
||||
|
||||
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by `docker pull ajbura/cinny`.
|
||||
|
||||
### Configuring default Homeserver
|
||||
|
||||
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2021-present Ajay Bura (ajbura)
|
||||
Copyright (c) 2021 Ajay Bura (ajbura)
|
||||
|
||||
Code licensed under the MIT License: <http://opensource.org/licenses/MIT>
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"defaultHomeserver": 3,
|
||||
"defaultHomeserver": 4,
|
||||
"homeserverList": [
|
||||
"converser.eu",
|
||||
"envs.net",
|
||||
"halogen.city",
|
||||
"kde.org",
|
||||
|
||||
Generated
+2010
-1364
File diff suppressed because it is too large.
Load diff
+26
-24
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "2.1.3",
|
||||
"version": "2.0.4",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"npm": ">=6.14.8",
|
||||
"node": ">=14.15.0"
|
||||
"npm": ">=6.14.8 <=8.5.5",
|
||||
"node": ">=14.15.0 <=17.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --config ./webpack.dev.js --open",
|
||||
@@ -15,40 +15,42 @@
|
||||
"author": "Ajay Bura",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.12",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@khanacademy/simple-markdown": "^0.8.3",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
||||
"@fontsource/inter": "^4.5.11",
|
||||
"@fontsource/roboto": "^4.5.7",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"blurhash": "^1.1.5",
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"dateformat": "^5.0.3",
|
||||
"emojibase-data": "^7.0.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"flux": "^4.0.3",
|
||||
"formik": "^2.2.9",
|
||||
"html-react-parser": "^3.0.4",
|
||||
"katex": "^0.16.2",
|
||||
"linkify-html": "^4.0.0-beta.5",
|
||||
"linkifyjs": "^4.0.0-beta.5",
|
||||
"matrix-js-sdk": "^19.4.0",
|
||||
"html-react-parser": "^2.0.0",
|
||||
"katex": "^0.15.6",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"matrix-js-sdk": "^18.1.0",
|
||||
"micromark": "^3.0.10",
|
||||
"micromark-extension-gfm": "^2.0.1",
|
||||
"micromark-extension-math": "^2.0.2",
|
||||
"micromark-util-chunked": "^1.0.0",
|
||||
"micromark-util-resolve-all": "^1.0.0",
|
||||
"micromark-util-symbol": "^1.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-autosize-textarea": "^7.1.0",
|
||||
"react-blurhash": "^0.1.3",
|
||||
"react-dnd": "^15.1.2",
|
||||
"react-dnd-html5-backend": "^15.1.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-modal": "^3.15.1",
|
||||
"sanitize-html": "^2.7.1",
|
||||
"sanitize-html": "^2.7.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"assert": "^2.0.0",
|
||||
"babel-loader": "^8.2.5",
|
||||
@@ -59,27 +61,27 @@
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-loader": "^4.1.0",
|
||||
"html-loader": "^3.1.2",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sass": "^1.54.5",
|
||||
"sass": "^1.53.0",
|
||||
"sass-loader": "^13.0.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.10.1",
|
||||
"webpack-dev-server": "^4.9.3",
|
||||
"webpack-merge": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2021-present Ajay Bura (ajbura)
|
||||
Graphics (c) by Ajay Bura (ajbura)
|
||||
|
||||
Graphic content is licensed under a
|
||||
Creative Commons Attribution 4.0 International License.
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 3V5H17.8L12.9 9.9L14.3 11.3L19 6.6V10.2H21V3H14Z" fill="black"/>
|
||||
<path d="M5 5H10V3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V14.2H19V19H5V5Z" fill="black"/>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<g>
|
||||
<polygon points="14,3 14,5 17.8,5 12.9,9.9 14.3,11.3 19,6.6 19,10.2 21,10.2 21,3 "/>
|
||||
<path d="M3,10.2h2V5h5V3H5C3.9,3,3,3.9,3,5V10.2z"/>
|
||||
<path d="M5,14.2H3V19c0,1.1,0.9,2,2,2h5v-2H5V14.2z"/>
|
||||
<path d="M19,19h-5v2h5c1.1,0,2-0.9,2-2v-4.8h-2V19z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 297 B After Width: | Height: | Size: 703 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.92896 3.51471L3.51474 4.92892L5.97515 7.38933C4.46742 8.5776 3.32116 9.93994 2.7 10.8C2.1 11.5 2.1 12.5 2.7 13.2C4 15 7.6 19 12 19C13.5709 19 15.0398 18.4902 16.3384 17.7526L19.0711 20.4853L20.4853 19.0711L4.92896 3.51471ZM4.2 12C4.68291 11.3561 5.85678 9.9637 7.39721 8.81139L9.29238 10.7066C9.10496 11.0982 9 11.5368 9 12C9 13.6569 10.3431 15 12 15C12.4632 15 12.9018 14.895 13.2934 14.7076L14.8573 16.2715C13.9566 16.7128 12.9896 17 12 17C8.4 17 5.1 13.2 4.2 12Z" fill="black"/>
|
||||
<path d="M9.6226 5.37995L11.2906 7.04797C11.5254 7.01661 11.762 7 12 7C15.6 7 18.9 10.8 19.8 12C19.493 12.4094 18.9066 13.1213 18.1244 13.8817L19.5194 15.2768C20.2973 14.4974 20.9049 13.7471 21.3 13.2C21.9 12.5 21.9 11.5 21.3 10.8C20 9 16.4 5 12 5C11.1762 5 10.3805 5.14021 9.6226 5.37995Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 943 B |
@@ -1,4 +1,13 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 19C7.6 19 4 15 2.7 13.2C2.1 12.5 2.1 11.5 2.7 10.8C4 9 7.6 5 12 5C16.4 5 20 9 21.3 10.8C21.9 11.5 21.9 12.5 21.3 13.2C20 15 16.4 19 12 19ZM12 7C8.4 7 5.1 10.8 4.2 12C5.1 13.2 8.4 17 12 17C15.6 17 18.9 13.2 19.8 12C18.9 10.8 15.6 7 12 7Z" fill="black"/>
|
||||
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" fill="black"/>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M12,19c-4.4,0-8-4-9.3-5.8c-0.6-0.7-0.6-1.7,0-2.4C4,9,7.6,5,12,5s8,4,9.3,5.8c0.6,0.7,0.6,1.7,0,2.4C20,15,16.4,19,12,19
|
||||
z M12,7c-3.6,0-6.9,3.8-7.8,5c0.9,1.2,4.2,5,7.8,5s6.9-3.8,7.8-5C18.9,10.8,15.6,7,12,7z"/>
|
||||
</g>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 718 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 3L21 8V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V5C3 3.89543 3.89543 3 5 3H16ZM19 9H17C15.8954 9 15 8.10457 15 7V5H5V19H19V9Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12H17C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12H9Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 501 B |
@@ -26,10 +26,10 @@
|
||||
&--icon {
|
||||
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
||||
|
||||
}
|
||||
.ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
flex-shrink: 0;
|
||||
.ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ function Input({
|
||||
{ resizable
|
||||
? (
|
||||
<TextareaAutosize
|
||||
dir="auto"
|
||||
style={{ minHeight: `${minHeight}px` }}
|
||||
name={name}
|
||||
id={id}
|
||||
@@ -35,7 +34,6 @@ function Input({
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
dir="auto"
|
||||
ref={forwardRef}
|
||||
id={id}
|
||||
name={name}
|
||||
|
||||
@@ -5,6 +5,7 @@ import katex from 'katex';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import 'katex/dist/contrib/copy-tex';
|
||||
import 'katex/dist/contrib/copy-tex.css';
|
||||
|
||||
const Math = React.memo(({
|
||||
content, throwOnError, errorColor, displayMode,
|
||||
|
||||
@@ -42,6 +42,7 @@ function RawModal({
|
||||
shouldCloseOnEsc={closeFromOutside}
|
||||
shouldCloseOnOverlayClick={closeFromOutside}
|
||||
shouldReturnFocusAfterClose={false}
|
||||
closeTimeoutMS={300}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
.ReactModal__Overlay {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms var(--fluid-slide-up);
|
||||
}
|
||||
.ReactModal__Overlay--after-open{
|
||||
opacity: 1;
|
||||
}
|
||||
.ReactModal__Overlay--before-close{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ReactModal__Content {
|
||||
transform: translateY(100%);
|
||||
transition: transform 200ms var(--fluid-slide-up);
|
||||
}
|
||||
|
||||
.ReactModal__Content--after-open{
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ReactModal__Content--before-close{
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.raw-modal {
|
||||
--small-modal-width: 525px;
|
||||
--medium-modal-width: 712px;
|
||||
--large-modal-width: 1024px;
|
||||
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
@@ -36,31 +60,4 @@
|
||||
height: 100%;
|
||||
background-color: var(--bg-overlay);
|
||||
}
|
||||
}
|
||||
|
||||
.ReactModal__Overlay {
|
||||
animation: raw-modal--overlay 150ms;
|
||||
}
|
||||
|
||||
.ReactModal__Content {
|
||||
animation: raw-modal--content 150ms;
|
||||
}
|
||||
|
||||
@keyframes raw-modal--content {
|
||||
0% {
|
||||
transform: translateY(100px);
|
||||
opacity: .5;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes raw-modal--overlay {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import dateFormat from 'dateformat';
|
||||
import { isInSameDay } from '../../../util/common';
|
||||
|
||||
function Time({ timestamp, fullTime }) {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
||||
let formattedDate = formattedFullTime;
|
||||
|
||||
if (!fullTime) {
|
||||
const compareDate = new Date();
|
||||
const isToday = isInSameDay(date, compareDate);
|
||||
compareDate.setDate(compareDate.getDate() - 1);
|
||||
const isYesterday = isInSameDay(date, compareDate);
|
||||
|
||||
formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
|
||||
if (isYesterday) {
|
||||
formattedDate = `Yesterday, ${formattedDate}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<time
|
||||
dateTime={date.toISOString()}
|
||||
title={formattedFullTime}
|
||||
>
|
||||
{formattedDate}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
|
||||
Time.defaultProps = {
|
||||
fullTime: false,
|
||||
};
|
||||
|
||||
Time.propTypes = {
|
||||
timestamp: PropTypes.number.isRequired,
|
||||
fullTime: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Time;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
import React, {
|
||||
useState, useMemo, useReducer, useEffect,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePack.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openReusableDialog } from '../../../client/action/navigation';
|
||||
import { suffixRename } from '../../../util/common';
|
||||
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Checkbox from '../../atoms/button/Checkbox';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import ImagePackProfile from './ImagePackProfile';
|
||||
import ImagePackItem from './ImagePackItem';
|
||||
import ImagePackUpload from './ImagePackUpload';
|
||||
|
||||
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Rename</Text>,
|
||||
(requestClose) => (
|
||||
<div style={{ padding: 'var(--sp-normal)' }}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const sc = e.target.shortcode.value;
|
||||
if (sc.trim() === '') return;
|
||||
isCompleted = true;
|
||||
resolve(sc.trim());
|
||||
requestClose();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={shortcode}
|
||||
name="shortcode"
|
||||
label="Shortcode"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<div style={{ height: 'var(--sp-normal)' }} />
|
||||
<Button variant="primary" type="submit">Rename</Button>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
() => {
|
||||
if (!isCompleted) resolve(null);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function getUsage(usage) {
|
||||
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
||||
if (usage.includes('emoticon')) return 'emoticon';
|
||||
if (usage.includes('sticker')) return 'sticker';
|
||||
|
||||
return 'both';
|
||||
}
|
||||
|
||||
function isGlobalPack(roomId, stateKey) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||
if (typeof globalContent !== 'object') return false;
|
||||
|
||||
const { rooms } = globalContent;
|
||||
if (typeof rooms !== 'object') return false;
|
||||
|
||||
return rooms[roomId]?.[stateKey] !== undefined;
|
||||
}
|
||||
|
||||
function useRoomImagePack(roomId, stateKey) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = useMemo(() => (
|
||||
ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent())
|
||||
), [room, stateKey]);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
||||
};
|
||||
|
||||
return {
|
||||
pack,
|
||||
sendPackContent,
|
||||
};
|
||||
}
|
||||
|
||||
function useUserImagePack() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
||||
const pack = useMemo(() => (
|
||||
ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? {
|
||||
pack: { display_name: 'Personal' },
|
||||
images: {},
|
||||
})
|
||||
), []);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.setAccountData('im.ponies.user_emotes', content);
|
||||
};
|
||||
|
||||
return {
|
||||
pack,
|
||||
sendPackContent,
|
||||
};
|
||||
}
|
||||
|
||||
function useImagePackHandles(pack, sendPackContent) {
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
|
||||
const getNewKey = (key) => {
|
||||
if (typeof key !== 'string') return undefined;
|
||||
let newKey = key?.replace(/\s/g, '_');
|
||||
if (pack.getImages().get(newKey)) {
|
||||
newKey = suffixRename(
|
||||
newKey,
|
||||
(suffixedKey) => pack.getImages().get(suffixedKey),
|
||||
);
|
||||
}
|
||||
return newKey;
|
||||
};
|
||||
|
||||
const handleAvatarChange = (url) => {
|
||||
pack.setAvatarUrl(url);
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleEditProfile = (name, attribution) => {
|
||||
pack.setDisplayName(name);
|
||||
pack.setAttribution(attribution);
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleUsageChange = (newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
pack.setUsage(usage);
|
||||
pack.getImages().forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
const handleRenameItem = async (key) => {
|
||||
const newKey = getNewKey(await renameImagePackItem(key));
|
||||
|
||||
if (!newKey || newKey === key) return;
|
||||
pack.updateImageKey(key, newKey);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleDeleteItem = async (key) => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete',
|
||||
`Are you sure that you want to delete "${key}"?`,
|
||||
'Delete',
|
||||
'danger',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
pack.removeImage(key);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleUsageItem = (key, newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
pack.setImageUsage(key, usage);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleAddItem = (key, url) => {
|
||||
const newKey = getNewKey(key);
|
||||
if (!newKey || !url) return;
|
||||
|
||||
pack.addImage(newKey, {
|
||||
url,
|
||||
});
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
return {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
};
|
||||
}
|
||||
|
||||
function addGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
if (!content.rooms) content.rooms = {};
|
||||
if (!content.rooms[roomId]) content.rooms[roomId] = {};
|
||||
content.rooms[roomId][stateKey] = {};
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
}
|
||||
function removeGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
if (!content.rooms) return Promise.resolve();
|
||||
if (!content.rooms[roomId]) return Promise.resolve();
|
||||
delete content.rooms[roomId][stateKey];
|
||||
if (Object.keys(content.rooms[roomId]).length === 0) {
|
||||
delete content.rooms[roomId];
|
||||
}
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
}
|
||||
|
||||
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
const [isGlobal, setIsGlobal] = useState(isGlobalPack(roomId, stateKey));
|
||||
|
||||
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
|
||||
|
||||
const {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
} = useImagePackHandles(pack, sendPackContent);
|
||||
|
||||
const handleGlobalChange = (isG) => {
|
||||
setIsGlobal(isG);
|
||||
if (isG) addGlobalImagePack(mx, roomId, stateKey);
|
||||
else removeGlobalImagePack(mx, roomId, stateKey);
|
||||
};
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
|
||||
const handleDeletePack = async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete Pack',
|
||||
`Are you sure that you want to delete "${pack.displayName}"?`,
|
||||
'Delete',
|
||||
'danger',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
handlePackDelete(stateKey);
|
||||
};
|
||||
|
||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
||||
|
||||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||
displayName={pack.displayName ?? 'Unknown'}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={canChange ? handleUsageChange : null}
|
||||
onAvatarChange={canChange ? handleAvatarChange : null}
|
||||
onEditProfile={canChange ? handleEditProfile : null}
|
||||
/>
|
||||
{ canChange && (
|
||||
<ImagePackUpload onUpload={handleAddItem} />
|
||||
)}
|
||||
{ images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
<Text variant="b3">Shortcode</Text>
|
||||
<Text variant="b3">Usage</Text>
|
||||
</div>
|
||||
{images.map(([shortcode, image]) => (
|
||||
<ImagePackItem
|
||||
key={shortcode}
|
||||
url={mx.mxcUrlToHttp(image.mxc)}
|
||||
shortcode={shortcode}
|
||||
usage={getUsage(image.usage)}
|
||||
onUsageChange={canChange ? handleUsageItem : undefined}
|
||||
onDelete={canChange ? handleDeleteItem : undefined}
|
||||
onRename={canChange ? handleRenameItem : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(pack.images.size > 2 || handlePackDelete) && (
|
||||
<div className="image-pack__footer">
|
||||
{pack.images.size > 2 && (
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{
|
||||
viewMore
|
||||
? 'View less'
|
||||
: `View ${pack.images.size - 2} more`
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
{ handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
|
||||
</div>
|
||||
)}
|
||||
<div className="image-pack__global">
|
||||
<Checkbox variant="positive" onToggle={handleGlobalChange} isActive={isGlobal} />
|
||||
<div>
|
||||
<Text variant="b2">Use globally</Text>
|
||||
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePack.defaultProps = {
|
||||
handlePackDelete: null,
|
||||
};
|
||||
ImagePack.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
stateKey: PropTypes.string.isRequired,
|
||||
handlePackDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
function ImagePackUser() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
|
||||
const { pack, sendPackContent } = useUserImagePack();
|
||||
|
||||
const {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
} = useImagePackHandles(pack, sendPackContent);
|
||||
|
||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
||||
|
||||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||
displayName={pack.displayName ?? 'Personal'}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={handleUsageChange}
|
||||
onAvatarChange={handleAvatarChange}
|
||||
onEditProfile={handleEditProfile}
|
||||
/>
|
||||
<ImagePackUpload onUpload={handleAddItem} />
|
||||
{ images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
<Text variant="b3">Shortcode</Text>
|
||||
<Text variant="b3">Usage</Text>
|
||||
</div>
|
||||
{images.map(([shortcode, image]) => (
|
||||
<ImagePackItem
|
||||
key={shortcode}
|
||||
url={mx.mxcUrlToHttp(image.mxc)}
|
||||
shortcode={shortcode}
|
||||
usage={getUsage(image.usage)}
|
||||
onUsageChange={handleUsageItem}
|
||||
onDelete={handleDeleteItem}
|
||||
onRename={handleRenameItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(pack.images.size > 2) && (
|
||||
<div className="image-pack__footer">
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{
|
||||
viewMore
|
||||
? 'View less'
|
||||
: `View ${pack.images.size - 2} more`
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useGlobalImagePack() {
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
const roomIdToStateKeys = new Map();
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
||||
const { rooms } = globalContent;
|
||||
|
||||
Object.keys(rooms).forEach((roomId) => {
|
||||
if (typeof rooms[roomId] !== 'object') return;
|
||||
const room = mx.getRoom(roomId);
|
||||
const stateKeys = Object.keys(rooms[roomId]);
|
||||
if (!room || stateKeys.length === 0) return;
|
||||
roomIdToStateKeys.set(roomId, stateKeys);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleEvent = (event) => {
|
||||
if (event.getType() === 'im.ponies.emote_rooms') forceUpdate();
|
||||
};
|
||||
mx.addListener('accountData', handleEvent);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return roomIdToStateKeys;
|
||||
}
|
||||
|
||||
function ImagePackGlobal() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const roomIdToStateKeys = useGlobalImagePack();
|
||||
|
||||
const handleChange = (roomId, stateKey) => {
|
||||
removeGlobalImagePack(mx, roomId, stateKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-global">
|
||||
<MenuHeader>Global packs</MenuHeader>
|
||||
<div>
|
||||
{
|
||||
roomIdToStateKeys.size > 0
|
||||
? [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
return (
|
||||
stateKeys.map((stateKey) => {
|
||||
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
|
||||
if (!pack) return null;
|
||||
return (
|
||||
<div className="image-pack__global" key={pack.id}>
|
||||
<Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive />
|
||||
<div>
|
||||
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
|
||||
<Text variant="b3">{room.name}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
: <div className="image-pack-global__empty"><Text>No global packs</Text></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImagePack;
|
||||
|
||||
export { ImagePackUser, ImagePackGlobal };
|
||||
@@ -1,47 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
|
||||
.image-pack {
|
||||
&-item {
|
||||
border-top: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
& > *:nth-child(2) {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: var(--sp-normal);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
|
||||
&__global {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: var(--sp-tight);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.image-pack-global {
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
||||
}
|
||||
& .image-pack__global {
|
||||
padding: 0 var(--sp-normal);
|
||||
padding-bottom: var(--sp-normal);
|
||||
&:first-child {
|
||||
padding-top: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackItem.scss';
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
|
||||
function ImagePackItem({
|
||||
url, shortcode, usage, onUsageChange, onDelete, onRename,
|
||||
}) {
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
onSelect={(newUsage) => {
|
||||
onUsageChange(shortcode, newUsage);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-item">
|
||||
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
|
||||
<div className="image-pack-item__content">
|
||||
<Text>{shortcode}</Text>
|
||||
</div>
|
||||
<div className="image-pack-item__usage">
|
||||
<div className="image-pack-item__btn">
|
||||
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
|
||||
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
|
||||
</div>
|
||||
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
||||
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
||||
<Text variant="b2">
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackItem.defaultProps = {
|
||||
onUsageChange: null,
|
||||
onDelete: null,
|
||||
onRename: null,
|
||||
};
|
||||
ImagePackItem.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
shortcode: PropTypes.string.isRequired,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onRename: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagePackItem;
|
||||
@@ -1,43 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.image-pack-item {
|
||||
margin: 0 var(--sp-normal);
|
||||
padding: var(--sp-tight) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
& .avatar-container img {
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
|
||||
&__usage {
|
||||
display: flex;
|
||||
gap: var(--sp-ultra-tight);
|
||||
& button {
|
||||
padding: 6px;
|
||||
}
|
||||
& > button.btn-surface {
|
||||
padding: 6px var(--sp-tight);
|
||||
min-width: 0;
|
||||
@include dir.side(margin, var(--sp-ultra-tight), 0);
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: none;
|
||||
}
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.image-pack-item__btn {
|
||||
display: flex;
|
||||
gap: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackProfile.scss';
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import ImageUpload from '../image-upload/ImageUpload';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
|
||||
function ImagePackProfile({
|
||||
avatarUrl, displayName, attribution, usage,
|
||||
onUsageChange, onAvatarChange, onEditProfile,
|
||||
}) {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { nameInput, attributionInput } = e.target;
|
||||
const name = nameInput.value.trim() || undefined;
|
||||
const att = attributionInput.value.trim() || undefined;
|
||||
|
||||
onEditProfile(name, att);
|
||||
setIsEdit(false);
|
||||
};
|
||||
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
onSelect={(newUsage) => {
|
||||
onUsageChange(newUsage);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-profile">
|
||||
{
|
||||
onAvatarChange
|
||||
? (
|
||||
<ImageUpload
|
||||
bgColor="#555"
|
||||
text={displayName}
|
||||
imageSrc={avatarUrl}
|
||||
size="normal"
|
||||
onUpload={onAvatarChange}
|
||||
onRequestRemove={() => onAvatarChange(undefined)}
|
||||
/>
|
||||
)
|
||||
: <Avatar bgColor="#555" text={displayName} imageSrc={avatarUrl} size="normal" />
|
||||
}
|
||||
<div className="image-pack-profile__content">
|
||||
{
|
||||
isEdit
|
||||
? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="nameInput" label="Name" value={displayName} required />
|
||||
<Input name="attributionInput" label="Attribution" value={attribution} resizable />
|
||||
<div>
|
||||
<Button variant="primary" type="submit">Save</Button>
|
||||
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Text>{displayName}</Text>
|
||||
{onEditProfile && <IconButton size="extra-small" onClick={() => setIsEdit(true)} src={PencilIC} tooltip="Edit" />}
|
||||
</div>
|
||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="image-pack-profile__usage">
|
||||
<Text variant="b3">Pack usage</Text>
|
||||
<Button
|
||||
onClick={onUsageChange ? handleUsageSelect : undefined}
|
||||
iconSrc={onUsageChange ? ChevronBottomIC : null}
|
||||
>
|
||||
<Text>
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackProfile.defaultProps = {
|
||||
avatarUrl: null,
|
||||
attribution: null,
|
||||
onUsageChange: null,
|
||||
onAvatarChange: null,
|
||||
onEditProfile: null,
|
||||
};
|
||||
ImagePackProfile.propTypes = {
|
||||
avatarUrl: PropTypes.string,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
attribution: PropTypes.string,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onAvatarChange: PropTypes.func,
|
||||
onEditProfile: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagePackProfile;
|
||||
@@ -1,37 +0,0 @@
|
||||
@use '../../partials/flex';
|
||||
|
||||
.image-pack-profile {
|
||||
padding: var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--sp-tight);
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
|
||||
& > div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-extra-tight);
|
||||
|
||||
& .ic-btn {
|
||||
padding: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
& > form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-extra-tight);
|
||||
& > div:last-child {
|
||||
margin: var(--sp-extra-tight) 0;
|
||||
display: flex;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__usage {
|
||||
& > *:first-child {
|
||||
margin-bottom: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackUpload.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { scaleDownImage } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||
|
||||
function ImagePackUpload({ onUpload }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const inputRef = useRef(null);
|
||||
const shortcodeRef = useRef(null);
|
||||
const [imgFile, setImgFile] = useState(null);
|
||||
const [progress, setProgress] = useState(false);
|
||||
|
||||
const handleSubmit = async (evt) => {
|
||||
evt.preventDefault();
|
||||
if (!imgFile) return;
|
||||
const { shortcodeInput } = evt.target;
|
||||
const shortcode = shortcodeInput.value.trim();
|
||||
if (shortcode === '') return;
|
||||
|
||||
setProgress(true);
|
||||
const image = await scaleDownImage(imgFile, 512, 512);
|
||||
const url = await mx.uploadContent(image, {
|
||||
onlyContentUri: true,
|
||||
});
|
||||
|
||||
onUpload(shortcode, url);
|
||||
setProgress(false);
|
||||
setImgFile(null);
|
||||
shortcodeRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleFileChange = (evt) => {
|
||||
const img = evt.target.files[0];
|
||||
if (!img) return;
|
||||
setImgFile(img);
|
||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf('.'));
|
||||
shortcodeRef.current.focus();
|
||||
};
|
||||
const handleRemove = () => {
|
||||
setImgFile(null);
|
||||
inputRef.current.value = null;
|
||||
shortcodeRef.current.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="image-pack-upload">
|
||||
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" accept=".png, .gif, .webp" required />
|
||||
{
|
||||
imgFile
|
||||
? (
|
||||
<div className="image-pack-upload__file">
|
||||
<IconButton onClick={handleRemove} src={CirclePlusIC} tooltip="Remove file" />
|
||||
<Text>{imgFile.name}</Text>
|
||||
</div>
|
||||
)
|
||||
: <Button onClick={() => inputRef.current.click()}>Import image</Button>
|
||||
}
|
||||
<Input forwardRef={shortcodeRef} name="shortcodeInput" placeholder="shortcode" required />
|
||||
<Button disabled={progress} variant="primary" type="submit">{progress ? 'Uploading...' : 'Upload'}</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
ImagePackUpload.propTypes = {
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackUpload;
|
||||
@@ -1,43 +0,0 @@
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.image-pack-upload {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
gap: var(--sp-tight);
|
||||
|
||||
& > .input-container {
|
||||
flex-grow: 1;
|
||||
input {
|
||||
padding: 9px var(--sp-normal);
|
||||
}
|
||||
}
|
||||
&__file {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--bg-surface-low);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
|
||||
& button {
|
||||
--parent-height: 40px;
|
||||
width: var(--parent-height);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& .ic-raw {
|
||||
background-color: var(--bg-caution);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
& .text {
|
||||
@extend .cp-txt__ellipsis;
|
||||
@include dir.side(margin, var(--sp-ultra-tight), var(--sp-normal));
|
||||
max-width: 86px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
||||
|
||||
function ImagePackUsageSelector({ usage, onSelect }) {
|
||||
return (
|
||||
<div>
|
||||
<MenuHeader>Usage</MenuHeader>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'emoticon' ? CheckIC : undefined}
|
||||
variant={usage === 'emoticon' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('emoticon')}
|
||||
>
|
||||
Emoji
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'sticker' ? CheckIC : undefined}
|
||||
variant={usage === 'sticker' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('sticker')}
|
||||
>
|
||||
Sticker
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'both' ? CheckIC : undefined}
|
||||
variant={usage === 'both' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('both')}
|
||||
>
|
||||
Both
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackUsageSelector.propTypes = {
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackUsageSelector;
|
||||
@@ -7,13 +7,9 @@ import initMatrix from '../../../client/initMatrix';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Spinner from '../../atoms/spinner/Spinner';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
|
||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||
|
||||
function ImageUpload({
|
||||
text, bgColor, imageSrc, onUpload, onRequestRemove,
|
||||
size,
|
||||
}) {
|
||||
const [uploadPromise, setUploadPromise] = useState(null);
|
||||
const uploadImageRef = useRef(null);
|
||||
@@ -54,14 +50,10 @@ function ImageUpload({
|
||||
imageSrc={imageSrc}
|
||||
text={text}
|
||||
bgColor={bgColor}
|
||||
size={size}
|
||||
size="large"
|
||||
/>
|
||||
<div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
|
||||
{uploadPromise === null && (
|
||||
size === 'large'
|
||||
? <Text variant="b3" weight="bold">Upload</Text>
|
||||
: <RawIcon src={PlusIC} color="white" />
|
||||
)}
|
||||
{uploadPromise === null && <Text variant="b3" weight="bold">Upload</Text>}
|
||||
{uploadPromise !== null && <Spinner size="small" />}
|
||||
</div>
|
||||
</button>
|
||||
@@ -83,7 +75,6 @@ ImageUpload.defaultProps = {
|
||||
text: null,
|
||||
bgColor: 'transparent',
|
||||
imageSrc: null,
|
||||
size: 'large',
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
@@ -92,7 +83,6 @@ ImageUpload.propTypes = {
|
||||
imageSrc: PropTypes.string,
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
onRequestRemove: PropTypes.func.isRequired,
|
||||
size: PropTypes.oneOf(['large', 'normal']),
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
@@ -4,17 +4,42 @@ 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';
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/a9e28db33058d1893d964ec96cd247ecc3d92fc3/src/utils/blobs.ts#L73
|
||||
const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
function getBlobSafeMimeType(mimetype) {
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
return mimetype;
|
||||
}
|
||||
|
||||
async function getDecryptedBlob(response, type, decryptData) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@@ -36,8 +61,9 @@ async function getUrl(link, type, decryptData) {
|
||||
}
|
||||
}
|
||||
|
||||
function getNativeHeight(width, height, maxWidth = 296) {
|
||||
const scale = maxWidth / width;
|
||||
function getNativeHeight(width, height) {
|
||||
const MEDIA_MAX_WIDTH = 296;
|
||||
const scale = MEDIA_MAX_WIDTH / width;
|
||||
return scale * height;
|
||||
}
|
||||
|
||||
@@ -121,11 +147,9 @@ File.propTypes = {
|
||||
};
|
||||
|
||||
function Image({
|
||||
name, width, height, link, file, type, blurhash,
|
||||
name, width, height, link, file, type,
|
||||
}) {
|
||||
const [url, setUrl] = useState(null);
|
||||
const [blur, setBlur] = useState(true);
|
||||
const [lightbox, setLightbox] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
@@ -140,42 +164,13 @@ function Image({
|
||||
};
|
||||
}, []);
|
||||
|
||||
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 className="file-container">
|
||||
<FileHeader name={name} link={url || link} type={type} external />
|
||||
<div style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }} className="image-container">
|
||||
{ url !== null && <img src={url || link} alt={name} />}
|
||||
</div>
|
||||
{url && (
|
||||
<ImageLightbox
|
||||
url={url}
|
||||
alt={name}
|
||||
isOpen={lightbox}
|
||||
onRequestClose={toggleLightbox}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Image.defaultProps = {
|
||||
@@ -183,7 +178,6 @@ Image.defaultProps = {
|
||||
width: null,
|
||||
height: null,
|
||||
type: '',
|
||||
blurhash: '',
|
||||
};
|
||||
Image.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
@@ -192,46 +186,6 @@ Image.propTypes = {
|
||||
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({
|
||||
@@ -278,13 +232,12 @@ Audio.propTypes = {
|
||||
};
|
||||
|
||||
function Video({
|
||||
name, link, thumbnail, thumbnailFile, thumbnailType,
|
||||
width, height, file, type, blurhash,
|
||||
name, link, thumbnail,
|
||||
width, height, file, type, thumbnailFile, thumbnailType,
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [url, setUrl] = useState(null);
|
||||
const [thumbUrl, setThumbUrl] = useState(null);
|
||||
const [blur, setBlur] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
@@ -299,16 +252,16 @@ function Video({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadVideo = async () => {
|
||||
async function loadVideo() {
|
||||
const myUrl = await getUrl(link, type, file);
|
||||
setUrl(myUrl);
|
||||
setIsLoading(false);
|
||||
};
|
||||
}
|
||||
|
||||
const handlePlayVideo = () => {
|
||||
function handlePlayVideo() {
|
||||
setIsLoading(true);
|
||||
loadVideo();
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-container">
|
||||
@@ -316,20 +269,14 @@ function Video({
|
||||
<div
|
||||
style={{
|
||||
height: width !== null ? getNativeHeight(width, height) : 'unset',
|
||||
backgroundImage: thumbUrl === null ? 'none' : `url(${thumbUrl}`,
|
||||
}}
|
||||
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 */
|
||||
{ url === null && isLoading && <Spinner size="small" /> }
|
||||
{ url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
|
||||
{ url !== null && (
|
||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||
<video autoPlay controls poster={thumbUrl}>
|
||||
<source src={url} type={getBlobSafeMimeType(type)} />
|
||||
</video>
|
||||
@@ -343,24 +290,22 @@ Video.defaultProps = {
|
||||
height: null,
|
||||
file: null,
|
||||
thumbnail: null,
|
||||
type: '',
|
||||
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,
|
||||
thumbnailFile: PropTypes.shape({}),
|
||||
thumbnailType: PropTypes.string,
|
||||
};
|
||||
|
||||
export {
|
||||
File, Image, Sticker, Audio, Video,
|
||||
File, Image, Audio, Video,
|
||||
};
|
||||
@@ -27,15 +27,6 @@
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
.sticker-container {
|
||||
display: inline-flex;
|
||||
max-width: 128px;
|
||||
width: 100%;
|
||||
& img {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-container,
|
||||
.video-container,
|
||||
.audio-container {
|
||||
@@ -51,40 +42,25 @@
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.image-container,
|
||||
.video-container {
|
||||
& img,
|
||||
& canvas {
|
||||
.image-container {
|
||||
& img {
|
||||
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%;
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
.audio-container {
|
||||
audio {
|
||||
width: 100%;
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import React, {
|
||||
import PropTypes from 'prop-types';
|
||||
import './Message.scss';
|
||||
|
||||
import { getShortcodeToCustomEmoji } from '../../organisms/emoji-board/custom-emoji';
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
@@ -24,7 +25,6 @@ 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';
|
||||
|
||||
@@ -37,7 +37,6 @@ 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';
|
||||
|
||||
function PlaceholderMessage() {
|
||||
return (
|
||||
@@ -69,7 +68,7 @@ const MessageAvatar = React.memo(({
|
||||
));
|
||||
|
||||
const MessageHeader = React.memo(({
|
||||
userId, username, timestamp, fullTime,
|
||||
userId, username, time,
|
||||
}) => (
|
||||
<div className="message__header">
|
||||
<Text
|
||||
@@ -83,20 +82,14 @@ const MessageHeader = React.memo(({
|
||||
<span>{twemojify(userId)}</span>
|
||||
</Text>
|
||||
<div className="message__time">
|
||||
<Text variant="b3">
|
||||
<Time timestamp={timestamp} fullTime={fullTime} />
|
||||
</Text>
|
||||
<Text variant="b3">{time}</Text>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
MessageHeader.defaultProps = {
|
||||
fullTime: false,
|
||||
};
|
||||
MessageHeader.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
timestamp: PropTypes.number.isRequired,
|
||||
fullTime: PropTypes.bool,
|
||||
time: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function MessageReply({ name, color, body }) {
|
||||
@@ -169,8 +162,8 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||
}, []);
|
||||
|
||||
const focusReply = (ev) => {
|
||||
if (!ev.key || ev.key === ' ' || ev.key === 'Enter') {
|
||||
if (ev.key) ev.preventDefault();
|
||||
if (!ev.keyCode || ev.keyCode === 32 || ev.keyCode === 13) {
|
||||
if (ev.keyCode) ev.preventDefault();
|
||||
if (reply?.event === null) return;
|
||||
if (reply?.event.isRedacted()) return;
|
||||
roomTimeline.loadEventTimeline(eventId);
|
||||
@@ -207,13 +200,7 @@ const MessageBody = React.memo(({
|
||||
let content = null;
|
||||
if (isCustomHTML) {
|
||||
try {
|
||||
content = twemojify(
|
||||
sanitizeCustomHtml(initMatrix.matrixClient, body),
|
||||
undefined,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
content = twemojify(sanitizeCustomHtml(body), undefined, true, false, true);
|
||||
} catch {
|
||||
console.error('Malformed custom html: ', body);
|
||||
content = twemojify(body, undefined);
|
||||
@@ -253,7 +240,7 @@ const MessageBody = React.memo(({
|
||||
|
||||
return (
|
||||
<div className="message__body">
|
||||
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
||||
<div className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
||||
{ msgType === 'm.emote' && (
|
||||
<>
|
||||
{'* '}
|
||||
@@ -290,12 +277,7 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
if (e.keyCode === 13 && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
onSave(editInputRef.current.value);
|
||||
}
|
||||
@@ -340,7 +322,7 @@ function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
|
||||
return rEvent;
|
||||
}
|
||||
|
||||
function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
|
||||
function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
|
||||
const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
|
||||
if (myAlreadyReactEvent) {
|
||||
const rId = myAlreadyReactEvent.getId();
|
||||
@@ -348,17 +330,17 @@ function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
|
||||
redactEvent(roomId, rId);
|
||||
return;
|
||||
}
|
||||
sendReaction(roomId, eventId, emojiKey, shortcode);
|
||||
sendReaction(roomId, eventId, emojiKey);
|
||||
}
|
||||
|
||||
function pickEmoji(e, roomId, eventId, roomTimeline) {
|
||||
openEmojiBoard(getEventCords(e), (emoji) => {
|
||||
toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
|
||||
toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
|
||||
e.target.click();
|
||||
});
|
||||
}
|
||||
|
||||
function genReactionMsg(userIds, reaction, shortcode) {
|
||||
function genReactionMsg(userIds, reaction) {
|
||||
return (
|
||||
<>
|
||||
{userIds.map((userId, index) => (
|
||||
@@ -372,22 +354,24 @@ function genReactionMsg(userIds, reaction, shortcode) {
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
||||
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
|
||||
{twemojify(reaction, { className: 'react-emoji' })}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageReaction({
|
||||
reaction, shortcode, count, users, isActive, onClick,
|
||||
shortcodeToEmoji, reaction, count, users, isActive, onClick,
|
||||
}) {
|
||||
const customEmojiMatch = reaction.match(/^:(\S+):$/);
|
||||
let customEmojiUrl = null;
|
||||
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
||||
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
|
||||
if (customEmojiMatch) {
|
||||
const customEmoji = shortcodeToEmoji.get(customEmojiMatch[1]);
|
||||
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(customEmoji?.mxc);
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
className="msg__reaction-tooltip"
|
||||
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction, shortcode) : 'Unable to load who has reacted'}</Text>}
|
||||
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction) : 'Unable to load who has reacted'}</Text>}
|
||||
>
|
||||
<button
|
||||
onClick={onClick}
|
||||
@@ -396,7 +380,7 @@ function MessageReaction({
|
||||
>
|
||||
{
|
||||
customEmojiUrl
|
||||
? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
|
||||
? <img className="react-emoji" draggable="false" alt={reaction} src={customEmojiUrl} />
|
||||
: twemojify(reaction, { className: 'react-emoji' })
|
||||
}
|
||||
<Text variant="b3" className="msg__reaction-count">{count}</Text>
|
||||
@@ -404,12 +388,9 @@ function MessageReaction({
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
MessageReaction.defaultProps = {
|
||||
shortcode: undefined,
|
||||
};
|
||||
MessageReaction.propTypes = {
|
||||
shortcodeToEmoji: PropTypes.shape({}).isRequired,
|
||||
reaction: PropTypes.node.isRequired,
|
||||
shortcode: PropTypes.string,
|
||||
count: PropTypes.number.isRequired,
|
||||
users: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
@@ -420,10 +401,11 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
const { roomId, room, reactionTimeline } = roomTimeline;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const reactions = {};
|
||||
const shortcodeToEmoji = getShortcodeToCustomEmoji(room);
|
||||
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
||||
|
||||
const eventReactions = reactionTimeline.get(mEvent.getId());
|
||||
const addReaction = (key, shortcode, count, senderId, isActive) => {
|
||||
const addReaction = (key, count, senderId, isActive) => {
|
||||
let reaction = reactions[key];
|
||||
if (reaction === undefined) {
|
||||
reaction = {
|
||||
@@ -432,7 +414,6 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
isActive: false,
|
||||
};
|
||||
}
|
||||
if (shortcode) reaction.shortcode = shortcode;
|
||||
if (count) {
|
||||
reaction.count = count;
|
||||
} else {
|
||||
@@ -448,10 +429,9 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
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);
|
||||
addReaction(reaction.key, undefined, senderId, isActive);
|
||||
});
|
||||
} else {
|
||||
// Use aggregated reactions
|
||||
@@ -459,7 +439,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
if (!aggregatedReaction) return null;
|
||||
aggregatedReaction.forEach((reaction) => {
|
||||
if (reaction.type !== 'm.reaction') return;
|
||||
addReaction(reaction.key, undefined, reaction.count, undefined, false);
|
||||
addReaction(reaction.key, reaction.count, undefined, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -469,13 +449,13 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
Object.keys(reactions).map((key) => (
|
||||
<MessageReaction
|
||||
key={key}
|
||||
shortcodeToEmoji={shortcodeToEmoji}
|
||||
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);
|
||||
toggleEmoji(roomId, mEvent.getId(), key, roomTimeline);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
@@ -627,14 +607,7 @@ function genMediaContent(mE) {
|
||||
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'];
|
||||
if (mE.getType() === 'm.sticker') msgType = 'm.image';
|
||||
|
||||
switch (msgType) {
|
||||
case 'm.file':
|
||||
@@ -655,18 +628,6 @@ function genMediaContent(mE) {
|
||||
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':
|
||||
@@ -693,7 +654,6 @@ function genMediaContent(mE) {
|
||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||
file={isEncryptedFile ? mContent.file : null}
|
||||
type={mContent.info?.mimetype}
|
||||
blurhash={blurhash}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -714,9 +674,9 @@ function getEditedBody(editedMEvent) {
|
||||
}
|
||||
|
||||
function Message({
|
||||
mEvent, isBodyOnly, roomTimeline,
|
||||
focus, fullTime, isEdit, setEdit, cancelEdit,
|
||||
mEvent, isBodyOnly, roomTimeline, focus, time,
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const roomId = mEvent.getRoomId();
|
||||
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
|
||||
|
||||
@@ -731,7 +691,7 @@ function Message({
|
||||
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
|
||||
|
||||
const edit = useCallback(() => {
|
||||
setEdit(eventId);
|
||||
setIsEditing(true);
|
||||
}, []);
|
||||
const reply = useCallback(() => {
|
||||
replyTo(senderId, mEvent.getId(), body);
|
||||
@@ -775,12 +735,7 @@ function Message({
|
||||
}
|
||||
<div className="message__main-container">
|
||||
{!isBodyOnly && (
|
||||
<MessageHeader
|
||||
userId={senderId}
|
||||
username={username}
|
||||
timestamp={mEvent.getTs()}
|
||||
fullTime={fullTime}
|
||||
/>
|
||||
<MessageHeader userId={senderId} username={username} time={time} />
|
||||
)}
|
||||
{roomTimeline && isReply && (
|
||||
<MessageReplyWrapper
|
||||
@@ -788,7 +743,7 @@ function Message({
|
||||
eventId={mEvent.replyEventId}
|
||||
/>
|
||||
)}
|
||||
{!isEdit && (
|
||||
{!isEditing && (
|
||||
<MessageBody
|
||||
senderName={username}
|
||||
isCustomHTML={isCustomHTML}
|
||||
@@ -797,22 +752,22 @@ function Message({
|
||||
isEdited={isEdited}
|
||||
/>
|
||||
)}
|
||||
{isEdit && (
|
||||
{isEditing && (
|
||||
<MessageEdit
|
||||
body={body}
|
||||
onSave={(newBody) => {
|
||||
if (newBody !== body) {
|
||||
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
||||
}
|
||||
cancelEdit();
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onCancel={cancelEdit}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
/>
|
||||
)}
|
||||
{haveReactions && (
|
||||
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
|
||||
)}
|
||||
{roomTimeline && !isEdit && (
|
||||
{roomTimeline && !isEditing && (
|
||||
<MessageOptions
|
||||
roomTimeline={roomTimeline}
|
||||
mEvent={mEvent}
|
||||
@@ -828,20 +783,13 @@ 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,
|
||||
time: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export { Message, MessageReply, PlaceholderMessage };
|
||||
@@ -15,7 +15,7 @@
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__avatar-container {
|
||||
padding-top: 6px;
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
@@ -101,6 +101,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.message__header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -114,16 +115,10 @@
|
||||
@extend .cp-txt__ellipsis;
|
||||
color: inherit;
|
||||
}
|
||||
& > span:last-child {
|
||||
display: none;
|
||||
}
|
||||
& > span:last-child { display: none; }
|
||||
&:hover {
|
||||
& > span:first-child {
|
||||
display: none;
|
||||
}
|
||||
& > span:last-child {
|
||||
display: block;
|
||||
}
|
||||
& > span:first-child { display: none; }
|
||||
& > span:last-child { display: block; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,11 +168,7 @@
|
||||
& > .text > a {
|
||||
white-space: initial !important;
|
||||
}
|
||||
|
||||
& p:not(:last-child) {
|
||||
margin-bottom: var(--sp-normal);
|
||||
}
|
||||
|
||||
|
||||
& span[data-mx-pill] {
|
||||
background-color: hsla(0, 0%, 64%, 0.15);
|
||||
padding: 0 2px;
|
||||
@@ -200,7 +191,7 @@
|
||||
& span[data-mx-spoiler] {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(124, 124, 124, 0.5);
|
||||
color: transparent;
|
||||
color:transparent;
|
||||
cursor: pointer;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -229,8 +220,6 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -261,14 +250,15 @@
|
||||
cursor: pointer;
|
||||
|
||||
& .react-emoji {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 2px;
|
||||
}
|
||||
&-count {
|
||||
margin: 0 var(--sp-ultra-tight);
|
||||
color: var(--tc-surface-normal);
|
||||
color: var(--tc-surface-normal)
|
||||
}
|
||||
&-tooltip .react-emoji {
|
||||
&-tooltip .react-emoji {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 var(--sp-ultra-tight);
|
||||
@@ -281,19 +271,19 @@
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--bg-surface-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);
|
||||
background-color: var(--bg-caution-active)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,12 +304,7 @@
|
||||
|
||||
// markdown formating
|
||||
.message__body {
|
||||
& h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
& h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
margin-bottom: var(--sp-ultra-tight);
|
||||
font-weight: var(--fw-medium);
|
||||
@@ -440,8 +425,7 @@
|
||||
@include scrollbar.scroll__h;
|
||||
@include scrollbar.scroll--auto-hide;
|
||||
|
||||
& td,
|
||||
& th {
|
||||
& td, & th {
|
||||
padding: var(--sp-extra-tight);
|
||||
border: 1px solid var(--bg-surface-border);
|
||||
border-width: 0 1px 1px 0;
|
||||
@@ -449,11 +433,11 @@
|
||||
&:last-child {
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
[dir='rtl'] & {
|
||||
[dir=rtl] & {
|
||||
border-width: 0 1px 1px 0;
|
||||
}
|
||||
}
|
||||
[dir='rtl'] &:first-child {
|
||||
[dir=rtl] &:first-child {
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -13,7 +12,7 @@ import InviteCancelArraowIC from '../../../../public/res/ic/outlined/invite-canc
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
|
||||
function TimelineChange({
|
||||
variant, content, timestamp, onClick,
|
||||
variant, content, time, onClick,
|
||||
}) {
|
||||
let iconSrc;
|
||||
|
||||
@@ -49,9 +48,7 @@ function TimelineChange({
|
||||
</Text>
|
||||
</div>
|
||||
<div className="timeline-change__time">
|
||||
<Text variant="b3">
|
||||
<Time timestamp={timestamp} />
|
||||
</Text>
|
||||
<Text variant="b3">{time}</Text>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -71,7 +68,7 @@ TimelineChange.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
timestamp: PropTypes.number.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { useReducer, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomEmojis.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { suffixRename } from '../../../util/common';
|
||||
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import ImagePack from '../image-pack/ImagePack';
|
||||
|
||||
function useRoomPacks(room) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
|
||||
const packEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
|
||||
const unUsablePacks = [];
|
||||
const usablePacks = packEvents.filter((mEvent) => {
|
||||
if (typeof mEvent.getContent()?.images !== 'object') {
|
||||
unUsablePacks.push(mEvent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleEvent = (event, state, prevEvent) => {
|
||||
if (event.getRoomId() !== room.roomId) return;
|
||||
if (event.getType() !== 'im.ponies.room_emotes') return;
|
||||
if (!prevEvent?.getContent()?.images || !event.getContent().images) {
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
mx.on('RoomState.events', handleEvent);
|
||||
return () => {
|
||||
mx.removeListener('RoomState.events', handleEvent);
|
||||
};
|
||||
}, [room, mx]);
|
||||
|
||||
const isStateKeyAvailable = (key) => !room.currentState.getStateEvents('im.ponies.room_emotes', key);
|
||||
|
||||
const createPack = async (name) => {
|
||||
const packContent = {
|
||||
pack: { display_name: name },
|
||||
images: {},
|
||||
};
|
||||
let stateKey = '';
|
||||
if (unUsablePacks.length > 0) {
|
||||
const mEvent = unUsablePacks[0];
|
||||
stateKey = mEvent.getStateKey();
|
||||
} else {
|
||||
stateKey = packContent.pack.display_name.replace(/\s/g, '-');
|
||||
if (!isStateKeyAvailable(stateKey)) {
|
||||
stateKey = suffixRename(
|
||||
stateKey,
|
||||
isStateKeyAvailable,
|
||||
);
|
||||
}
|
||||
}
|
||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', packContent, stateKey);
|
||||
};
|
||||
|
||||
const deletePack = async (stateKey) => {
|
||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', {}, stateKey);
|
||||
};
|
||||
|
||||
return {
|
||||
usablePacks,
|
||||
createPack,
|
||||
deletePack,
|
||||
};
|
||||
}
|
||||
|
||||
function RoomEmojis({ roomId }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const { usablePacks, createPack, deletePack } = useRoomPacks(room);
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
|
||||
const handlePackCreate = (e) => {
|
||||
e.preventDefault();
|
||||
const { nameInput } = e.target;
|
||||
const name = nameInput.value.trim();
|
||||
if (name === '') return;
|
||||
nameInput.value = '';
|
||||
|
||||
createPack(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="room-emojis">
|
||||
{ canChange && (
|
||||
<div className="room-emojis__add-pack">
|
||||
<MenuHeader>Create Pack</MenuHeader>
|
||||
<form onSubmit={handlePackCreate}>
|
||||
<Input name="nameInput" placeholder="Pack Name" required />
|
||||
<Button variant="primary" type="submit">Create pack</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
usablePacks.length > 0
|
||||
? usablePacks.reverse().map((mEvent) => (
|
||||
<ImagePack
|
||||
key={mEvent.getId()}
|
||||
roomId={roomId}
|
||||
stateKey={mEvent.getStateKey()}
|
||||
handlePackDelete={canChange ? deletePack : undefined}
|
||||
/>
|
||||
)) : (
|
||||
<div className="room-emojis__empty">
|
||||
<Text>No emoji or sticker pack.</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomEmojis.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomEmojis;
|
||||
@@ -1,29 +0,0 @@
|
||||
.room-emojis {
|
||||
.image-pack,
|
||||
.room-emojis__add-pack,
|
||||
.room-emojis__empty {
|
||||
margin: var(--sp-normal) 0;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
overflow: hidden;
|
||||
|
||||
& > .context-menu__header:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
&__add-pack {
|
||||
& form {
|
||||
margin: var(--sp-normal);
|
||||
display: flex;
|
||||
gap: var(--sp-normal);
|
||||
& .input-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__empty {
|
||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -237,12 +237,12 @@ function RoomPermissions({ roomId }) {
|
||||
? permissions[permInfo.parent]?.[permKey]
|
||||
: permissions[permKey];
|
||||
|
||||
if (permValue === undefined) permValue = permInfo.default;
|
||||
if (!permValue) permValue = permInfo.default;
|
||||
|
||||
if (typeof permValue === 'number') {
|
||||
powerLevel = permValue;
|
||||
} else if (permKey === 'notifications') {
|
||||
powerLevel = permValue.room ?? 50;
|
||||
powerLevel = permValue.room || 50;
|
||||
}
|
||||
return (
|
||||
<SettingTile
|
||||
|
||||
@@ -132,7 +132,7 @@ function RoomProfile({ roomId }) {
|
||||
|
||||
const renderEditNameAndTopic = () => (
|
||||
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
||||
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" />}
|
||||
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" required />}
|
||||
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
|
||||
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
|
||||
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomSearch.scss';
|
||||
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { selectRoom } from '../../../client/action/navigation';
|
||||
@@ -118,13 +120,14 @@ function RoomSearch({ roomId }) {
|
||||
const renderTimeline = (timeline) => (
|
||||
<div className="room-search__result-item" key={timeline[0].getId()}>
|
||||
{ timeline.map((mEvent) => {
|
||||
const time = dateFormat(mEvent.getDate(), 'dd/mm/yyyy - hh:MM TT');
|
||||
const id = mEvent.getId();
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<Message
|
||||
mEvent={mEvent}
|
||||
isBodyOnly={false}
|
||||
fullTime
|
||||
time={time}
|
||||
/>
|
||||
<Button onClick={() => selectRoom(roomId, id)}>View</Button>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openSpaceSettings, openSpaceManage, openInviteUser } from '../../../client/action/navigation';
|
||||
import { markAsRead } from '../../../client/action/notifications';
|
||||
import { leave } from '../../../client/action/room';
|
||||
import {
|
||||
createSpaceShortcut,
|
||||
@@ -18,7 +17,6 @@ import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import CategoryIC from '../../../../public/res/ic/outlined/category.svg';
|
||||
import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
|
||||
@@ -30,21 +28,11 @@ import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
|
||||
function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const { roomList } = initMatrix;
|
||||
const room = mx.getRoom(roomId);
|
||||
const canInvite = room?.canInvite(mx.getUserId());
|
||||
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
|
||||
const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId);
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
const spaceChildren = roomList.getCategorizedSpaces([roomId]);
|
||||
spaceChildren?.forEach((childIds) => {
|
||||
childIds?.forEach((childId) => {
|
||||
markAsRead(childId);
|
||||
});
|
||||
});
|
||||
afterOptionSelect();
|
||||
};
|
||||
const handleInviteClick = () => {
|
||||
openInviteUser(roomId);
|
||||
afterOptionSelect();
|
||||
@@ -83,7 +71,6 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||
return (
|
||||
<div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
|
||||
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
||||
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleCategorizeClick}
|
||||
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
|
||||
|
||||
@@ -70,7 +70,7 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
||||
unicode={`:${emoji.shortcode}:`}
|
||||
shortcodes={emoji.shortcode}
|
||||
src={initMatrix.matrixClient.mxcUrlToHttp(emoji.mxc)}
|
||||
data-mx-emoticon={emoji.mxc}
|
||||
data-mx-emoticon
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -141,13 +141,10 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
function getEmojiDataFromTarget(target) {
|
||||
const unicode = target.getAttribute('unicode');
|
||||
const hexcode = target.getAttribute('hexcode');
|
||||
const mxc = target.getAttribute('data-mx-emoticon');
|
||||
let shortcodes = target.getAttribute('shortcodes');
|
||||
if (typeof shortcodes === 'undefined') shortcodes = undefined;
|
||||
else shortcodes = shortcodes.split(',');
|
||||
return {
|
||||
unicode, hexcode, shortcodes, mxc,
|
||||
};
|
||||
return { unicode, hexcode, shortcodes };
|
||||
}
|
||||
|
||||
function selectEmoji(e) {
|
||||
@@ -205,23 +202,21 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
setAvailableEmojis([]);
|
||||
return;
|
||||
}
|
||||
// Retrieve the packs for the new room
|
||||
// Remove packs that aren't marked as emoji packs
|
||||
// Remove packs without emojis
|
||||
const packs = getRelevantPacks(
|
||||
initMatrix.matrixClient.getRoom(selectedRoomId),
|
||||
)
|
||||
.filter((pack) => pack.usage.indexOf('emoticon') !== -1)
|
||||
.filter((pack) => pack.getEmojis().length !== 0);
|
||||
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(selectedRoomId);
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
if (room) {
|
||||
const packs = getRelevantPacks(
|
||||
room.client,
|
||||
[room, ...parentRooms],
|
||||
).filter((pack) => pack.getEmojis().length !== 0);
|
||||
|
||||
// Set an index for each pack so that we know where to jump when the user uses the nav
|
||||
for (let i = 0; i < packs.length; i += 1) {
|
||||
packs[i].packIndex = i;
|
||||
}
|
||||
setAvailableEmojis(packs);
|
||||
// Set an index for each pack so that we know where to jump when the user uses the nav
|
||||
for (let i = 0; i < packs.length; i += 1) {
|
||||
packs[i].packIndex = i;
|
||||
}
|
||||
|
||||
setAvailableEmojis(packs);
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
@@ -252,6 +247,39 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
|
||||
return (
|
||||
<div id="emoji-board" className="emoji-board">
|
||||
<div className="emoji-board__content">
|
||||
<div className="emoji-board__content__search">
|
||||
<RawIcon size="small" src={SearchIC} />
|
||||
<Input onChange={handleSearchChange} forwardRef={searchRef} placeholder="Search" />
|
||||
</div>
|
||||
<div className="emoji-board__content__emojis">
|
||||
<ScrollView ref={scrollEmojisRef} autoHide>
|
||||
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
||||
<SearchedEmoji />
|
||||
{recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
|
||||
{
|
||||
availableEmojis.map((pack) => (
|
||||
<EmojiGroup
|
||||
name={pack.displayName}
|
||||
key={pack.packIndex}
|
||||
groupEmojis={pack.getEmojis()}
|
||||
className="custom-emoji-group"
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
emojiGroups.map((group) => (
|
||||
<EmojiGroup key={group.name} name={group.name} groupEmojis={group.emojis} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div ref={emojiInfo} className="emoji-board__content__info">
|
||||
<div>{ parse(twemoji.parse('🙂')) }</div>
|
||||
<Text>:slight_smile:</Text>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollView invisible>
|
||||
<div className="emoji-board__nav">
|
||||
{recentEmojis.length > 0 && (
|
||||
@@ -259,21 +287,20 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
onClick={() => openGroup(0)}
|
||||
src={RecentClockIC}
|
||||
tooltip="Recent"
|
||||
tooltipPlacement="left"
|
||||
tooltipPlacement="right"
|
||||
/>
|
||||
)}
|
||||
<div className="emoji-board__nav-custom">
|
||||
{
|
||||
availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient
|
||||
.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc);
|
||||
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
src={src}
|
||||
key={pack.packIndex}
|
||||
tooltip={pack.displayName ?? 'Unknown'}
|
||||
tooltipPlacement="left"
|
||||
tooltip={pack.displayName}
|
||||
tooltipPlacement="right"
|
||||
isImage
|
||||
/>
|
||||
);
|
||||
@@ -297,46 +324,13 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
key={indx}
|
||||
src={ico}
|
||||
tooltip={name}
|
||||
tooltipPlacement="left"
|
||||
tooltipPlacement="right"
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
<div className="emoji-board__content">
|
||||
<div className="emoji-board__content__search">
|
||||
<RawIcon size="small" src={SearchIC} />
|
||||
<Input onChange={handleSearchChange} forwardRef={searchRef} placeholder="Search" />
|
||||
</div>
|
||||
<div className="emoji-board__content__emojis">
|
||||
<ScrollView ref={scrollEmojisRef} autoHide>
|
||||
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
||||
<SearchedEmoji />
|
||||
{recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
|
||||
{
|
||||
availableEmojis.map((pack) => (
|
||||
<EmojiGroup
|
||||
name={pack.displayName ?? 'Unknown'}
|
||||
key={pack.packIndex}
|
||||
groupEmojis={pack.getEmojis()}
|
||||
className="custom-emoji-group"
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
emojiGroups.map((group) => (
|
||||
<EmojiGroup key={group.name} name={group.name} groupEmojis={group.emojis} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div ref={emojiInfo} className="emoji-board__content__info">
|
||||
<div>{ parse(twemoji.parse('🙂')) }</div>
|
||||
<Text>:slight_smile:</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
|
||||
min-height: 100%;
|
||||
padding: 4px 6px;
|
||||
@include dir.side(border, none, 1px solid var(--bg-surface-border));
|
||||
background-color: var(--bg-surface-low);
|
||||
@include dir.side(border, 1px solid var(--bg-surface-border), none);
|
||||
|
||||
position: relative;
|
||||
|
||||
@@ -83,7 +84,6 @@
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
& > p:last-child {
|
||||
@@ -121,12 +121,8 @@
|
||||
@include dir.side(margin, var(--left-margin), var(--right-margin));
|
||||
}
|
||||
& .emoji {
|
||||
max-width: 38px;
|
||||
max-height: 38px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
object-fit: contain;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: var(--emoji-padding);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
|
||||
@@ -1,224 +1,135 @@
|
||||
import { emojis } from './emoji';
|
||||
|
||||
// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
|
||||
// Custom emoji are stored in one of three places:
|
||||
// - User emojis, which are stored in account data
|
||||
// - Room emojis, which are stored in state events in a room
|
||||
// - Emoji packs, which are rooms of emojis referenced in the account data or in a room's
|
||||
// cannonical space
|
||||
//
|
||||
// Emojis and packs referenced from within a user's account data should be available
|
||||
// globally, while emojis and packs in rooms and spaces should only be available within
|
||||
// those spaces and rooms
|
||||
|
||||
class ImagePack {
|
||||
static parsePack(eventId, packContent) {
|
||||
if (!eventId || typeof packContent?.images !== 'object') {
|
||||
// Convert a raw image pack into a more maliable format
|
||||
//
|
||||
// Takes an image pack as per MSC 2545 (e.g. as in the Matrix spec), and converts it to a
|
||||
// format used here, while filling in defaults.
|
||||
//
|
||||
// The room argument is the room the pack exists in, which is used as a fallback for
|
||||
// missing properties
|
||||
//
|
||||
// Returns `null` if the rawPack is not a properly formatted image pack, although there
|
||||
// is still a fair amount of tolerance for malformed packs.
|
||||
static parsePack(rawPack, room) {
|
||||
if (typeof rawPack.images === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImagePack(eventId, packContent);
|
||||
}
|
||||
const pack = rawPack.pack ?? {};
|
||||
|
||||
constructor(eventId, content) {
|
||||
this.id = eventId;
|
||||
this.content = JSON.parse(JSON.stringify(content));
|
||||
|
||||
this.applyPack(content);
|
||||
this.applyImages(content);
|
||||
}
|
||||
|
||||
applyPack(content) {
|
||||
const pack = content.pack ?? {};
|
||||
|
||||
this.displayName = pack.display_name;
|
||||
this.avatarUrl = pack.avatar_url;
|
||||
this.usage = pack.usage ?? ['emoticon', 'sticker'];
|
||||
this.attribution = pack.attribution;
|
||||
}
|
||||
|
||||
applyImages(content) {
|
||||
this.images = new Map();
|
||||
this.emoticons = [];
|
||||
this.stickers = [];
|
||||
|
||||
Object.entries(content.images).forEach(([shortcode, data]) => {
|
||||
const displayName = pack.display_name ?? (room ? room.name : undefined);
|
||||
const avatar = pack.avatar_url ?? (room ? room.getMxcAvatarUrl() : undefined);
|
||||
const usage = pack.usage ?? ['emoticon', 'sticker'];
|
||||
const { attribution } = pack;
|
||||
const images = Object.entries(rawPack.images).flatMap((e) => {
|
||||
const data = e[1];
|
||||
const shortcode = e[0];
|
||||
const mxc = data.url;
|
||||
const body = data.body ?? shortcode;
|
||||
const usage = data.usage ?? this.usage;
|
||||
const { info } = data;
|
||||
const usage_ = data.usage ?? usage;
|
||||
|
||||
if (!mxc) return;
|
||||
const image = {
|
||||
shortcode, mxc, body, usage, info,
|
||||
};
|
||||
|
||||
this.images.set(shortcode, image);
|
||||
if (usage.includes('emoticon')) {
|
||||
this.emoticons.push(image);
|
||||
}
|
||||
if (usage.includes('sticker')) {
|
||||
this.stickers.push(image);
|
||||
if (mxc) {
|
||||
return [{
|
||||
shortcode, mxc, body, info, usage: usage_,
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return new ImagePack(displayName, avatar, usage, attribution, images);
|
||||
}
|
||||
|
||||
getImages() {
|
||||
return this.images;
|
||||
constructor(displayName, avatar, usage, attribution, images) {
|
||||
this.displayName = displayName;
|
||||
this.avatar = avatar;
|
||||
this.usage = usage;
|
||||
this.attribution = attribution;
|
||||
this.images = images;
|
||||
}
|
||||
|
||||
// Produce a list of emoji in this image pack
|
||||
getEmojis() {
|
||||
return this.emoticons;
|
||||
return this.images.filter((i) => i.usage.indexOf('emoticon') !== -1);
|
||||
}
|
||||
|
||||
// Produce a list of stickers in this image pack
|
||||
getStickers() {
|
||||
return this.stickers;
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
_updatePackProperty(property, value) {
|
||||
if (this.content.pack === undefined) {
|
||||
this.content.pack = {};
|
||||
}
|
||||
this.content.pack[property] = value;
|
||||
this.applyPack(this.content);
|
||||
}
|
||||
|
||||
setAvatarUrl(avatarUrl) {
|
||||
this._updatePackProperty('avatar_url', avatarUrl);
|
||||
}
|
||||
|
||||
setDisplayName(displayName) {
|
||||
this._updatePackProperty('display_name', displayName);
|
||||
}
|
||||
|
||||
setAttribution(attribution) {
|
||||
this._updatePackProperty('attribution', attribution);
|
||||
}
|
||||
|
||||
setUsage(usage) {
|
||||
this._updatePackProperty('usage', usage);
|
||||
}
|
||||
|
||||
addImage(key, imgContent) {
|
||||
this.content.images = {
|
||||
[key]: imgContent,
|
||||
...this.content.images,
|
||||
};
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
removeImage(key) {
|
||||
if (this.content.images[key] === undefined) return;
|
||||
delete this.content.images[key];
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
updateImageKey(key, newKey) {
|
||||
if (this.content.images[key] === undefined) return;
|
||||
const copyImages = {};
|
||||
Object.keys(this.content.images).forEach((imgKey) => {
|
||||
copyImages[imgKey === key ? newKey : imgKey] = this.content.images[imgKey];
|
||||
});
|
||||
this.content.images = copyImages;
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
_updateImageProperty(key, property, value) {
|
||||
if (this.content.images[key] === undefined) return;
|
||||
this.content.images[key][property] = value;
|
||||
this.applyImages(this.content);
|
||||
}
|
||||
|
||||
setImageUrl(key, url) {
|
||||
this._updateImageProperty(key, 'url', url);
|
||||
}
|
||||
|
||||
setImageBody(key, body) {
|
||||
this._updateImageProperty(key, 'body', body);
|
||||
}
|
||||
|
||||
setImageInfo(key, info) {
|
||||
this._updateImageProperty(key, 'info', info);
|
||||
}
|
||||
|
||||
setImageUsage(key, usage) {
|
||||
this._updateImageProperty(key, 'usage', usage);
|
||||
return this.images.filter((i) => i.usage.indexOf('sticker') !== -1);
|
||||
}
|
||||
}
|
||||
|
||||
function getGlobalImagePacks(mx) {
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||
if (typeof globalContent !== 'object') return [];
|
||||
|
||||
const { rooms } = globalContent;
|
||||
if (typeof rooms !== 'object') return [];
|
||||
|
||||
const roomIds = Object.keys(rooms);
|
||||
|
||||
const packs = roomIds.flatMap((roomId) => {
|
||||
if (typeof rooms[roomId] !== 'object') return [];
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return [];
|
||||
const stateKeys = Object.keys(rooms[roomId]);
|
||||
|
||||
return stateKeys.map((stateKey) => {
|
||||
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = ImagePack.parsePack(data?.getId(), data?.getContent());
|
||||
if (pack) {
|
||||
pack.displayName ??= room.name;
|
||||
pack.avatarUrl ??= room.getMxcAvatarUrl();
|
||||
}
|
||||
return pack;
|
||||
}).filter((pack) => pack !== null);
|
||||
});
|
||||
|
||||
return packs;
|
||||
}
|
||||
|
||||
// Retrieve a list of user emojis
|
||||
//
|
||||
// Result is an ImagePack, or null if the user hasn't set up or has deleted their personal
|
||||
// image pack.
|
||||
//
|
||||
// Accepts a reference to a matrix client as the only argument
|
||||
function getUserImagePack(mx) {
|
||||
const accountDataEmoji = mx.getAccountData('im.ponies.user_emotes');
|
||||
if (!accountDataEmoji) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userImagePack = ImagePack.parsePack(mx.getUserId(), accountDataEmoji.event.content);
|
||||
if (userImagePack) userImagePack.displayName ??= 'Personal Emoji';
|
||||
const userImagePack = ImagePack.parsePack(accountDataEmoji.event.content);
|
||||
if (userImagePack) userImagePack.displayName ??= 'Your Emoji';
|
||||
return userImagePack;
|
||||
}
|
||||
|
||||
function getRoomImagePacks(room) {
|
||||
const dataEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
|
||||
// Produces a list of all of the emoji packs in a room
|
||||
//
|
||||
// Returns a list of `ImagePack`s. This does not include packs in spaces that contain
|
||||
// this room.
|
||||
function getPacksInRoom(room) {
|
||||
const packs = room.currentState.getStateEvents('im.ponies.room_emotes');
|
||||
|
||||
return dataEvents
|
||||
.map((data) => {
|
||||
const pack = ImagePack.parsePack(data?.getId(), data?.getContent());
|
||||
if (pack) {
|
||||
pack.displayName ??= room.name;
|
||||
pack.avatarUrl ??= room.getMxcAvatarUrl();
|
||||
}
|
||||
return pack;
|
||||
})
|
||||
.filter((pack) => pack !== null);
|
||||
return packs
|
||||
.map((p) => ImagePack.parsePack(p.event.content, room))
|
||||
.filter((p) => p !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatrixClient} mx Provide if you want to include user personal/global pack
|
||||
* @param {Room[]} rooms Provide rooms if you want to include rooms pack
|
||||
* @returns {ImagePack[]} packs
|
||||
*/
|
||||
function getRelevantPacks(mx, rooms) {
|
||||
const userPack = mx ? getUserImagePack(mx) : [];
|
||||
const globalPacks = mx ? getGlobalImagePacks(mx) : [];
|
||||
const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
|
||||
const roomsPack = rooms?.flatMap(getRoomImagePacks) ?? [];
|
||||
|
||||
// Produce a list of all image packs which should be shown for a given room
|
||||
//
|
||||
// This includes packs in that room, the user's personal images, and will eventually
|
||||
// include the user's enabled global image packs and space-level packs.
|
||||
//
|
||||
// This differs from getPacksInRoom, as the former only returns packs that are directly in
|
||||
// a room, whereas this function returns all packs which should be shown to the user while
|
||||
// they are in this room.
|
||||
//
|
||||
// Packs will be returned in the order that shortcode conflicts should be resolved, with
|
||||
// higher priority packs coming first.
|
||||
function getRelevantPacks(room) {
|
||||
return [].concat(
|
||||
userPack ?? [],
|
||||
globalPacks,
|
||||
roomsPack.filter((pack) => !globalPackIds.has(pack.id)),
|
||||
getUserImagePack(room.client) ?? [],
|
||||
getPacksInRoom(room),
|
||||
);
|
||||
}
|
||||
|
||||
function getShortcodeToEmoji(mx, rooms) {
|
||||
// Returns all user+room emojis and all standard unicode emojis
|
||||
//
|
||||
// Accepts a reference to a matrix client as the only argument
|
||||
//
|
||||
// Result is a map from shortcode to the corresponding emoji. If two emoji share a
|
||||
// shortcode, only one will be presented, with priority given to custom emoji.
|
||||
//
|
||||
// Will eventually be expanded to include all emojis revelant to a room and the user
|
||||
function getShortcodeToEmoji(room) {
|
||||
const allEmoji = new Map();
|
||||
|
||||
emojis.forEach((emoji) => {
|
||||
if (Array.isArray(emoji.shortcodes)) {
|
||||
if (emoji.shortcodes.constructor.name === 'Array') {
|
||||
emoji.shortcodes.forEach((shortcode) => {
|
||||
allEmoji.set(shortcode, emoji);
|
||||
});
|
||||
@@ -227,7 +138,7 @@ function getShortcodeToEmoji(mx, rooms) {
|
||||
}
|
||||
});
|
||||
|
||||
getRelevantPacks(mx, rooms)
|
||||
getRelevantPacks(room).reverse()
|
||||
.flatMap((pack) => pack.getEmojis())
|
||||
.forEach((emoji) => {
|
||||
allEmoji.set(emoji.shortcode, emoji);
|
||||
@@ -239,7 +150,7 @@ function getShortcodeToEmoji(mx, rooms) {
|
||||
function getShortcodeToCustomEmoji(room) {
|
||||
const allEmoji = new Map();
|
||||
|
||||
getRelevantPacks(room.client, [room])
|
||||
getRelevantPacks(room).reverse()
|
||||
.flatMap((pack) => pack.getEmojis())
|
||||
.forEach((emoji) => {
|
||||
allEmoji.set(emoji.shortcode, emoji);
|
||||
@@ -248,20 +159,27 @@ function getShortcodeToCustomEmoji(room) {
|
||||
return allEmoji;
|
||||
}
|
||||
|
||||
function getEmojiForCompletion(mx, rooms) {
|
||||
// Produces a special list of emoji specifically for auto-completion
|
||||
//
|
||||
// This list contains each emoji once, with all emoji being deduplicated by shortcode.
|
||||
// However, the order of the standard emoji will have been preserved, and alternate
|
||||
// shortcodes for the standard emoji will not be considered.
|
||||
//
|
||||
// Standard emoji are guaranteed to be earlier in the list than custom emoji
|
||||
function getEmojiForCompletion(room) {
|
||||
const allEmoji = new Map();
|
||||
getRelevantPacks(mx, rooms)
|
||||
getRelevantPacks(room).reverse()
|
||||
.flatMap((pack) => pack.getEmojis())
|
||||
.forEach((emoji) => {
|
||||
allEmoji.set(emoji.shortcode, emoji);
|
||||
});
|
||||
|
||||
return Array.from(allEmoji.values()).concat(emojis.filter((e) => !allEmoji.has(e.shortcode)));
|
||||
return emojis.filter((e) => !allEmoji.has(e.shortcode))
|
||||
.concat(Array.from(allEmoji.values()));
|
||||
}
|
||||
|
||||
export {
|
||||
ImagePack,
|
||||
getUserImagePack, getGlobalImagePacks, getRoomImagePacks,
|
||||
getUserImagePack,
|
||||
getShortcodeToEmoji, getShortcodeToCustomEmoji,
|
||||
getRelevantPacks, getEmojiForCompletion,
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { join } from '../../../client/action/room';
|
||||
import { selectRoom, selectTab } from '../../../client/action/navigation';
|
||||
import { selectRoom, selectSpace } from '../../../client/action/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
@@ -32,7 +32,7 @@ function JoinAliasContent({ term, requestClose }) {
|
||||
const openRoom = (roomId) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return;
|
||||
if (room.isSpaceRoom()) selectTab(roomId);
|
||||
if (room.isSpaceRoom()) selectSpace(roomId);
|
||||
else selectRoom(roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { selectTab, selectSpace } from '../../../client/action/navigation';
|
||||
import { selectSpace } from '../../../client/action/navigation';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { abbreviateNumber } from '../../../util/common';
|
||||
|
||||
@@ -107,10 +107,7 @@ function DrawerBreadcrumb({ spaceId }) {
|
||||
{ index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
||||
<Button
|
||||
className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
|
||||
onClick={() => {
|
||||
if (id === cons.tabs.HOME) selectTab(id);
|
||||
else selectSpace(id);
|
||||
}}
|
||||
onClick={() => selectSpace(id)}
|
||||
>
|
||||
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
|
||||
{ noti !== null && (
|
||||
|
||||
@@ -30,7 +30,7 @@ function Home({ spaceId }) {
|
||||
roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId));
|
||||
directIds = spaceChildIds.filter((roomId) => directs.has(roomId));
|
||||
} else {
|
||||
spaceIds = roomList.getOrphanSpaces().filter((id) => !accountData.spaceShortcut.has(id));
|
||||
spaceIds = roomList.getOrphanSpaces();
|
||||
roomIds = roomList.getOrphanRooms();
|
||||
}
|
||||
|
||||
@@ -80,10 +80,10 @@ function Home({ spaceId }) {
|
||||
<RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
|
||||
)}
|
||||
|
||||
{ isCategorized && [...categories.keys()].sort(roomIdByAtoZ).map((catId) => {
|
||||
{ isCategorized && [...categories].map(([catId, childIds]) => {
|
||||
const rms = [];
|
||||
const dms = [];
|
||||
categories.get(catId).forEach((id) => {
|
||||
childIds.forEach((id) => {
|
||||
if (directs.has(id)) dms.push(id);
|
||||
else rms.push(id);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { selectRoom, openReusableContextMenu } from '../../../client/action/navi
|
||||
import * as roomActions from '../../../client/action/room';
|
||||
|
||||
import {
|
||||
getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices,
|
||||
getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices
|
||||
} from '../../../util/matrixUtil';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
@@ -209,18 +209,19 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||
};
|
||||
|
||||
const toggleIgnore = async () => {
|
||||
const isIgnored = mx.getIgnoredUsers().includes(userId);
|
||||
const ignoredUsers = mx.getIgnoredUsers();
|
||||
const uIndex = ignoredUsers.indexOf(userId);
|
||||
if (uIndex >= 0) {
|
||||
if (uIndex === -1) return;
|
||||
ignoredUsers.splice(uIndex, 1);
|
||||
} else ignoredUsers.push(userId);
|
||||
|
||||
try {
|
||||
setIsIgnoring(true);
|
||||
if (isIgnored) {
|
||||
await roomActions.unignore([userId]);
|
||||
} else {
|
||||
await roomActions.ignore([userId]);
|
||||
}
|
||||
await mx.setIgnoredUsers(ignoredUsers);
|
||||
|
||||
if (isMountedRef.current === false) return;
|
||||
setIsUserIgnored(!isIgnored);
|
||||
setIsUserIgnored(uIndex < 0);
|
||||
setIsIgnoring(false);
|
||||
} catch {
|
||||
setIsIgnoring(false);
|
||||
|
||||
@@ -25,11 +25,9 @@ import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomH
|
||||
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||
@@ -44,7 +42,6 @@ const tabText = {
|
||||
GENERAL: 'General',
|
||||
SEARCH: 'Search',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
SECURITY: 'Security',
|
||||
};
|
||||
@@ -61,10 +58,6 @@ const tabItems = [{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
@@ -204,7 +197,6 @@ function RoomSettings({ roomId }) {
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
|
||||
</div>
|
||||
@@ -218,5 +210,7 @@ RoomSettings.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomSettings;
|
||||
export { tabText };
|
||||
export {
|
||||
RoomSettings as default,
|
||||
tabText,
|
||||
};
|
||||
@@ -8,14 +8,51 @@ import twemoji from 'twemoji';
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { toggleMarkdown } from '../../../client/action/settings';
|
||||
import * as roomActions from '../../../client/action/room';
|
||||
import {
|
||||
openCreateRoom,
|
||||
openPublicRooms,
|
||||
openInviteUser,
|
||||
} from '../../../client/action/navigation';
|
||||
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
|
||||
import AsyncSearch from '../../../util/AsyncSearch';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
|
||||
import { addRecentEmoji, getRecentEmojis } from '../emoji-board/recent';
|
||||
import commands from './commands';
|
||||
import { addRecentEmoji } from '../emoji-board/recent';
|
||||
|
||||
const commands = [{
|
||||
name: 'markdown',
|
||||
description: 'Toggle markdown for messages.',
|
||||
exe: () => toggleMarkdown(),
|
||||
}, {
|
||||
name: 'startDM',
|
||||
isOptions: true,
|
||||
description: 'Start direct message with user. Example: /startDM/@johndoe.matrix.org',
|
||||
exe: (roomId, searchTerm) => openInviteUser(undefined, searchTerm),
|
||||
}, {
|
||||
name: 'createRoom',
|
||||
description: 'Create new room',
|
||||
exe: () => openCreateRoom(),
|
||||
}, {
|
||||
name: 'join',
|
||||
isOptions: true,
|
||||
description: 'Join room with alias. Example: /join/#cinny:matrix.org',
|
||||
exe: (roomId, searchTerm) => openPublicRooms(searchTerm),
|
||||
}, {
|
||||
name: 'leave',
|
||||
description: 'Leave current room',
|
||||
exe: (roomId) => {
|
||||
roomActions.leave(roomId);
|
||||
},
|
||||
}, {
|
||||
name: 'invite',
|
||||
isOptions: true,
|
||||
description: 'Invite user to room. Example: /invite/@johndoe:matrix.org',
|
||||
exe: (roomId, searchTerm) => openInviteUser(roomId, searchTerm),
|
||||
}];
|
||||
|
||||
function CmdItem({ onClick, children }) {
|
||||
return (
|
||||
@@ -34,16 +71,16 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||
const cmdOptString = (typeof option === 'string') ? `/${option}` : '/?';
|
||||
return cmds.map((cmd) => (
|
||||
<CmdItem
|
||||
key={cmd}
|
||||
key={cmd.name}
|
||||
onClick={() => {
|
||||
fireCmd({
|
||||
prefix: cmdPrefix,
|
||||
option,
|
||||
result: commands[cmd],
|
||||
result: cmd,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text variant="b2">{`${cmd}${cmd.isOptions ? cmdOptString : ''}`}</Text>
|
||||
<Text variant="b2">{`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`}</Text>
|
||||
</CmdItem>
|
||||
));
|
||||
}
|
||||
@@ -172,19 +209,13 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const setupSearch = {
|
||||
'/': () => {
|
||||
asyncSearch.setup(Object.keys(commands), { isContain: true });
|
||||
setCmd({ prefix, suggestions: Object.keys(commands) });
|
||||
asyncSearch.setup(commands, { keys: ['name'], isContain: true });
|
||||
setCmd({ prefix, suggestions: commands });
|
||||
},
|
||||
':': () => {
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
const emojis = getEmojiForCompletion(mx, [mx.getRoom(roomId), ...parentRooms]);
|
||||
const recentEmoji = getRecentEmojis(20);
|
||||
const emojis = getEmojiForCompletion(mx.getRoom(roomId));
|
||||
asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 20 });
|
||||
setCmd({
|
||||
prefix,
|
||||
suggestions: recentEmoji.length > 0 ? recentEmoji : emojis.slice(26, 46),
|
||||
});
|
||||
setCmd({ prefix, suggestions: emojis.slice(26, 46) });
|
||||
},
|
||||
'@': () => {
|
||||
const members = mx.getRoom(roomId).getJoinedMembers().map((member) => ({
|
||||
@@ -205,9 +236,8 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
}
|
||||
function fireCmd(myCmd) {
|
||||
if (myCmd.prefix === '/') {
|
||||
viewEvent.emit('cmd_fired', {
|
||||
replace: `/${myCmd.result.name}`,
|
||||
});
|
||||
myCmd.result.exe(roomId, myCmd.option);
|
||||
viewEvent.emit('cmd_fired');
|
||||
}
|
||||
if (myCmd.prefix === ':') {
|
||||
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
|
||||
@@ -217,7 +247,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
}
|
||||
if (myCmd.prefix === '@') {
|
||||
viewEvent.emit('cmd_fired', {
|
||||
replace: `@${myCmd.result.userId}`,
|
||||
replace: myCmd.result.name,
|
||||
});
|
||||
}
|
||||
deactivateCmd();
|
||||
@@ -226,11 +256,11 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
function listenKeyboard(event) {
|
||||
const { activeElement } = document;
|
||||
const lastCmdItem = document.activeElement.parentNode.lastElementChild;
|
||||
if (event.key === 'Escape') {
|
||||
if (event.keyCode === 27) {
|
||||
if (activeElement.className !== 'cmd-item') return;
|
||||
viewEvent.emit('focus_msg_input');
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
if (event.keyCode === 9) {
|
||||
if (lastCmdItem.className !== 'cmd-item') return;
|
||||
if (lastCmdItem !== activeElement) return;
|
||||
if (event.shiftKey) return;
|
||||
|
||||
@@ -118,22 +118,17 @@ function handleOnClickCapture(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderEvent(
|
||||
roomTimeline,
|
||||
mEvent,
|
||||
prevMEvent,
|
||||
isFocus,
|
||||
isEdit,
|
||||
setEdit,
|
||||
cancelEdit,
|
||||
) {
|
||||
function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus = false) {
|
||||
const isBodyOnly = (prevMEvent !== null
|
||||
&& prevMEvent.getSender() === mEvent.getSender()
|
||||
&& prevMEvent.getType() !== 'm.room.member'
|
||||
&& prevMEvent.getType() !== 'm.room.create'
|
||||
&& diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
|
||||
);
|
||||
const timestamp = mEvent.getTs();
|
||||
const mDate = mEvent.getDate();
|
||||
const isToday = isInSameDay(mDate, new Date());
|
||||
|
||||
const time = dateFormat(mDate, isToday ? 'hh:MM TT' : 'dd/mm/yyyy');
|
||||
|
||||
if (mEvent.getType() === 'm.room.member') {
|
||||
const timelineChange = parseTimelineChange(mEvent);
|
||||
@@ -143,7 +138,7 @@ function renderEvent(
|
||||
key={mEvent.getId()}
|
||||
variant={timelineChange.variant}
|
||||
content={timelineChange.content}
|
||||
timestamp={timestamp}
|
||||
time={time}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -154,10 +149,7 @@ function renderEvent(
|
||||
isBodyOnly={isBodyOnly}
|
||||
roomTimeline={roomTimeline}
|
||||
focus={isFocus}
|
||||
fullTime={false}
|
||||
isEdit={isEdit}
|
||||
setEdit={setEdit}
|
||||
cancelEdit={cancelEdit}
|
||||
time={time}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -397,8 +389,6 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||
const timelineSVRef = useRef(null);
|
||||
const timelineScrollRef = useRef(null);
|
||||
const eventLimitRef = useRef(null);
|
||||
const [editEventId, setEditEventId] = useState(null);
|
||||
const cancelEdit = () => setEditEventId(null);
|
||||
|
||||
const readUptoEvtStore = useStore(roomTimeline);
|
||||
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
||||
@@ -483,42 +473,6 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||
}
|
||||
}, [newEvent]);
|
||||
|
||||
const listenKeyboard = useCallback((event) => {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) return;
|
||||
if (event.key !== 'ArrowUp') return;
|
||||
if (navigation.isRawModalVisible) return;
|
||||
|
||||
if (document.activeElement.id !== 'message-textarea') return;
|
||||
if (document.activeElement.value !== '') return;
|
||||
|
||||
const {
|
||||
timeline: tl, activeTimeline, liveTimeline, matrixClient: mx,
|
||||
} = roomTimeline;
|
||||
const limit = eventLimitRef.current;
|
||||
if (activeTimeline !== liveTimeline) return;
|
||||
if (tl.length > limit.length) return;
|
||||
|
||||
const mTypes = ['m.text'];
|
||||
for (let i = tl.length - 1; i >= 0; i -= 1) {
|
||||
const mE = tl[i];
|
||||
if (
|
||||
mE.getSender() === mx.getUserId()
|
||||
&& mE.getType() === 'm.room.message'
|
||||
&& mTypes.includes(mE.getContent()?.msgtype)
|
||||
) {
|
||||
setEditEventId(mE.getId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [roomTimeline]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.addEventListener('keydown', listenKeyboard);
|
||||
return () => {
|
||||
document.body.removeEventListener('keydown', listenKeyboard);
|
||||
};
|
||||
}, [listenKeyboard]);
|
||||
|
||||
const handleTimelineScroll = (event) => {
|
||||
const timelineScroll = timelineScrollRef.current;
|
||||
if (!event.target) return;
|
||||
@@ -584,15 +538,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||
const isFocus = focusId === mEvent.getId();
|
||||
if (isFocus) jumpToItemIndex = itemCountIndex;
|
||||
|
||||
tl.push(renderEvent(
|
||||
roomTimeline,
|
||||
mEvent,
|
||||
isNewEvent ? null : prevMEvent,
|
||||
isFocus,
|
||||
editEventId === mEvent.getId(),
|
||||
setEditEventId,
|
||||
cancelEdit,
|
||||
));
|
||||
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
|
||||
itemCountIndex += 1;
|
||||
}
|
||||
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import TextareaAutosize from 'react-autosize-textarea';
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import settings from '../../../client/state/settings';
|
||||
import { openEmojiBoard, openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { openEmojiBoard } from '../../../client/action/navigation';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { bytesToSize, getEventCords } from '../../../util/common';
|
||||
import { getUsername } from '../../../util/matrixUtil';
|
||||
@@ -20,13 +20,9 @@ import IconButton from '../../atoms/button/IconButton';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import { MessageReply } from '../../molecules/message/Message';
|
||||
|
||||
import StickerBoard from '../sticker-board/StickerBoard';
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
|
||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import SendIC from '../../../../public/res/ic/outlined/send.svg';
|
||||
import StickerIC from '../../../../public/res/ic/outlined/sticker.svg';
|
||||
import ShieldIC from '../../../../public/res/ic/outlined/shield.svg';
|
||||
import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
|
||||
import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.svg';
|
||||
@@ -34,8 +30,6 @@ import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg';
|
||||
import FileIC from '../../../../public/res/ic/outlined/file.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
import commands from './commands';
|
||||
|
||||
const CMD_REGEX = /(^\/|:|@)(\S*)$/;
|
||||
let isTyping = false;
|
||||
let isCmdActivated = false;
|
||||
@@ -135,9 +129,7 @@ function RoomViewInput({
|
||||
function firedCmd(cmdData) {
|
||||
const msg = textAreaRef.current.value;
|
||||
textAreaRef.current.value = replaceCmdWith(
|
||||
msg,
|
||||
cmdCursorPos,
|
||||
typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '',
|
||||
msg, cmdCursorPos, typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '',
|
||||
);
|
||||
deactivateCmd();
|
||||
}
|
||||
@@ -185,58 +177,30 @@ function RoomViewInput({
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
const sendBody = async (body, msgType = 'm.text') => {
|
||||
const sendMessage = async () => {
|
||||
requestAnimationFrame(() => deactivateCmdAndEmit());
|
||||
const msgBody = textAreaRef.current.value;
|
||||
if (roomsInput.isSending(roomId)) return;
|
||||
if (msgBody.trim() === '' && attachment === null) return;
|
||||
sendIsTyping(false);
|
||||
|
||||
roomsInput.setMessage(roomId, body);
|
||||
roomsInput.setMessage(roomId, msgBody);
|
||||
if (attachment !== null) {
|
||||
roomsInput.setAttachment(roomId, attachment);
|
||||
}
|
||||
textAreaRef.current.disabled = true;
|
||||
textAreaRef.current.style.cursor = 'not-allowed';
|
||||
await roomsInput.sendInput(roomId, msgType);
|
||||
await roomsInput.sendInput(roomId);
|
||||
textAreaRef.current.disabled = false;
|
||||
textAreaRef.current.style.cursor = 'unset';
|
||||
focusInput();
|
||||
|
||||
textAreaRef.current.value = roomsInput.getMessage(roomId);
|
||||
viewEvent.emit('message_sent');
|
||||
textAreaRef.current.style.height = 'unset';
|
||||
if (replyTo !== null) setReplyTo(null);
|
||||
};
|
||||
|
||||
const processCommand = (cmdBody) => {
|
||||
const spaceIndex = cmdBody.indexOf(' ');
|
||||
const cmdName = cmdBody.slice(1, spaceIndex > -1 ? spaceIndex : undefined);
|
||||
const cmdData = spaceIndex > -1 ? cmdBody.slice(spaceIndex + 1) : '';
|
||||
if (!commands[cmdName]) {
|
||||
confirmDialog('Invalid Command', `"${cmdName}" is not a valid command.`, 'Alright');
|
||||
return;
|
||||
}
|
||||
if (['me', 'shrug'].includes(cmdName)) {
|
||||
commands[cmdName].exe(roomId, cmdData, (message, msgType) => sendBody(message, msgType));
|
||||
return;
|
||||
}
|
||||
commands[cmdName].exe(roomId, cmdData);
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
requestAnimationFrame(() => deactivateCmdAndEmit());
|
||||
const msgBody = textAreaRef.current.value.trim();
|
||||
if (msgBody.startsWith('/')) {
|
||||
processCommand(msgBody.trim());
|
||||
textAreaRef.current.value = '';
|
||||
textAreaRef.current.style.height = 'unset';
|
||||
return;
|
||||
}
|
||||
if (msgBody === '' && attachment === null) return;
|
||||
sendBody(msgBody, 'm.text');
|
||||
};
|
||||
|
||||
const handleSendSticker = async (data) => {
|
||||
roomsInput.sendSticker(roomId, data);
|
||||
};
|
||||
|
||||
function processTyping(msg) {
|
||||
const isEmptyMsg = msg === '';
|
||||
|
||||
@@ -290,12 +254,7 @@ function RoomViewInput({
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
roomsInput.cancelReplyTo(roomId);
|
||||
setReplyTo(null);
|
||||
}
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
if (e.keyCode === 13 && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
@@ -369,7 +328,6 @@ function RoomViewInput({
|
||||
<ScrollView autoHide>
|
||||
<Text className="room-input__textarea-wrapper">
|
||||
<TextareaAutosize
|
||||
dir="auto"
|
||||
id="message-textarea"
|
||||
ref={textAreaRef}
|
||||
onChange={handleMsgTyping}
|
||||
@@ -382,29 +340,6 @@ function RoomViewInput({
|
||||
{isMarkdown && <RawIcon size="extra-small" src={MarkdownIC} />}
|
||||
</div>
|
||||
<div ref={rightOptionsRef} className="room-input__option-container">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
openReusableContextMenu(
|
||||
'top',
|
||||
(() => {
|
||||
const cords = getEventCords(e);
|
||||
cords.y -= 20;
|
||||
return cords;
|
||||
})(),
|
||||
(closeMenu) => (
|
||||
<StickerBoard
|
||||
roomId={roomId}
|
||||
onSelect={(data) => {
|
||||
handleSendSticker(data);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}}
|
||||
tooltip="Sticker"
|
||||
src={StickerIC}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
const cords = getEventCords(e);
|
||||
@@ -453,7 +388,6 @@ function RoomViewInput({
|
||||
/>
|
||||
<MessageReply
|
||||
userId={replyTo.userId}
|
||||
onKeyDown={handleKeyDown}
|
||||
name={getUsername(replyTo.userId)}
|
||||
color={colorMXID(replyTo.userId)}
|
||||
body={replyTo.body}
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
&__preview > img {
|
||||
max-height: 40px;
|
||||
border-radius: var(--bo-radius);
|
||||
max-width: 150px;
|
||||
}
|
||||
&__icon {
|
||||
padding: var(--sp-extra-tight);
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import React from 'react';
|
||||
import './commands.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import * as roomActions from '../../../client/action/room';
|
||||
import { hasDMWith, hasDevices } from '../../../util/matrixUtil';
|
||||
import { selectRoom, openReusableDialog } from '../../../client/action/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||||
|
||||
const MXID_REG = /^@\S+:\S+$/;
|
||||
const ROOM_ID_ALIAS_REG = /^(#|!)\S+:\S+$/;
|
||||
const ROOM_ID_REG = /^!\S+:\S+$/;
|
||||
const MXC_REG = /^mxc:\/\/\S+$/;
|
||||
|
||||
export function processMxidAndReason(data) {
|
||||
let reason;
|
||||
let idData = data;
|
||||
const reasonMatch = data.match(/\s-r\s/);
|
||||
if (reasonMatch) {
|
||||
idData = data.slice(0, reasonMatch.index);
|
||||
reason = data.slice(reasonMatch.index + reasonMatch[0].length);
|
||||
if (reason.trim() === '') reason = undefined;
|
||||
}
|
||||
const rawIds = idData.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
return {
|
||||
userIds,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
const commands = {
|
||||
me: {
|
||||
name: 'me',
|
||||
description: 'Display action',
|
||||
exe: (roomId, data, onSuccess) => {
|
||||
const body = data.trim();
|
||||
if (body === '') return;
|
||||
onSuccess(body, 'm.emote');
|
||||
},
|
||||
},
|
||||
shrug: {
|
||||
name: 'shrug',
|
||||
description: 'Send ¯\\_(ツ)_/¯ as message',
|
||||
exe: (roomId, data, onSuccess) => onSuccess(
|
||||
`¯\\_(ツ)_/¯${data.trim() !== '' ? ` ${data}` : ''}`,
|
||||
'm.text',
|
||||
),
|
||||
},
|
||||
help: {
|
||||
name: 'help',
|
||||
description: 'View all commands',
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
exe: () => openHelpDialog(),
|
||||
},
|
||||
startdm: {
|
||||
name: 'startdm',
|
||||
description: 'Start DM with user. Example: /startdm userId1 userId2',
|
||||
exe: async (roomId, data) => {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG) && id !== mx.getUserId());
|
||||
if (userIds.length === 0) return;
|
||||
if (userIds.length === 1) {
|
||||
const dmRoomId = hasDMWith(userIds[0]);
|
||||
if (dmRoomId) {
|
||||
selectRoom(dmRoomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const devices = await Promise.all(userIds.map(hasDevices));
|
||||
const isEncrypt = devices.every((hasDevice) => hasDevice);
|
||||
const result = await roomActions.createDM(userIds, isEncrypt);
|
||||
selectRoom(result.room_id);
|
||||
},
|
||||
},
|
||||
join: {
|
||||
name: 'join',
|
||||
description: 'Join room with alias. Example: /join alias1 alias2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const roomIds = rawIds.filter((id) => id.match(ROOM_ID_ALIAS_REG));
|
||||
roomIds.map((id) => roomActions.join(id));
|
||||
},
|
||||
},
|
||||
leave: {
|
||||
name: 'leave',
|
||||
description: 'Leave current room.',
|
||||
exe: (roomId, data) => {
|
||||
if (data.trim() === '') {
|
||||
roomActions.leave(roomId);
|
||||
return;
|
||||
}
|
||||
const rawIds = data.split(' ');
|
||||
const roomIds = rawIds.filter((id) => id.match(ROOM_ID_REG));
|
||||
roomIds.map((id) => roomActions.leave(id));
|
||||
},
|
||||
},
|
||||
invite: {
|
||||
name: 'invite',
|
||||
description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.invite(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
disinvite: {
|
||||
name: 'disinvite',
|
||||
description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.kick(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
kick: {
|
||||
name: 'kick',
|
||||
description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.kick(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
ban: {
|
||||
name: 'ban',
|
||||
description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
|
||||
exe: (roomId, data) => {
|
||||
const { userIds, reason } = processMxidAndReason(data);
|
||||
userIds.map((id) => roomActions.ban(roomId, id, reason));
|
||||
},
|
||||
},
|
||||
unban: {
|
||||
name: 'unban',
|
||||
description: 'Unban user from room. Example: /unban userId1 userId2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
userIds.map((id) => roomActions.unban(roomId, id));
|
||||
},
|
||||
},
|
||||
ignore: {
|
||||
name: 'ignore',
|
||||
description: 'Ignore user. Example: /ignore userId1 userId2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
if (userIds.length > 0) roomActions.ignore(userIds);
|
||||
},
|
||||
},
|
||||
unignore: {
|
||||
name: 'unignore',
|
||||
description: 'Unignore user. Example: /unignore userId1 userId2',
|
||||
exe: (roomId, data) => {
|
||||
const rawIds = data.split(' ');
|
||||
const userIds = rawIds.filter((id) => id.match(MXID_REG));
|
||||
if (userIds.length > 0) roomActions.unignore(userIds);
|
||||
},
|
||||
},
|
||||
myroomnick: {
|
||||
name: 'myroomnick',
|
||||
description: 'Change my room nick',
|
||||
exe: (roomId, data) => {
|
||||
const nick = data.trim();
|
||||
if (nick === '') return;
|
||||
roomActions.setMyRoomNick(roomId, nick);
|
||||
},
|
||||
},
|
||||
myroomavatar: {
|
||||
name: 'myroomavatar',
|
||||
description: 'Change my room avatar. Example /myroomavatar mxc://xyzabc',
|
||||
exe: (roomId, data) => {
|
||||
if (data.match(MXC_REG)) {
|
||||
roomActions.setMyRoomAvatar(roomId, data);
|
||||
}
|
||||
},
|
||||
},
|
||||
converttodm: {
|
||||
name: 'converttodm',
|
||||
description: 'Convert room to direct message',
|
||||
exe: (roomId) => {
|
||||
roomActions.convertToDm(roomId);
|
||||
},
|
||||
},
|
||||
converttoroom: {
|
||||
name: 'converttoroom',
|
||||
description: 'Convert direct message to room',
|
||||
exe: (roomId) => {
|
||||
roomActions.convertToRoom(roomId);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function openHelpDialog() {
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Commands</Text>,
|
||||
() => (
|
||||
<div className="commands-dialog">
|
||||
{Object.keys(commands).map((cmdName) => (
|
||||
<SettingTile
|
||||
key={cmdName}
|
||||
title={cmdName}
|
||||
content={<Text variant="b3">{commands[cmdName].description}</Text>}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export default commands;
|
||||
@@ -1,10 +0,0 @@
|
||||
.commands-dialog {
|
||||
& > * {
|
||||
padding: var(--sp-tight) var(--sp-normal);
|
||||
border-bottom: 1px solid var(--bg-surface-border);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: var(--sp-extra-loose);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ function getUsersActionJsx(roomId, userIds, actionStr) {
|
||||
|
||||
const othersCount = userIds.length - MAX_VISIBLE_COUNT;
|
||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
||||
return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} others are {actionStr}</>;
|
||||
return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} other are {actionStr}</>;
|
||||
}
|
||||
|
||||
function parseTimelineChange(mEvent) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||||
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
|
||||
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
|
||||
import { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack';
|
||||
|
||||
import ProfileEditor from '../profile-editor/ProfileEditor';
|
||||
import CrossSigning from './CrossSigning';
|
||||
@@ -32,7 +31,6 @@ import KeyBackup from './KeyBackup';
|
||||
import DeviceManage from './DeviceManage';
|
||||
|
||||
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||
import BellIC from '../../../../public/res/ic/outlined/bell.svg';
|
||||
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
|
||||
@@ -59,25 +57,23 @@ function AppearanceSection() {
|
||||
)}
|
||||
content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Theme"
|
||||
content={(
|
||||
<SegmentedControls
|
||||
selected={settings.useSystemTheme ? -1 : settings.getThemeIndex()}
|
||||
segments={[
|
||||
{ text: 'Light' },
|
||||
{ text: 'Silver' },
|
||||
{ text: 'Dark' },
|
||||
{ text: 'Butter' },
|
||||
]}
|
||||
onSelect={(index) => {
|
||||
if (settings.useSystemTheme) toggleSystemTheme();
|
||||
settings.setTheme(index);
|
||||
updateState({});
|
||||
}}
|
||||
/>
|
||||
{!settings.useSystemTheme && (
|
||||
<SettingTile
|
||||
title="Theme"
|
||||
content={(
|
||||
<SegmentedControls
|
||||
selected={settings.getThemeIndex()}
|
||||
segments={[
|
||||
{ text: 'Light' },
|
||||
{ text: 'Silver' },
|
||||
{ text: 'Dark' },
|
||||
{ text: 'Butter' },
|
||||
]}
|
||||
onSelect={(index) => settings.setTheme(index)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-appearance__card">
|
||||
<MenuHeader>Room messages</MenuHeader>
|
||||
@@ -171,15 +167,6 @@ function NotificationsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiSection() {
|
||||
return (
|
||||
<>
|
||||
<div className="settings-emoji__card"><ImagePackUser /></div>
|
||||
<div className="settings-emoji__card"><ImagePackGlobal /></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SecuritySection() {
|
||||
return (
|
||||
<div className="settings-security">
|
||||
@@ -261,7 +248,6 @@ function AboutSection() {
|
||||
export const tabText = {
|
||||
APPEARANCE: 'Appearance',
|
||||
NOTIFICATIONS: 'Notifications',
|
||||
EMOJI: 'Emoji',
|
||||
SECURITY: 'Security',
|
||||
ABOUT: 'About',
|
||||
};
|
||||
@@ -275,11 +261,6 @@ const tabItems = [{
|
||||
iconSrc: BellIC,
|
||||
disabled: false,
|
||||
render: () => <NotificationsSection />,
|
||||
}, {
|
||||
text: tabText.EMOJI,
|
||||
iconSrc: EmojiIC,
|
||||
disabled: false,
|
||||
render: () => <EmojiSection />,
|
||||
}, {
|
||||
text: tabText.SECURITY,
|
||||
iconSrc: LockIC,
|
||||
|
||||
@@ -40,8 +40,7 @@
|
||||
.settings-notifications,
|
||||
.settings-security__card,
|
||||
.settings-security .device-manage,
|
||||
.settings-about__card,
|
||||
.settings-emoji__card {
|
||||
.settings-about__card {
|
||||
@extend .settings-window__card;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
|
||||
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
|
||||
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
|
||||
import RoomMembers from '../../molecules/room-members/RoomMembers';
|
||||
import RoomEmojis from '../../molecules/room-emojis/RoomEmojis';
|
||||
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
@@ -36,7 +35,6 @@ import PinIC from '../../../../public/res/ic/outlined/pin.svg';
|
||||
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
|
||||
import CategoryIC from '../../../../public/res/ic/outlined/category.svg';
|
||||
import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
@@ -44,7 +42,6 @@ import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
const tabText = {
|
||||
GENERAL: 'General',
|
||||
MEMBERS: 'Members',
|
||||
EMOJIS: 'Emojis',
|
||||
PERMISSIONS: 'Permissions',
|
||||
};
|
||||
|
||||
@@ -56,10 +53,6 @@ const tabItems = [{
|
||||
iconSrc: UserIC,
|
||||
text: tabText.MEMBERS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: EmojiIC,
|
||||
text: tabText.EMOJIS,
|
||||
disabled: false,
|
||||
}, {
|
||||
iconSrc: ShieldUserIC,
|
||||
text: tabText.PERMISSIONS,
|
||||
@@ -185,7 +178,6 @@ function SpaceSettings() {
|
||||
<div className="space-settings__cards-wrapper">
|
||||
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
|
||||
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
|
||||
{selectedTab.text === tabText.EMOJIS && <RoomEmojis roomId={roomId} />}
|
||||
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './StickerBoard.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getRelevantPacks } from '../emoji-board/custom-emoji';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
|
||||
function StickerBoard({ roomId, onSelect }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const scrollRef = useRef(null);
|
||||
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
|
||||
const packs = getRelevantPacks(
|
||||
mx,
|
||||
[room, ...parentRooms],
|
||||
).filter((pack) => pack.getStickers().length !== 0);
|
||||
|
||||
function isTargetNotSticker(target) {
|
||||
return target.classList.contains('sticker-board__sticker') === false;
|
||||
}
|
||||
function getStickerData(target) {
|
||||
const mxc = target.getAttribute('data-mx-sticker');
|
||||
const body = target.getAttribute('title');
|
||||
const httpUrl = target.getAttribute('src');
|
||||
return { mxc, body, httpUrl };
|
||||
}
|
||||
const handleOnSelect = (e) => {
|
||||
if (isTargetNotSticker(e.target)) return;
|
||||
|
||||
const stickerData = getStickerData(e.target);
|
||||
onSelect(stickerData);
|
||||
};
|
||||
|
||||
const openGroup = (groupIndex) => {
|
||||
const scrollContent = scrollRef.current.firstElementChild;
|
||||
scrollContent.children[groupIndex].scrollIntoView();
|
||||
};
|
||||
|
||||
const renderPack = (pack) => (
|
||||
<div className="sticker-board__pack" key={pack.id}>
|
||||
<Text className="sticker-board__pack-header" variant="b2" weight="bold">{pack.displayName ?? 'Unknown'}</Text>
|
||||
<div className="sticker-board__pack-items">
|
||||
{pack.getStickers().map((sticker) => (
|
||||
<img
|
||||
key={sticker.shortcode}
|
||||
className="sticker-board__sticker"
|
||||
src={mx.mxcUrlToHttp(sticker.mxc)}
|
||||
alt={sticker.shortcode}
|
||||
title={sticker.body ?? sticker.shortcode}
|
||||
data-mx-sticker={sticker.mxc}
|
||||
loading="lazy"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="sticker-board">
|
||||
{packs.length > 0 && (
|
||||
<ScrollView invisible>
|
||||
<div className="sticker-board__sidebar">
|
||||
{packs.map((pack, index) => {
|
||||
const src = mx.mxcUrlToHttp(pack.avatarUrl ?? pack.getStickers()[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
key={pack.id}
|
||||
onClick={() => openGroup(index)}
|
||||
src={src}
|
||||
tooltip={pack.displayName || 'Unknown'}
|
||||
tooltipPlacement="left"
|
||||
isImage
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollView>
|
||||
)}
|
||||
<div className="sticker-board__container">
|
||||
<ScrollView autoHide ref={scrollRef}>
|
||||
<div
|
||||
onClick={handleOnSelect}
|
||||
className="sticker-board__content"
|
||||
>
|
||||
{
|
||||
packs.length > 0
|
||||
? packs.map(renderPack)
|
||||
: (
|
||||
<div className="sticker-board__empty">
|
||||
<Text>There is no sticker pack.</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
StickerBoard.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default StickerBoard;
|
||||
@@ -1,74 +0,0 @@
|
||||
@use '../../partials/dir';
|
||||
|
||||
.sticker-board {
|
||||
--sticker-board-height: 390px;
|
||||
--sticker-board-width: 286px;
|
||||
display: flex;
|
||||
height: var(--sticker-board-height);
|
||||
display: flex;
|
||||
|
||||
& > .scrollbar {
|
||||
width: initial;
|
||||
height: var(--sticker-board-height);
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
padding: 4px 6px;
|
||||
@include dir.side(border, none, 1px solid var(--bg-surface-border));
|
||||
}
|
||||
|
||||
&__container {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
width: var(--sticker-board-width);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__content {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&__pack {
|
||||
margin-bottom: var(--sp-normal);
|
||||
position: relative;
|
||||
|
||||
&-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
background-color: var(--bg-surface);
|
||||
|
||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
||||
padding: var(--sp-extra-tight) var(--sp-ultra-tight);
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 -4px 0 0 var(--bg-surface);
|
||||
border-bottom: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
&-items {
|
||||
margin: var(--sp-tight);
|
||||
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-normal) var(--sp-tight);
|
||||
|
||||
img {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
width: 100%;
|
||||
height: var(--sticker-board-height);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@ import Avatar from '../../atoms/avatar/Avatar';
|
||||
import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import EyeIC from '../../../../public/res/ic/outlined/eye.svg';
|
||||
import EyeBlindIC from '../../../../public/res/ic/outlined/eye-blind.svg';
|
||||
import CinnySvg from '../../../../public/res/svg/cinny.svg';
|
||||
import SSOButtons from '../../molecules/sso-buttons/SSOButtons';
|
||||
|
||||
@@ -56,8 +54,11 @@ function Homeserver({ onChange }) {
|
||||
const setupHsConfig = async (servername) => {
|
||||
setProcess({ isLoading: true, message: 'Looking for homeserver...' });
|
||||
let baseUrl = null;
|
||||
baseUrl = await getBaseUrl(servername);
|
||||
|
||||
try {
|
||||
baseUrl = await getBaseUrl(servername);
|
||||
} catch (e) {
|
||||
baseUrl = e.message;
|
||||
}
|
||||
if (searchingHs !== servername) return;
|
||||
setProcess({ isLoading: true, message: `Connecting to ${baseUrl}...` });
|
||||
const tempClient = auth.createTemporaryClient(baseUrl);
|
||||
@@ -96,7 +97,7 @@ function Homeserver({ onChange }) {
|
||||
if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) {
|
||||
throw new Error();
|
||||
}
|
||||
setHs({ selected: hsList[selectedHs], list: hsList, allowCustom });
|
||||
setHs({ selected: hsList[selectedHs], list: hsList, allowCustom: allowCustom });
|
||||
} catch {
|
||||
setHs({ selected: 'matrix.org', list: ['matrix.org'], allowCustom: true });
|
||||
}
|
||||
@@ -113,14 +114,8 @@ function Homeserver({ onChange }) {
|
||||
return (
|
||||
<>
|
||||
<div className="homeserver-form">
|
||||
<Input
|
||||
name="homeserver"
|
||||
onChange={handleHsInput}
|
||||
value={hs?.selected}
|
||||
forwardRef={hsRef}
|
||||
label="Homeserver"
|
||||
disabled={hs === null || !hs.allowCustom}
|
||||
/>
|
||||
<Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver"
|
||||
disabled={hs === null || !hs.allowCustom} />
|
||||
<ContextMenu
|
||||
placement="right"
|
||||
content={(hideMenu) => (
|
||||
@@ -161,7 +156,6 @@ Homeserver.propTypes = {
|
||||
|
||||
function Login({ loginFlow, baseUrl }) {
|
||||
const [typeIndex, setTypeIndex] = useState(0);
|
||||
const [passVisible, setPassVisible] = useState(false);
|
||||
const loginTypes = ['Username', 'Email'];
|
||||
const isPassword = loginFlow?.filter((flow) => flow.type === 'm.login.password')[0];
|
||||
const ssoProviders = loginFlow?.filter((flow) => flow.type === 'm.login.sso')[0];
|
||||
@@ -172,38 +166,31 @@ function Login({ loginFlow, baseUrl }) {
|
||||
|
||||
const validator = (values) => {
|
||||
const errors = {};
|
||||
if (typeIndex === 0 && values.username.length > 0 && values.username.indexOf(':') > -1) {
|
||||
errors.username = 'Username must contain local-part only';
|
||||
}
|
||||
if (typeIndex === 1 && values.email.length > 0 && !isValidInput(values.email, EMAIL_REGEX)) {
|
||||
errors.email = BAD_EMAIL_ERROR;
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
const submitter = async (values, actions) => {
|
||||
let userBaseUrl = baseUrl;
|
||||
let { username } = values;
|
||||
const mxIdMatch = username.match(/^@(.+):(.+\..+)$/);
|
||||
if (typeIndex === 0 && mxIdMatch) {
|
||||
[, username, userBaseUrl] = mxIdMatch;
|
||||
userBaseUrl = await getBaseUrl(userBaseUrl);
|
||||
}
|
||||
|
||||
return auth.login(
|
||||
userBaseUrl,
|
||||
typeIndex === 0 ? normalizeUsername(username) : undefined,
|
||||
typeIndex === 1 ? values.email : undefined,
|
||||
values.password,
|
||||
).then(() => {
|
||||
actions.setSubmitting(true);
|
||||
window.location.reload();
|
||||
}).catch((error) => {
|
||||
let msg = error.message;
|
||||
if (msg === 'Unknown message') msg = 'Please check your credentials';
|
||||
actions.setErrors({
|
||||
password: msg === 'Invalid password' ? msg : undefined,
|
||||
other: msg !== 'Invalid password' ? msg : undefined,
|
||||
});
|
||||
actions.setSubmitting(false);
|
||||
const submitter = (values, actions) => auth.login(
|
||||
baseUrl,
|
||||
typeIndex === 0 ? normalizeUsername(values.username) : undefined,
|
||||
typeIndex === 1 ? values.email : undefined,
|
||||
values.password,
|
||||
).then(() => {
|
||||
actions.setSubmitting(true);
|
||||
window.location.reload();
|
||||
}).catch((error) => {
|
||||
let msg = error.message;
|
||||
if (msg === 'Unknown message') msg = 'Please check your credentials';
|
||||
actions.setErrors({
|
||||
password: msg === 'Invalid password' ? msg : undefined,
|
||||
other: msg !== 'Invalid password' ? msg : undefined,
|
||||
});
|
||||
};
|
||||
actions.setSubmitting(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -249,10 +236,7 @@ function Login({ loginFlow, baseUrl }) {
|
||||
{errors.username && <Text className="auth-form__error" variant="b3">{errors.username}</Text>}
|
||||
{typeIndex === 1 && <Input values={values.email} name="email" onChange={handleChange} label="Email" type="email" required />}
|
||||
{errors.email && <Text className="auth-form__error" variant="b3">{errors.email}</Text>}
|
||||
<div className="auth-form__pass-eye-wrapper">
|
||||
<Input values={values.password} name="password" onChange={handleChange} label="Password" type={passVisible ? 'text' : 'password'} required />
|
||||
<IconButton onClick={() => setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" />
|
||||
</div>
|
||||
<Input values={values.password} name="password" onChange={handleChange} label="Password" type="password" required />
|
||||
{errors.password && <Text className="auth-form__error" variant="b3">{errors.password}</Text>}
|
||||
{errors.other && <Text className="auth-form__error" variant="b3">{errors.other}</Text>}
|
||||
<div className="auth-form__btns">
|
||||
@@ -285,8 +269,6 @@ let sid;
|
||||
let clientSecret;
|
||||
function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||
const [process, setProcess] = useState({});
|
||||
const [passVisible, setPassVisible] = useState(false);
|
||||
const [cPassVisible, setCPassVisible] = useState(false);
|
||||
const formRef = useRef();
|
||||
|
||||
const ssoProviders = loginFlow?.filter((flow) => flow.type === 'm.login.sso')[0];
|
||||
@@ -337,7 +319,6 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||
if (!isAvail) {
|
||||
actions.setErrors({ username: 'Username is already taken' });
|
||||
actions.setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
if (isEmail && values.email.length > 0) {
|
||||
const result = await auth.verifyEmail(baseUrl, values.email, clientSecret, 1);
|
||||
@@ -456,15 +437,9 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||
<form className="auth-form" ref={formRef} onSubmit={handleSubmit}>
|
||||
<Input values={values.username} name="username" onChange={handleChange} label="Username" type="username" required />
|
||||
{errors.username && <Text className="auth-form__error" variant="b3">{errors.username}</Text>}
|
||||
<div className="auth-form__pass-eye-wrapper">
|
||||
<Input values={values.password} name="password" onChange={handleChange} label="Password" type={passVisible ? 'text' : 'password'} required />
|
||||
<IconButton onClick={() => setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" />
|
||||
</div>
|
||||
<Input values={values.password} name="password" onChange={handleChange} label="Password" type="password" required />
|
||||
{errors.password && <Text className="auth-form__error" variant="b3">{errors.password}</Text>}
|
||||
<div className="auth-form__pass-eye-wrapper">
|
||||
<Input values={values.confirmPassword} name="confirmPassword" onChange={handleChange} label="Confirm password" type={cPassVisible ? 'text' : 'password'} required />
|
||||
<IconButton onClick={() => setCPassVisible(!cPassVisible)} src={cPassVisible ? EyeIC : EyeBlindIC} size="extra-small" />
|
||||
</div>
|
||||
<Input values={values.confirmPassword} name="confirmPassword" onChange={handleChange} label="Confirm password" type="password" required />
|
||||
{errors.confirmPassword && <Text className="auth-form__error" variant="b3">{errors.confirmPassword}</Text>}
|
||||
{isEmail && <Input values={values.email} name="email" onChange={handleChange} label={`Email${isEmailRequired ? '' : ' (optional)'}`} type="email" required={isEmailRequired} />}
|
||||
{errors.email && <Text className="auth-form__error" variant="b3">{errors.email}</Text>}
|
||||
|
||||
@@ -97,8 +97,7 @@
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
& > .input-container,
|
||||
&__pass-eye-wrapper {
|
||||
& > .input-container {
|
||||
margin: var(--sp-tight) 0 var(--sp-ultra-tight);
|
||||
}
|
||||
|
||||
@@ -108,20 +107,6 @@
|
||||
margin-top: calc(var(--sp-extra-loose) + var(--sp-tight));
|
||||
}
|
||||
|
||||
&__pass-eye-wrapper {
|
||||
position: relative;
|
||||
& .ic-btn {
|
||||
position: absolute;
|
||||
@include dir.prop(right, 6px, unset);
|
||||
@include dir.prop(left, unset, 6px );
|
||||
bottom: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
& input {
|
||||
@include dir.side(padding, var(--sp-normal), 46px);
|
||||
}
|
||||
}
|
||||
|
||||
&__btns {
|
||||
padding-top: var(--sp-loose);
|
||||
margin-bottom: var(--sp-extra-loose);
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import initMatrix from '../initMatrix';
|
||||
|
||||
async function logout() {
|
||||
function logout() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
mx.stopClient();
|
||||
try {
|
||||
await mx.logout();
|
||||
} catch {
|
||||
// ignore if failed to logout
|
||||
}
|
||||
mx.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
mx.logout().then(() => {
|
||||
mx.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
export default logout;
|
||||
@@ -6,7 +6,7 @@ import { getIdServer } from '../../util/matrixUtil';
|
||||
/**
|
||||
* https://github.com/matrix-org/matrix-react-sdk/blob/1e6c6e9d800890c732d60429449bc280de01a647/src/Rooms.js#L73
|
||||
* @param {string} roomId Id of room to add
|
||||
* @param {string} userId User id to which dm || undefined to remove
|
||||
* @param {string} userId User id to which dm
|
||||
* @returns {Promise} A promise
|
||||
*/
|
||||
function addRoomToMDirect(roomId, userId) {
|
||||
@@ -79,23 +79,13 @@ function guessDMRoomTargetId(room, myUserId) {
|
||||
return oldestMember.userId;
|
||||
}
|
||||
|
||||
function convertToDm(roomId) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
return addRoomToMDirect(roomId, guessDMRoomTargetId(room, mx.getUserId()));
|
||||
}
|
||||
|
||||
function convertToRoom(roomId) {
|
||||
return addRoomToMDirect(roomId, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} roomId
|
||||
* @param {boolean} isDM
|
||||
* @param {string[]} via
|
||||
*/
|
||||
async function join(roomIdOrAlias, isDM = false, via = undefined) {
|
||||
async function join(roomIdOrAlias, isDM, via) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const roomIdParts = roomIdOrAlias.split(':');
|
||||
const viaServers = via || [roomIdParts[1]];
|
||||
@@ -160,10 +150,10 @@ async function create(options, isDM = false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createDM(userIdOrIds, isEncrypted = true) {
|
||||
async function createDM(userId, isEncrypted = true) {
|
||||
const options = {
|
||||
is_direct: true,
|
||||
invite: Array.isArray(userIdOrIds) ? userIdOrIds : [userIdOrIds],
|
||||
invite: [userId],
|
||||
visibility: 'private',
|
||||
preset: 'trusted_private_chat',
|
||||
initial_state: [],
|
||||
@@ -272,10 +262,10 @@ async function createRoom(opts) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function invite(roomId, userId, reason) {
|
||||
async function invite(roomId, userId) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
const result = await mx.invite(roomId, userId, undefined, reason);
|
||||
const result = await mx.invite(roomId, userId);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -300,21 +290,6 @@ async function unban(roomId, userId) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function ignore(userIds) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
let ignoredUsers = mx.getIgnoredUsers().concat(userIds);
|
||||
ignoredUsers = [...new Set(ignoredUsers)];
|
||||
await mx.setIgnoredUsers(ignoredUsers);
|
||||
}
|
||||
|
||||
async function unignore(userIds) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
const ignoredUsers = mx.getIgnoredUsers();
|
||||
await mx.setIgnoredUsers(ignoredUsers.filter((id) => !userIds.includes(id)));
|
||||
}
|
||||
|
||||
async function setPowerLevel(roomId, userId, powerLevel) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
@@ -325,37 +300,9 @@ async function setPowerLevel(roomId, userId, powerLevel) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function setMyRoomNick(roomId, nick) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const mEvent = room.currentState.getStateEvents('m.room.member', mx.getUserId());
|
||||
const content = mEvent?.getContent();
|
||||
if (!content) return;
|
||||
await mx.sendStateEvent(roomId, 'm.room.member', {
|
||||
...content,
|
||||
displayname: nick,
|
||||
}, mx.getUserId());
|
||||
}
|
||||
|
||||
async function setMyRoomAvatar(roomId, mxc) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const mEvent = room.currentState.getStateEvents('m.room.member', mx.getUserId());
|
||||
const content = mEvent?.getContent();
|
||||
if (!content) return;
|
||||
await mx.sendStateEvent(roomId, 'm.room.member', {
|
||||
...content,
|
||||
avatar_url: mxc,
|
||||
}, mx.getUserId());
|
||||
}
|
||||
|
||||
export {
|
||||
convertToDm,
|
||||
convertToRoom,
|
||||
join, leave,
|
||||
createDM, createRoom,
|
||||
invite, kick, ban, unban,
|
||||
ignore, unignore,
|
||||
setPowerLevel,
|
||||
setMyRoomNick, setMyRoomAvatar,
|
||||
};
|
||||
@@ -11,18 +11,17 @@ async function redactEvent(roomId, eventId, reason) {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendReaction(roomId, toEventId, reaction, shortcode) {
|
||||
async function sendReaction(roomId, toEventId, reaction) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const content = {
|
||||
'm.relates_to': {
|
||||
event_id: toEventId,
|
||||
key: reaction,
|
||||
rel_type: 'm.annotation',
|
||||
},
|
||||
};
|
||||
if (typeof shortcode === 'string') content.shortcode = shortcode;
|
||||
|
||||
try {
|
||||
await mx.sendEvent(roomId, 'm.reaction', content);
|
||||
await mx.sendEvent(roomId, 'm.reaction', {
|
||||
'm.relates_to': {
|
||||
event_id: toEventId,
|
||||
key: reaction,
|
||||
rel_type: 'm.annotation',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ function listenKeyboard(event) {
|
||||
// Ctrl/Cmd +
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
// open search modal
|
||||
if (event.key === 'k') {
|
||||
if (event.code === 'KeyK') {
|
||||
event.preventDefault();
|
||||
if (navigation.isRawModalVisible) return;
|
||||
openSearch();
|
||||
}
|
||||
|
||||
// focus message field on paste
|
||||
if (event.key === 'v') {
|
||||
if (event.code === 'KeyV') {
|
||||
if (navigation.isRawModalVisible) return;
|
||||
const msgTextarea = document.getElementById('message-textarea');
|
||||
const { activeElement } = document;
|
||||
@@ -51,8 +51,11 @@ function listenKeyboard(event) {
|
||||
|
||||
if (!event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||
if (navigation.isRawModalVisible) return;
|
||||
if (['input', 'textarea'].includes(document.activeElement.tagName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
if (event.code === 'Escape') {
|
||||
if (navigation.isRoomSettings) {
|
||||
toggleRoomSettings();
|
||||
return;
|
||||
@@ -63,10 +66,6 @@ function listenKeyboard(event) {
|
||||
}
|
||||
}
|
||||
|
||||
if (['input', 'textarea'].includes(document.activeElement.tagName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// focus the text field on most keypresses
|
||||
if (shouldFocusMessageField(event.code)) {
|
||||
// press any key to focus and type in message field
|
||||
|
||||
@@ -4,17 +4,6 @@ import { selectTab, selectSpace, selectRoom } from '../action/navigation';
|
||||
|
||||
function initRoomListListener(roomList) {
|
||||
const listenRoomLeave = (roomId) => {
|
||||
const parents = roomList.roomIdToParents.get(roomId);
|
||||
|
||||
if (parents) {
|
||||
[...parents].forEach((pId) => {
|
||||
const data = navigation.spaceToRoom.get(pId);
|
||||
if (data?.roomId === roomId) {
|
||||
navigation.spaceToRoom.delete(pId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (navigation.selectedRoomId === roomId) {
|
||||
selectRoom(null);
|
||||
}
|
||||
@@ -24,8 +13,6 @@ function initRoomListListener(roomList) {
|
||||
if (idIndex === 0) selectTab(cons.tabs.HOME);
|
||||
else selectSpace(navigation.selectedSpacePath[idIndex - 1]);
|
||||
}
|
||||
|
||||
navigation.removeRecentRoom(roomId);
|
||||
};
|
||||
|
||||
roomList.on(cons.events.roomList.ROOM_LEAVED, listenRoomLeave);
|
||||
|
||||
@@ -8,19 +8,12 @@ import AccountData from './state/AccountData';
|
||||
import RoomsInput from './state/RoomsInput';
|
||||
import Notifications from './state/Notifications';
|
||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||
import navigation from './state/navigation';
|
||||
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
|
||||
// logger.disableAll();
|
||||
|
||||
class InitMatrix extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
navigation.initMatrix = this;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.startClient();
|
||||
this.setupSync();
|
||||
@@ -40,6 +33,7 @@ class InitMatrix extends EventEmitter {
|
||||
accessToken: secret.accessToken,
|
||||
userId: secret.userId,
|
||||
store: indexedDBStore,
|
||||
sessionStore: new sdk.WebStorageSessionStore(global.localStorage),
|
||||
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
||||
deviceId: secret.deviceId,
|
||||
timelineSupport: true,
|
||||
@@ -73,7 +67,7 @@ class InitMatrix extends EventEmitter {
|
||||
if (prevState === null) {
|
||||
this.roomList = new RoomList(this.matrixClient);
|
||||
this.accountData = new AccountData(this.roomList);
|
||||
this.roomsInput = new RoomsInput(this.matrixClient, this.roomList);
|
||||
this.roomsInput = new RoomsInput(this.matrixClient);
|
||||
this.notifications = new Notifications(this.roomList);
|
||||
this.emit('init_loading_finished');
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class RoomList extends EventEmitter {
|
||||
|
||||
child.forEach((childId) => {
|
||||
const room = this.matrixClient.getRoom(childId);
|
||||
if (room === null || room.getMyMembership() !== 'join') return;
|
||||
if (room === null) return;
|
||||
if (room.isSpaceRoom()) categorizeSpace(childId);
|
||||
else mappedChild.add(childId);
|
||||
});
|
||||
@@ -257,10 +257,10 @@ class RoomList extends EventEmitter {
|
||||
const latestMDirects = this.getMDirects();
|
||||
|
||||
latestMDirects.forEach((directId) => {
|
||||
const myRoom = this.matrixClient.getRoom(directId);
|
||||
if (this.mDirects.has(directId)) return;
|
||||
this.mDirects.add(directId);
|
||||
|
||||
const myRoom = this.matrixClient.getRoom(directId);
|
||||
if (myRoom === null) return;
|
||||
if (myRoom.getMyMembership() === 'join') {
|
||||
this.directs.add(directId);
|
||||
@@ -268,19 +268,6 @@ class RoomList extends EventEmitter {
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
}
|
||||
});
|
||||
|
||||
[...this.directs].forEach((directId) => {
|
||||
if (latestMDirects.has(directId)) return;
|
||||
this.mDirects.delete(directId);
|
||||
|
||||
const myRoom = this.matrixClient.getRoom(directId);
|
||||
if (myRoom === null) return;
|
||||
if (myRoom.getMyMembership() === 'join') {
|
||||
this.directs.delete(directId);
|
||||
this.rooms.add(directId);
|
||||
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.matrixClient.on('Room.name', (room) => {
|
||||
|
||||
+71
-148
@@ -1,36 +1,25 @@
|
||||
import EventEmitter from 'events';
|
||||
import { micromark } from 'micromark';
|
||||
import { gfm, gfmHtml } from 'micromark-extension-gfm';
|
||||
import encrypt from 'browser-encrypt-attachment';
|
||||
import { encode } from 'blurhash';
|
||||
import { math } from 'micromark-extension-math';
|
||||
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
|
||||
import { getBlobSafeMimeType } from '../../util/mimetypes';
|
||||
import { sanitizeText } from '../../util/sanitize';
|
||||
import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
|
||||
import cons from './cons';
|
||||
import settings from './settings';
|
||||
import { htmlOutput, parser } from '../../util/markdown';
|
||||
|
||||
const blurhashField = 'xyz.amorgan.blurhash';
|
||||
const MXID_REGEX = /\B@\S+:\S+\.\S+[^.,:;?!\s]/g;
|
||||
const SHORTCODE_REGEX = /\B:([\w-]+):\B/g;
|
||||
|
||||
function encodeBlurhash(img) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 100;
|
||||
canvas.height = 100;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
const data = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
return encode(data.data, data.width, data.height, 4, 4);
|
||||
}
|
||||
|
||||
function loadImage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function getImageDimension(file) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = (err) => reject(err);
|
||||
img.src = url;
|
||||
img.onload = async () => {
|
||||
resolve({
|
||||
w: img.width,
|
||||
h: img.height,
|
||||
});
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function loadVideo(videoFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
@@ -57,12 +46,7 @@ function loadVideo(videoFile) {
|
||||
reader.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
if (videoFile.type === 'video/quicktime') {
|
||||
const quicktimeVideoFile = new File([videoFile], videoFile.name, { type: 'video/mp4' });
|
||||
reader.readAsDataURL(quicktimeVideoFile);
|
||||
} else {
|
||||
reader.readAsDataURL(videoFile);
|
||||
}
|
||||
reader.readAsDataURL(videoFile);
|
||||
});
|
||||
}
|
||||
function getVideoThumbnail(video, width, height, mimeType) {
|
||||
@@ -101,11 +85,14 @@ function getVideoThumbnail(video, width, height, mimeType) {
|
||||
}
|
||||
|
||||
function getFormattedBody(markdown) {
|
||||
let content = parser(markdown);
|
||||
if (content.length === 1 && content[0].type === 'paragraph') {
|
||||
content = content[0].content;
|
||||
}
|
||||
return htmlOutput(content);
|
||||
const result = micromark(markdown, {
|
||||
extensions: [gfm(), spoilerExtension(), math()],
|
||||
htmlExtensions: [gfmHtml(), spoilerExtensionHtml, mathExtensionHtml],
|
||||
});
|
||||
const bodyParts = result.match(/^(<p>)(.*)(<\/p>)$/);
|
||||
if (bodyParts === null) return result;
|
||||
if (bodyParts[2].indexOf('</p>') >= 0) return result;
|
||||
return bodyParts[2];
|
||||
}
|
||||
|
||||
function getReplyFormattedBody(roomId, reply) {
|
||||
@@ -128,46 +115,32 @@ function bindReplyToContent(roomId, reply, content) {
|
||||
return newContent;
|
||||
}
|
||||
|
||||
function findAndReplace(text, regex, filter, replace) {
|
||||
let copyText = text;
|
||||
Array.from(copyText.matchAll(regex))
|
||||
.filter(filter)
|
||||
.reverse() /* to replace backward to forward */
|
||||
.forEach((match) => {
|
||||
const matchText = match[0];
|
||||
const tag = replace(match);
|
||||
// Apply formatting to a plain text message
|
||||
//
|
||||
// This includes inserting any custom emoji that might be relevant, and (only if the
|
||||
// user has enabled it in their settings) formatting the message using markdown.
|
||||
function formatAndEmojifyText(room, text) {
|
||||
const allEmoji = getShortcodeToEmoji(room);
|
||||
|
||||
copyText = copyText.substr(0, match.index)
|
||||
+ tag
|
||||
+ copyText.substr(match.index + matchText.length);
|
||||
});
|
||||
return copyText;
|
||||
}
|
||||
// Start by applying markdown formatting (if relevant)
|
||||
let formattedText;
|
||||
if (settings.isMarkdown) {
|
||||
formattedText = getFormattedBody(text);
|
||||
} else {
|
||||
formattedText = text;
|
||||
}
|
||||
|
||||
function formatUserPill(room, text) {
|
||||
const { userIdsToDisplayNames } = room.currentState;
|
||||
return findAndReplace(
|
||||
text,
|
||||
MXID_REGEX,
|
||||
(match) => userIdsToDisplayNames[match[0]],
|
||||
(match) => (
|
||||
`<a href="https://matrix.to/#/${match[0]}">@${userIdsToDisplayNames[match[0]]}</a>`
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function formatEmoji(mx, room, roomList, text) {
|
||||
const parentIds = roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
const allEmoji = getShortcodeToEmoji(mx, [room, ...parentRooms]);
|
||||
|
||||
return findAndReplace(
|
||||
text,
|
||||
SHORTCODE_REGEX,
|
||||
(match) => allEmoji.has(match[1]),
|
||||
(match) => {
|
||||
const emoji = allEmoji.get(match[1]);
|
||||
// Check to see if there are any :shortcode-style-tags: in the message
|
||||
Array.from(formattedText.matchAll(/\B:([\w-]+):\B/g))
|
||||
// Then filter to only the ones corresponding to a valid emoji
|
||||
.filter((match) => allEmoji.has(match[1]))
|
||||
// Reversing the array ensures that indices are preserved as we start replacing
|
||||
.reverse()
|
||||
// Replace each :shortcode: with an <img/> tag
|
||||
.forEach((shortcodeMatch) => {
|
||||
const emoji = allEmoji.get(shortcodeMatch[1]);
|
||||
|
||||
// Render the tag that will replace the shortcode
|
||||
let tag;
|
||||
if (emoji.mxc) {
|
||||
tag = `<img data-mx-emoticon="" src="${
|
||||
@@ -180,17 +153,21 @@ function formatEmoji(mx, room, roomList, text) {
|
||||
} else {
|
||||
tag = emoji.unicode;
|
||||
}
|
||||
return tag;
|
||||
},
|
||||
);
|
||||
|
||||
// Splice the tag into the text
|
||||
formattedText = formattedText.substr(0, shortcodeMatch.index)
|
||||
+ tag
|
||||
+ formattedText.substr(shortcodeMatch.index + shortcodeMatch[0].length);
|
||||
});
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
|
||||
class RoomsInput extends EventEmitter {
|
||||
constructor(mx, roomList) {
|
||||
constructor(mx) {
|
||||
super();
|
||||
|
||||
this.matrixClient = mx;
|
||||
this.roomList = roomList;
|
||||
this.roomIdToInput = new Map();
|
||||
}
|
||||
|
||||
@@ -274,8 +251,7 @@ class RoomsInput extends EventEmitter {
|
||||
return this.roomIdToInput.get(roomId)?.isSending || false;
|
||||
}
|
||||
|
||||
async sendInput(roomId, msgType) {
|
||||
const room = this.matrixClient.getRoom(roomId);
|
||||
async sendInput(roomId) {
|
||||
const input = this.getInput(roomId);
|
||||
input.isSending = true;
|
||||
this.roomIdToInput.set(roomId, input);
|
||||
@@ -285,27 +261,17 @@ class RoomsInput extends EventEmitter {
|
||||
}
|
||||
|
||||
if (this.getMessage(roomId).trim() !== '') {
|
||||
const rawMessage = input.message;
|
||||
let content = {
|
||||
body: rawMessage,
|
||||
msgtype: msgType ?? 'm.text',
|
||||
body: input.message,
|
||||
msgtype: 'm.text',
|
||||
};
|
||||
|
||||
// Apply formatting if relevant
|
||||
let formattedBody = settings.isMarkdown
|
||||
? getFormattedBody(rawMessage)
|
||||
: sanitizeText(rawMessage);
|
||||
|
||||
formattedBody = formatUserPill(room, formattedBody);
|
||||
formattedBody = formatEmoji(this.matrixClient, room, this.roomList, formattedBody);
|
||||
|
||||
content.body = findAndReplace(
|
||||
content.body,
|
||||
MXID_REGEX,
|
||||
(match) => room.currentState.userIdsToDisplayNames[match[0]],
|
||||
(match) => `@${room.currentState.userIdsToDisplayNames[match[0]]}`,
|
||||
const formattedBody = formatAndEmojifyText(
|
||||
this.matrixClient.getRoom(roomId),
|
||||
input.message,
|
||||
);
|
||||
if (formattedBody !== sanitizeText(rawMessage)) {
|
||||
if (formattedBody !== input.message) {
|
||||
// Formatting was applied, and we need to switch to custom HTML
|
||||
content.format = 'org.matrix.custom.html';
|
||||
content.formatted_body = formattedBody;
|
||||
@@ -321,36 +287,8 @@ class RoomsInput extends EventEmitter {
|
||||
this.emit(cons.events.roomsInput.MESSAGE_SENT, roomId);
|
||||
}
|
||||
|
||||
async sendSticker(roomId, data) {
|
||||
const { mxc: url, body, httpUrl } = data;
|
||||
const info = {};
|
||||
|
||||
const img = new Image();
|
||||
img.src = httpUrl;
|
||||
|
||||
try {
|
||||
const res = await fetch(httpUrl);
|
||||
const blob = await res.blob();
|
||||
info.w = img.width;
|
||||
info.h = img.height;
|
||||
info.mimetype = blob.type;
|
||||
info.size = blob.size;
|
||||
info.thumbnail_info = { ...info };
|
||||
info.thumbnail_url = url;
|
||||
} catch {
|
||||
// send sticker without info
|
||||
}
|
||||
|
||||
this.matrixClient.sendEvent(roomId, 'm.sticker', {
|
||||
body,
|
||||
url,
|
||||
info,
|
||||
});
|
||||
this.emit(cons.events.roomsInput.MESSAGE_SENT, roomId);
|
||||
}
|
||||
|
||||
async sendFile(roomId, file) {
|
||||
const fileType = getBlobSafeMimeType(file.type).slice(0, file.type.indexOf('/'));
|
||||
const fileType = file.type.slice(0, file.type.indexOf('/'));
|
||||
const info = {
|
||||
mimetype: file.type,
|
||||
size: file.size,
|
||||
@@ -359,11 +297,10 @@ class RoomsInput extends EventEmitter {
|
||||
let uploadData = null;
|
||||
|
||||
if (fileType === 'image') {
|
||||
const img = await loadImage(URL.createObjectURL(file));
|
||||
const imgDimension = await getImageDimension(file);
|
||||
|
||||
info.w = img.width;
|
||||
info.h = img.height;
|
||||
info[blurhashField] = encodeBlurhash(img);
|
||||
info.w = imgDimension.w;
|
||||
info.h = imgDimension.h;
|
||||
|
||||
content.msgtype = 'm.image';
|
||||
content.body = file.name || 'Image';
|
||||
@@ -373,11 +310,8 @@ class RoomsInput extends EventEmitter {
|
||||
|
||||
try {
|
||||
const video = await loadVideo(file);
|
||||
|
||||
info.w = video.videoWidth;
|
||||
info.h = video.videoHeight;
|
||||
info[blurhashField] = encodeBlurhash(video);
|
||||
|
||||
const thumbnailData = await getVideoThumbnail(video, video.videoWidth, video.videoHeight, 'image/jpeg');
|
||||
const thumbnailUploadData = await this.uploadFile(roomId, thumbnailData.thumbnail);
|
||||
info.thumbnail_info = thumbnailData.info;
|
||||
@@ -456,17 +390,14 @@ class RoomsInput extends EventEmitter {
|
||||
}
|
||||
|
||||
async sendEditedMessage(roomId, mEvent, editedBody) {
|
||||
const room = this.matrixClient.getRoom(roomId);
|
||||
const isReply = typeof mEvent.getWireContent()['m.relates_to']?.['m.in_reply_to'] !== 'undefined';
|
||||
|
||||
const msgtype = mEvent.getWireContent().msgtype ?? 'm.text';
|
||||
|
||||
const content = {
|
||||
body: ` * ${editedBody}`,
|
||||
msgtype,
|
||||
msgtype: 'm.text',
|
||||
'm.new_content': {
|
||||
body: editedBody,
|
||||
msgtype,
|
||||
msgtype: 'm.text',
|
||||
},
|
||||
'm.relates_to': {
|
||||
event_id: mEvent.getId(),
|
||||
@@ -475,19 +406,11 @@ class RoomsInput extends EventEmitter {
|
||||
};
|
||||
|
||||
// Apply formatting if relevant
|
||||
let formattedBody = settings.isMarkdown
|
||||
? getFormattedBody(editedBody)
|
||||
: sanitizeText(editedBody);
|
||||
formattedBody = formatUserPill(room, formattedBody);
|
||||
formattedBody = formatEmoji(this.matrixClient, room, this.roomList, formattedBody);
|
||||
|
||||
content.body = findAndReplace(
|
||||
content.body,
|
||||
MXID_REGEX,
|
||||
(match) => room.currentState.userIdsToDisplayNames[match[0]],
|
||||
(match) => `@${room.currentState.userIdsToDisplayNames[match[0]]}`,
|
||||
const formattedBody = formatAndEmojifyText(
|
||||
this.matrixClient.getRoom(roomId),
|
||||
editedBody,
|
||||
);
|
||||
if (formattedBody !== sanitizeText(editedBody)) {
|
||||
if (formattedBody !== editedBody) {
|
||||
content.formatted_body = ` * ${formattedBody}`;
|
||||
content.format = 'org.matrix.custom.html';
|
||||
content['m.new_content'].formatted_body = formattedBody;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const cons = {
|
||||
version: '2.1.3',
|
||||
version: '2.0.4',
|
||||
secretKey: {
|
||||
ACCESS_TOKEN: 'cinny_access_token',
|
||||
DEVICE_ID: 'cinny_device_id',
|
||||
|
||||
+32
-236
@@ -5,8 +5,6 @@ import cons from './cons';
|
||||
class Navigation extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
// this will attached by initMatrix
|
||||
this.initMatrix = {};
|
||||
|
||||
this.selectedTab = cons.tabs.HOME;
|
||||
this.selectedSpaceId = null;
|
||||
@@ -16,20 +14,14 @@ class Navigation extends EventEmitter {
|
||||
this.isRoomSettings = false;
|
||||
this.recentRooms = [];
|
||||
|
||||
this.spaceToRoom = new Map();
|
||||
|
||||
this.rawModelStack = [];
|
||||
}
|
||||
|
||||
_addToSpacePath(roomId, asRoot) {
|
||||
if (typeof roomId !== 'string') {
|
||||
_setSpacePath(roomId) {
|
||||
if (roomId === null || roomId === cons.tabs.HOME) {
|
||||
this.selectedSpacePath = [cons.tabs.HOME];
|
||||
return;
|
||||
}
|
||||
if (asRoot) {
|
||||
this.selectedSpacePath = [roomId];
|
||||
return;
|
||||
}
|
||||
if (this.selectedSpacePath.includes(roomId)) {
|
||||
const spIndex = this.selectedSpacePath.indexOf(roomId);
|
||||
this.selectedSpacePath = this.selectedSpacePath.slice(0, spIndex + 1);
|
||||
@@ -38,223 +30,6 @@ class Navigation extends EventEmitter {
|
||||
this.selectedSpacePath.push(roomId);
|
||||
}
|
||||
|
||||
_mapRoomToSpace(roomId) {
|
||||
const { roomList, accountData } = this.initMatrix;
|
||||
if (
|
||||
this.selectedTab === cons.tabs.HOME
|
||||
&& roomList.rooms.has(roomId)
|
||||
&& !roomList.roomIdToParents.has(roomId)
|
||||
) {
|
||||
this.spaceToRoom.set(cons.tabs.HOME, {
|
||||
roomId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.selectedTab === cons.tabs.DIRECTS && roomList.directs.has(roomId)) {
|
||||
this.spaceToRoom.set(cons.tabs.DIRECTS, {
|
||||
roomId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parents = roomList.roomIdToParents.get(roomId);
|
||||
if (!parents) return;
|
||||
if (parents.has(this.selectedSpaceId)) {
|
||||
this.spaceToRoom.set(this.selectedSpaceId, {
|
||||
roomId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else if (accountData.categorizedSpaces.has(this.selectedSpaceId)) {
|
||||
const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]);
|
||||
const parent = [...parents].find((pId) => categories.has(pId));
|
||||
if (parent) {
|
||||
this.spaceToRoom.set(parent, {
|
||||
roomId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectRoom(roomId, eventId) {
|
||||
const prevSelectedRoomId = this.selectedRoomId;
|
||||
this.selectedRoomId = roomId;
|
||||
if (prevSelectedRoomId !== roomId) this._mapRoomToSpace(roomId);
|
||||
this.removeRecentRoom(prevSelectedRoomId);
|
||||
this.addRecentRoom(prevSelectedRoomId);
|
||||
this.removeRecentRoom(this.selectedRoomId);
|
||||
if (this.isRoomSettings && typeof this.selectedRoomId === 'string') {
|
||||
this.isRoomSettings = !this.isRoomSettings;
|
||||
this.emit(cons.events.navigation.ROOM_SETTINGS_TOGGLED, this.isRoomSettings);
|
||||
}
|
||||
this.emit(
|
||||
cons.events.navigation.ROOM_SELECTED,
|
||||
this.selectedRoomId,
|
||||
prevSelectedRoomId,
|
||||
eventId,
|
||||
);
|
||||
}
|
||||
|
||||
_selectTabWithRoom(roomId) {
|
||||
const { roomList, accountData } = this.initMatrix;
|
||||
const { categorizedSpaces } = accountData;
|
||||
|
||||
if (roomList.isOrphan(roomId)) {
|
||||
if (roomList.directs.has(roomId)) {
|
||||
this._selectSpace(null, true, false);
|
||||
this._selectTab(cons.tabs.DIRECTS, false);
|
||||
return;
|
||||
}
|
||||
this._selectSpace(null, true, false);
|
||||
this._selectTab(cons.tabs.HOME, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const parents = roomList.roomIdToParents.get(roomId);
|
||||
|
||||
if (parents.has(this.selectedSpaceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (categorizedSpaces.has(this.selectedSpaceId)) {
|
||||
const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]);
|
||||
if ([...parents].find((pId) => categories.has(pId))) {
|
||||
// No need to select tab
|
||||
// As one of parent is child of selected categorized space.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const spaceInPath = [...this.selectedSpacePath].reverse().find((sId) => parents.has(sId));
|
||||
if (spaceInPath) {
|
||||
this._selectSpace(spaceInPath, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomList.directs.has(roomId)) {
|
||||
this._selectSpace(null, true, false);
|
||||
this._selectTab(cons.tabs.DIRECTS, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parents.size > 0) {
|
||||
const sortedParents = [...parents].sort((p1, p2) => {
|
||||
const t1 = this.spaceToRoom.get(p1)?.timestamp ?? 0;
|
||||
const t2 = this.spaceToRoom.get(p2)?.timestamp ?? 0;
|
||||
return t2 - t1;
|
||||
});
|
||||
this._selectSpace(sortedParents[0], true, false);
|
||||
this._selectTab(sortedParents[0], false);
|
||||
}
|
||||
}
|
||||
|
||||
_getLatestActiveRoomId(roomIds) {
|
||||
const mx = this.initMatrix.matrixClient;
|
||||
|
||||
let ts = 0;
|
||||
let roomId = null;
|
||||
roomIds.forEach((childId) => {
|
||||
const room = mx.getRoom(childId);
|
||||
if (!room) return;
|
||||
const newTs = room.getLastActiveTimestamp();
|
||||
if (newTs > ts) {
|
||||
ts = newTs;
|
||||
roomId = childId;
|
||||
}
|
||||
});
|
||||
return roomId;
|
||||
}
|
||||
|
||||
_getLatestSelectedRoomId(spaceIds) {
|
||||
let ts = 0;
|
||||
let roomId = null;
|
||||
|
||||
spaceIds.forEach((sId) => {
|
||||
const data = this.spaceToRoom.get(sId);
|
||||
if (!data) return;
|
||||
const newTs = data.timestamp;
|
||||
if (newTs > ts) {
|
||||
ts = newTs;
|
||||
roomId = data.roomId;
|
||||
}
|
||||
});
|
||||
return roomId;
|
||||
}
|
||||
|
||||
_selectTab(tabId, selectRoom = true) {
|
||||
this.selectedTab = tabId;
|
||||
if (selectRoom) this._selectRoomWithTab(this.selectedTab);
|
||||
this.emit(cons.events.navigation.TAB_SELECTED, this.selectedTab);
|
||||
}
|
||||
|
||||
_selectSpace(roomId, asRoot, selectRoom = true) {
|
||||
this._addToSpacePath(roomId, asRoot);
|
||||
this.selectedSpaceId = roomId;
|
||||
if (!asRoot && selectRoom) this._selectRoomWithSpace(this.selectedSpaceId);
|
||||
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
||||
}
|
||||
|
||||
_selectRoomWithSpace(spaceId) {
|
||||
if (!spaceId) return;
|
||||
const { roomList, accountData, matrixClient } = this.initMatrix;
|
||||
const { categorizedSpaces } = accountData;
|
||||
|
||||
const data = this.spaceToRoom.get(spaceId);
|
||||
if (data && !categorizedSpaces.has(spaceId)) {
|
||||
this._selectRoom(data.roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const children = [];
|
||||
|
||||
if (categorizedSpaces.has(spaceId)) {
|
||||
const categories = roomList.getCategorizedSpaces([spaceId]);
|
||||
|
||||
const latestSelectedRoom = this._getLatestSelectedRoomId([...categories.keys()]);
|
||||
|
||||
if (latestSelectedRoom) {
|
||||
this._selectRoom(latestSelectedRoom);
|
||||
return;
|
||||
}
|
||||
|
||||
categories?.forEach((categoryId) => {
|
||||
categoryId?.forEach((childId) => {
|
||||
children.push(childId);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
roomList.getSpaceChildren(spaceId).forEach((id) => {
|
||||
if (matrixClient.getRoom(id)?.isSpaceRoom() === false) {
|
||||
children.push(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!children) {
|
||||
this._selectRoom(null);
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectRoom(this._getLatestActiveRoomId(children));
|
||||
}
|
||||
|
||||
_selectRoomWithTab(tabId) {
|
||||
const { roomList } = this.initMatrix;
|
||||
if (tabId === cons.tabs.HOME || tabId === cons.tabs.DIRECTS) {
|
||||
const data = this.spaceToRoom.get(tabId);
|
||||
if (data) {
|
||||
this._selectRoom(data.roomId);
|
||||
return;
|
||||
}
|
||||
const children = tabId === cons.tabs.HOME ? roomList.getOrphanRooms() : [...roomList.directs];
|
||||
this._selectRoom(this._getLatestActiveRoomId(children));
|
||||
return;
|
||||
}
|
||||
this._selectRoomWithSpace(tabId);
|
||||
}
|
||||
|
||||
removeRecentRoom(roomId) {
|
||||
if (typeof roomId !== 'string') return;
|
||||
const roomIdIndex = this.recentRooms.indexOf(roomId);
|
||||
@@ -284,19 +59,40 @@ class Navigation extends EventEmitter {
|
||||
navigate(action) {
|
||||
const actions = {
|
||||
[cons.actions.navigation.SELECT_TAB]: () => {
|
||||
const roomId = (
|
||||
action.tabId !== cons.tabs.HOME && action.tabId !== cons.tabs.DIRECTS
|
||||
) ? action.tabId : null;
|
||||
|
||||
this._selectSpace(roomId, true);
|
||||
this._selectTab(action.tabId);
|
||||
this.selectedTab = action.tabId;
|
||||
if (this.selectedTab !== cons.tabs.DIRECTS) {
|
||||
if (this.selectedTab === cons.tabs.HOME) {
|
||||
this.selectedSpacePath = [cons.tabs.HOME];
|
||||
this.selectedSpaceId = null;
|
||||
} else {
|
||||
this.selectedSpacePath = [this.selectedTab];
|
||||
this.selectedSpaceId = this.selectedTab;
|
||||
}
|
||||
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
||||
} else this.selectedSpaceId = null;
|
||||
this.emit(cons.events.navigation.TAB_SELECTED, this.selectedTab);
|
||||
},
|
||||
[cons.actions.navigation.SELECT_SPACE]: () => {
|
||||
this._selectSpace(action.roomId, false);
|
||||
this._setSpacePath(action.roomId);
|
||||
this.selectedSpaceId = action.roomId === cons.tabs.HOME ? null : action.roomId;
|
||||
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
||||
},
|
||||
[cons.actions.navigation.SELECT_ROOM]: () => {
|
||||
if (action.roomId) this._selectTabWithRoom(action.roomId);
|
||||
this._selectRoom(action.roomId, action.eventId);
|
||||
const prevSelectedRoomId = this.selectedRoomId;
|
||||
this.selectedRoomId = action.roomId;
|
||||
this.removeRecentRoom(prevSelectedRoomId);
|
||||
this.addRecentRoom(prevSelectedRoomId);
|
||||
this.removeRecentRoom(this.selectedRoomId);
|
||||
if (this.isRoomSettings && typeof this.selectedRoomId === 'string') {
|
||||
this.isRoomSettings = !this.isRoomSettings;
|
||||
this.emit(cons.events.navigation.ROOM_SETTINGS_TOGGLED, this.isRoomSettings);
|
||||
}
|
||||
this.emit(
|
||||
cons.events.navigation.ROOM_SELECTED,
|
||||
this.selectedRoomId,
|
||||
prevSelectedRoomId,
|
||||
action.eventId,
|
||||
);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_SPACE_SETTINGS]: () => {
|
||||
this.emit(cons.events.navigation.SPACE_SETTINGS_OPENED, action.roomId, action.tabText);
|
||||
|
||||
@@ -48,43 +48,31 @@ class Settings extends EventEmitter {
|
||||
return this.themes[this.themeIndex];
|
||||
}
|
||||
|
||||
_clearTheme() {
|
||||
document.body.classList.remove('system-theme');
|
||||
setTheme(themeIndex) {
|
||||
const appBody = document.getElementById('appBody');
|
||||
|
||||
appBody.classList.remove('system-theme');
|
||||
this.themes.forEach((themeName) => {
|
||||
if (themeName === '') return;
|
||||
document.body.classList.remove(themeName);
|
||||
appBody.classList.remove(themeName);
|
||||
});
|
||||
}
|
||||
|
||||
applyTheme() {
|
||||
this._clearTheme();
|
||||
// If use system theme is enabled
|
||||
// we will override current theme choice with system theme
|
||||
if (this.useSystemTheme) {
|
||||
document.body.classList.add('system-theme');
|
||||
} else if (this.themes[this.themeIndex]) {
|
||||
document.body.classList.add(this.themes[this.themeIndex]);
|
||||
appBody.classList.add('system-theme');
|
||||
} else if (this.themes[themeIndex] !== '') {
|
||||
appBody.classList.add(this.themes[themeIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(themeIndex) {
|
||||
setSettings('themeIndex', themeIndex);
|
||||
this.themeIndex = themeIndex;
|
||||
setSettings('themeIndex', this.themeIndex);
|
||||
this.applyTheme();
|
||||
}
|
||||
|
||||
toggleUseSystemTheme() {
|
||||
this.useSystemTheme = !this.useSystemTheme;
|
||||
setSettings('useSystemTheme', this.useSystemTheme);
|
||||
this.applyTheme();
|
||||
|
||||
this.emit(cons.events.settings.SYSTEM_THEME_TOGGLED, this.useSystemTheme);
|
||||
}
|
||||
|
||||
getUseSystemTheme() {
|
||||
if (typeof this.useSystemTheme === 'boolean') return this.useSystemTheme;
|
||||
|
||||
const settings = getSettings();
|
||||
if (settings === null) return true;
|
||||
if (typeof settings.useSystemTheme === 'undefined') return true;
|
||||
if (settings === null) return false;
|
||||
if (typeof settings.useSystemTheme === 'undefined') return false;
|
||||
return settings.useSystemTheme;
|
||||
}
|
||||
|
||||
@@ -150,7 +138,12 @@ class Settings extends EventEmitter {
|
||||
setter(action) {
|
||||
const actions = {
|
||||
[cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
|
||||
this.toggleUseSystemTheme();
|
||||
this.useSystemTheme = !this.useSystemTheme;
|
||||
|
||||
setSettings('useSystemTheme', this.useSystemTheme);
|
||||
this.setTheme(this.themeIndex);
|
||||
|
||||
this.emit(cons.events.settings.SYSTEM_THEME_TOGGLED, this.useSystemTheme);
|
||||
},
|
||||
[cons.actions.settings.TOGGLE_MARKDOWN]: () => {
|
||||
this.isMarkdown = !this.isMarkdown;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import settings from './client/state/settings';
|
||||
|
||||
import App from './app/pages/App';
|
||||
|
||||
settings.applyTheme();
|
||||
settings.setTheme(settings.getThemeIndex());
|
||||
|
||||
ReactDom.render(
|
||||
<App />,
|
||||
|
||||
+2
-3
@@ -105,7 +105,7 @@
|
||||
|
||||
/* shadow and overlay */
|
||||
--bg-overlay: rgba(0, 0, 0, 20%);
|
||||
--bg-overlay-low: rgba(0, 0, 0, 50%);
|
||||
--bg-overlay-low: rgba(0, 0, 0, 85%);
|
||||
|
||||
--bs-popup: 0 0 16px rgba(0, 0, 0, 10%);
|
||||
|
||||
@@ -264,8 +264,7 @@
|
||||
}
|
||||
|
||||
/* shadow and overlay */
|
||||
--bg-overlay: rgba(0, 0, 0, 60%);
|
||||
--bg-overlay-low: rgba(0, 0, 0, 80%);
|
||||
--bg-overlay: rgba(0, 0, 0, 50%);
|
||||
|
||||
--bs-popup: 0 0 16px rgba(0, 0, 0, 25%);
|
||||
|
||||
|
||||
@@ -132,65 +132,3 @@ export function copyToClipboard(text) {
|
||||
copyInput.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function suffixRename(name, validator) {
|
||||
let suffix = 2;
|
||||
let newName = name;
|
||||
do {
|
||||
newName = name + suffix;
|
||||
suffix += 1;
|
||||
} while (validator(newName));
|
||||
|
||||
return newName;
|
||||
}
|
||||
|
||||
export function getImageDimension(file) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
resolve({
|
||||
w: img.width,
|
||||
h: img.height,
|
||||
});
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export function scaleDownImage(imageFile, width, height) {
|
||||
return new Promise((resolve) => {
|
||||
const imgURL = URL.createObjectURL(imageFile);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
let newWidth = img.width;
|
||||
let newHeight = img.height;
|
||||
if (newHeight <= height && newWidth <= width) {
|
||||
resolve(imageFile);
|
||||
}
|
||||
|
||||
if (newHeight > height) {
|
||||
newWidth = Math.floor(newWidth * (height / newHeight));
|
||||
newHeight = height;
|
||||
}
|
||||
if (newWidth > width) {
|
||||
newHeight = Math.floor(newHeight * (width / newWidth));
|
||||
newWidth = width;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = newWidth;
|
||||
canvas.height = newHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, newWidth, newHeight);
|
||||
|
||||
canvas.toBlob((thumbnail) => {
|
||||
URL.revokeObjectURL(imgURL);
|
||||
resolve(thumbnail);
|
||||
}, imageFile.type);
|
||||
};
|
||||
|
||||
img.src = imgURL;
|
||||
});
|
||||
}
|
||||
+187
-78
@@ -1,89 +1,198 @@
|
||||
import SimpleMarkdown from '@khanacademy/simple-markdown';
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable no-plusplus */
|
||||
/* eslint-disable no-continue */
|
||||
|
||||
const {
|
||||
defaultRules, parserFor, outputFor, anyScopeRegex, blockRegex, inlineRegex, htmlTag, sanitizeText,
|
||||
} = SimpleMarkdown;
|
||||
import { codes } from 'micromark-util-symbol/codes';
|
||||
import { types } from 'micromark-util-symbol/types';
|
||||
import { resolveAll } from 'micromark-util-resolve-all';
|
||||
import { splice } from 'micromark-util-chunked';
|
||||
|
||||
function mathHtml(wrap, node) {
|
||||
return htmlTag(wrap, htmlTag('code', sanitizeText(node.content)), { 'data-mx-maths': node.content });
|
||||
function inlineExtension(marker, len, key) {
|
||||
const keySeq = `${key}Sequence`;
|
||||
const keySeqTmp = `${keySeq}Temporary`;
|
||||
|
||||
return () => {
|
||||
function tokenize(effects, ok, nok) {
|
||||
const { previous, events } = this;
|
||||
|
||||
let size = 0;
|
||||
|
||||
function more(code) {
|
||||
// consume more markers if the maximum length hasn't been reached yet
|
||||
if (code === marker && size < len) {
|
||||
effects.consume(code);
|
||||
size += 1;
|
||||
return more;
|
||||
}
|
||||
|
||||
// check for minimum length
|
||||
if (size < len) return nok(code);
|
||||
|
||||
effects.exit(keySeqTmp);
|
||||
return ok(code);
|
||||
}
|
||||
|
||||
function start(code) {
|
||||
// ignore code if it's not a marker
|
||||
if (code !== marker) return nok(code);
|
||||
|
||||
if (previous === marker
|
||||
&& events[events.length - 1][1].type !== types.characterEscape) return nok(code);
|
||||
|
||||
effects.enter(keySeqTmp);
|
||||
return more(code);
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
function resolve(events, context) {
|
||||
let i = -1;
|
||||
|
||||
while (++i < events.length) {
|
||||
if (events[i][0] !== 'enter' || events[i][1].type !== keySeqTmp) continue;
|
||||
|
||||
let open = i;
|
||||
while (open--) {
|
||||
if (events[open][0] !== 'exit' || events[open][1].type !== keySeqTmp) continue;
|
||||
|
||||
events[i][1].type = keySeq;
|
||||
events[open][1].type = keySeq;
|
||||
|
||||
const border = {
|
||||
type: key,
|
||||
start: { ...events[open][1].start },
|
||||
end: { ...events[i][1].end },
|
||||
};
|
||||
|
||||
const text = {
|
||||
type: `${key}Text`,
|
||||
start: { ...events[open][1].end },
|
||||
end: { ...events[i][1].start },
|
||||
};
|
||||
|
||||
const nextEvents = [
|
||||
['enter', border, context],
|
||||
['enter', events[open][1], context],
|
||||
['exit', events[open][1], context],
|
||||
['enter', text, context],
|
||||
];
|
||||
|
||||
splice(
|
||||
nextEvents,
|
||||
nextEvents.length,
|
||||
0,
|
||||
resolveAll(
|
||||
context.parser.constructs.insideSpan.null,
|
||||
events.slice(open + 1, i),
|
||||
context,
|
||||
),
|
||||
);
|
||||
|
||||
splice(nextEvents, nextEvents.length, 0, [
|
||||
['exit', text, context],
|
||||
['enter', events[i][1], context],
|
||||
['exit', events[i][1], context],
|
||||
['exit', border, context],
|
||||
]);
|
||||
|
||||
splice(events, open - 1, i - open + 3, nextEvents);
|
||||
|
||||
i = open + nextEvents.length - 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
events.forEach((event) => {
|
||||
if (event[1].type === keySeqTmp) {
|
||||
event[1].type = types.data;
|
||||
}
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
const tokenizer = {
|
||||
tokenize,
|
||||
resolveAll: resolve,
|
||||
};
|
||||
|
||||
return {
|
||||
text: { [marker]: tokenizer },
|
||||
insideSpan: { null: [tokenizer] },
|
||||
attentionMarkers: { null: [marker] },
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const rules = {
|
||||
...defaultRules,
|
||||
Array: {
|
||||
...defaultRules.Array,
|
||||
plain: (arr, output, state) => arr.map((node) => output(node, state)).join(''),
|
||||
const spoilerExtension = inlineExtension(codes.verticalBar, 2, 'spoiler');
|
||||
|
||||
const spoilerExtensionHtml = {
|
||||
enter: {
|
||||
spoiler() {
|
||||
this.tag('<span data-mx-spoiler>');
|
||||
},
|
||||
},
|
||||
displayMath: {
|
||||
order: defaultRules.list.order + 0.5,
|
||||
match: blockRegex(/^\$\$\n*([\s\S]+?)\n*\$\$/),
|
||||
parse: (capture) => ({ content: capture[1] }),
|
||||
plain: (node) => `$$\n${node.content}\n$$`,
|
||||
html: (node) => mathHtml('div', node),
|
||||
},
|
||||
newline: {
|
||||
...defaultRules.newline,
|
||||
plain: () => '\n',
|
||||
},
|
||||
paragraph: {
|
||||
...defaultRules.paragraph,
|
||||
plain: (node, output, state) => `${output(node.content, state)}\n\n`,
|
||||
html: (node, output, state) => htmlTag('p', output(node.content, state)),
|
||||
},
|
||||
escape: {
|
||||
...defaultRules.escape,
|
||||
plain: (node, output, state) => `\\${output(node.content, state)}`,
|
||||
},
|
||||
em: {
|
||||
...defaultRules.em,
|
||||
plain: (node, output, state) => `_${output(node.content, state)}_`,
|
||||
},
|
||||
strong: {
|
||||
...defaultRules.strong,
|
||||
plain: (node, output, state) => `**${output(node.content, state)}**`,
|
||||
},
|
||||
u: {
|
||||
...defaultRules.u,
|
||||
plain: (node, output, state) => `__${output(node.content, state)}__`,
|
||||
},
|
||||
del: {
|
||||
...defaultRules.del,
|
||||
plain: (node, output, state) => `~~${output(node.content, state)}~~`,
|
||||
},
|
||||
spoiler: {
|
||||
order: defaultRules.em.order - 0.5,
|
||||
match: inlineRegex(/^\|\|([\s\S]+?)\|\|(?:\(([\s\S]+?)\))?/),
|
||||
parse: (capture, parse, state) => ({
|
||||
content: parse(capture[1], state),
|
||||
reason: capture[2],
|
||||
}),
|
||||
plain: (node) => `[spoiler${node.reason ? `: ${node.reason}` : ''}](mxc://somewhere)`,
|
||||
html: (node, output, state) => `<span data-mx-spoiler${node.reason ? `="${sanitizeText(node.reason)}"` : ''}>${output(node.content, state)}</span>`,
|
||||
},
|
||||
inlineMath: {
|
||||
order: defaultRules.del.order + 0.5,
|
||||
match: inlineRegex(/^\$(\S[\s\S]+?\S|\S)\$(?!\d)/),
|
||||
parse: (capture) => ({ content: capture[1] }),
|
||||
plain: (node) => `$${node.content}$`,
|
||||
html: (node) => mathHtml('span', node),
|
||||
},
|
||||
br: {
|
||||
...defaultRules.br,
|
||||
match: anyScopeRegex(/^ *\n/),
|
||||
plain: () => '\n',
|
||||
},
|
||||
text: {
|
||||
...defaultRules.text,
|
||||
match: anyScopeRegex(/^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff]| *\n|\w+:\S|$)/),
|
||||
plain: (node) => node.content,
|
||||
exit: {
|
||||
spoiler() {
|
||||
this.tag('</span>');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const parser = parserFor(rules);
|
||||
|
||||
const plainOutput = outputFor(rules, 'plain');
|
||||
const htmlOutput = outputFor(rules, 'html');
|
||||
const mathExtensionHtml = {
|
||||
enter: {
|
||||
mathFlow() {
|
||||
this.lineEndingIfNeeded();
|
||||
},
|
||||
mathFlowFenceMeta() {
|
||||
this.buffer();
|
||||
},
|
||||
mathText() {
|
||||
this.buffer();
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
mathFlow() {
|
||||
const value = this.encode(this.resume().replace(/(?:\r?\n|\r)$/, ''));
|
||||
this.tag('<div data-mx-maths="');
|
||||
this.tag(value);
|
||||
this.tag('"><code>');
|
||||
this.raw(value);
|
||||
this.tag('</code></div>');
|
||||
this.setData('mathFlowOpen');
|
||||
this.setData('slurpOneLineEnding');
|
||||
},
|
||||
mathFlowFence() {
|
||||
// After the first fence.
|
||||
if (!this.getData('mathFlowOpen')) {
|
||||
this.setData('mathFlowOpen', true);
|
||||
this.setData('slurpOneLineEnding', true);
|
||||
this.buffer();
|
||||
}
|
||||
},
|
||||
mathFlowFenceMeta() {
|
||||
this.resume();
|
||||
},
|
||||
mathFlowValue(token) {
|
||||
this.raw(this.sliceSerialize(token));
|
||||
},
|
||||
mathText() {
|
||||
const value = this.encode(this.resume());
|
||||
this.tag('<span data-mx-maths="');
|
||||
this.tag(value);
|
||||
this.tag('"><code>');
|
||||
this.raw(value);
|
||||
this.tag('</code></span>');
|
||||
},
|
||||
mathTextData(token) {
|
||||
this.raw(this.sliceSerialize(token));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export {
|
||||
parser, plainOutput, htmlOutput,
|
||||
inlineExtension,
|
||||
spoilerExtension, spoilerExtensionHtml,
|
||||
mathExtensionHtml,
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export async function getBaseUrl(servername) {
|
||||
if (baseUrl === undefined) throw new Error();
|
||||
return baseUrl;
|
||||
} catch (e) {
|
||||
return `${protocol}${servername}`;
|
||||
throw new Error(`${protocol}${servername}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,10 +204,11 @@ export async function hasDevices(userId) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
try {
|
||||
const usersDeviceMap = await mx.downloadKeys([userId, mx.getUserId()]);
|
||||
return Object.values(usersDeviceMap)
|
||||
.every((userDevices) => (Object.keys(userDevices).length > 0));
|
||||
return Object.values(usersDeviceMap).every((userDevices) =>
|
||||
Object.keys(userDevices).length > 0,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Error determining if it's possible to encrypt to all users: ", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/cd15e08fc285da42134817cce50de8011809cd53/src/utils/blobs.ts
|
||||
export const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
'image/apng',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
|
||||
export function getBlobSafeMimeType(mimetype) {
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
// Required for Chromium browsers
|
||||
if (mimetype === 'video/quicktime') {
|
||||
return 'video/mp4';
|
||||
}
|
||||
return mimetype;
|
||||
}
|
||||
+6
-17
@@ -1,7 +1,7 @@
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import initMatrix from '../client/initMatrix';
|
||||
|
||||
const MAX_TAG_NESTING = 100;
|
||||
let mx = null;
|
||||
|
||||
const permittedHtmlTags = [
|
||||
'font', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
@@ -19,7 +19,7 @@ const permittedTagToAttributes = {
|
||||
div: ['data-mx-maths'],
|
||||
a: ['name', 'target', 'href', 'rel'],
|
||||
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
|
||||
ol: ['start'],
|
||||
o: ['start'],
|
||||
code: ['class'],
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ function transformATag(tagName, attribs) {
|
||||
'data-mx-pill': userId,
|
||||
},
|
||||
};
|
||||
if (userId === mx?.getUserId()) {
|
||||
if (userId === initMatrix.matrixClient.getUserId()) {
|
||||
pill.attribs['data-mx-ping'] = undefined;
|
||||
}
|
||||
return pill;
|
||||
@@ -76,28 +76,17 @@ function transformATag(tagName, attribs) {
|
||||
|
||||
function transformImgTag(tagName, attribs) {
|
||||
const { src } = attribs;
|
||||
if (src.startsWith('mxc://') === false) {
|
||||
return {
|
||||
tagName: 'a',
|
||||
attribs: {
|
||||
href: src,
|
||||
rel: 'noopener',
|
||||
target: '_blank',
|
||||
},
|
||||
text: attribs.alt || src,
|
||||
};
|
||||
}
|
||||
const mx = initMatrix.matrixClient;
|
||||
return {
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
src: mx?.mxcUrlToHttp(src),
|
||||
src: src.startsWith('mxc://') ? mx.mxcUrlToHttp(src) : src,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeCustomHtml(matrixClient, body) {
|
||||
mx = matrixClient;
|
||||
export function sanitizeCustomHtml(body) {
|
||||
return sanitizeHtml(body, {
|
||||
allowedTags: permittedHtmlTags,
|
||||
allowedAttributes: permittedTagToAttributes,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
|
||||
import linkifyHtml from 'linkify-html';
|
||||
import linkifyHtml from 'linkifyjs/html';
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
import { sanitizeText } from './sanitize';
|
||||
|
||||
Reference in new issue
Block a user