Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
085627cffd | ||
|
|
a41440857d | ||
|
|
3b7f71b573 | ||
|
|
0e79db7a3d | ||
|
|
78951a3bc4 | ||
|
|
44318d1ecd | ||
|
|
a6fb44e1ab | ||
|
|
374d2d1962 | ||
|
|
3ad143228d | ||
|
|
4ea14c853e | ||
|
|
38bbc1c6f5 | ||
|
|
9a34daa2bc | ||
|
|
e5e3f5f0a3 | ||
|
|
899a89cb3b | ||
|
|
63cb564818 | ||
|
|
e93511fde7 | ||
|
|
eeac5c6c45 | ||
|
|
f7b48333df | ||
|
|
cb88a6fd9c | ||
|
|
7b95546b80 | ||
|
|
5f9ea6c06a | ||
|
|
d54d1d964d | ||
|
|
868f271588 | ||
|
|
7606c9d325 | ||
|
|
3c66a6a1c1 | ||
|
|
bf900e7468 | ||
|
|
52cbcb9128 | ||
|
|
97e7ba25a6 | ||
|
|
bc9ff23372 | ||
|
|
c176ed8861 | ||
|
|
63c0653006 | ||
|
|
fbf899465d | ||
|
|
603e478794 | ||
|
|
347d23641b | ||
|
|
c4e973e83b | ||
|
|
d22abe300b | ||
|
|
2bab3119b0 | ||
|
|
ea3d7b8892 | ||
|
|
7eb2f78174 | ||
|
|
7db04440ff | ||
|
|
062beb718d | ||
|
|
b8f3cf1b47 | ||
|
|
c280c1339b | ||
|
|
543acc9fff | ||
|
|
55d609c82b | ||
|
|
f144c1cdbd | ||
|
|
1531535151 | ||
|
|
4fe4c8bb03 | ||
|
|
1302682900 | ||
|
|
62e0821be2 | ||
|
|
856e616247 | ||
|
|
71fdba3c4a | ||
|
|
7803bd5c06 | ||
|
|
8dc9e01439 | ||
|
|
1d6ce13a6c | ||
|
|
6a907b1542 | ||
|
|
bef759e358 | ||
|
|
63023c5266 | ||
|
|
b6f2f9998e | ||
|
|
6d9115f593 | ||
|
|
02f7d7c74d | ||
|
|
eeb4ec0e73 | ||
|
|
9efa05d4de | ||
|
|
72a8483a23 | ||
|
|
d4da49512c | ||
|
|
6437970f7f | ||
|
|
bf79618221 | ||
|
|
dbeac1f8eb | ||
|
|
1625b8a665 | ||
|
|
5ab202e5c9 | ||
|
|
2f4fbc9d49 | ||
|
|
613ffefeb3 | ||
|
|
89e93be091 | ||
|
|
9b42077f7a | ||
|
|
81b88477bd | ||
|
|
52376e10a5 | ||
|
|
b8b3f9e7ec | ||
|
|
194eb30042 | ||
|
|
b52aa9ac76 | ||
|
|
c825c41aed | ||
|
|
6d8f704542 | ||
|
|
10a399f7e6 | ||
|
|
d59a118e83 | ||
|
|
07570f9e8a | ||
|
|
90972e938e | ||
|
|
4297eb34c9 | ||
|
|
507f9c3c22 | ||
|
|
33551089ad | ||
|
|
c8301c6ad6 | ||
|
|
c3396780d7 | ||
|
|
8ea2b0b63b | ||
|
|
e170d02bff | ||
|
|
1f872a9b0c | ||
|
|
bcdc1d8915 | ||
|
|
f577435e98 | ||
|
|
bf94550a84 | ||
|
|
a900b04254 | ||
|
|
3fe5a9d222 | ||
|
|
a07a7676b7 | ||
|
|
593cc03962 | ||
|
|
820f255929 | ||
|
|
7db4146c8d | ||
|
|
91a6916f4c | ||
|
|
78374d0301 | ||
|
|
9185ee0cf9 | ||
|
|
e2de51832c | ||
|
|
759f16d5b9 | ||
|
|
d0ddedc2b2 | ||
|
|
4291005161 | ||
|
|
ee144ccb2b | ||
|
|
7fea21f221 | ||
|
|
9a4b40f242 | ||
|
|
c2d353b973 | ||
|
|
8b6cecdbf8 | ||
|
|
83d8f2821a | ||
|
|
0225204b4d | ||
|
|
478957803d | ||
|
|
0181cb26d2 | ||
|
|
2ddb3595c7 | ||
|
|
76e49d1bd0 | ||
|
|
91f1ee748e | ||
|
|
9bddf64510 | ||
|
|
2200ae143e | ||
|
|
80d9a2ca7d | ||
|
|
cbd1bf35c6 | ||
|
|
c1be57b205 | ||
|
|
4848bef0dd | ||
|
|
a8f374dd43 | ||
|
|
d43e41e1ba | ||
|
|
437ac4c59b | ||
|
|
de218409ab | ||
|
|
060ed859d4 | ||
|
|
0783c43169 | ||
|
|
b5a317e021 | ||
|
|
15c1f6dadf | ||
|
|
efda9991f2 | ||
|
|
9fb651a04e | ||
|
|
55c652a02a | ||
|
|
2be706fb3f | ||
|
|
3e2cc8bfae | ||
|
|
a6c339e13a | ||
|
|
b0174f3acc | ||
|
|
a343d9999e | ||
|
|
0a2cca6e40 | ||
|
|
bca00f46a9 | ||
|
|
5848c02d50 | ||
|
|
29ddcfa1f9 | ||
|
|
831bb83f4e | ||
|
|
b8a8babc88 | ||
|
|
4a35aa7272 | ||
|
|
2ded7d9b1f | ||
|
|
19f674cf5f | ||
|
|
790bf5eac3 | ||
|
|
e1adc6a3bc | ||
|
|
bcedab5113 | ||
|
|
adca914d67 | ||
|
|
8583ab19f0 | ||
|
|
1aa16a43ee | ||
|
|
e6661d3b0d | ||
|
|
678e0dc6ac | ||
|
|
bdc10fb729 | ||
|
|
584fa87bbd | ||
|
|
ac155bbf4c |
No files matched your search
@@ -1,5 +1,2 @@
|
||||
webpack.common.js
|
||||
webpack.dev.js
|
||||
webpack.prod.js
|
||||
experiment
|
||||
node_modules
|
||||
@@ -4,25 +4,56 @@ module.exports = {
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
'airbnb',
|
||||
'prettier',
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
'@typescript-eslint'
|
||||
],
|
||||
rules: {
|
||||
'linebreak-style': 0,
|
||||
'no-underscore-dangle': 0,
|
||||
|
||||
"import/prefer-default-export": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
|
||||
'react/no-unstable-nested-components': [
|
||||
'error',
|
||||
{ allowAsProps: true },
|
||||
],
|
||||
"react/jsx-filename-extension": [
|
||||
"error",
|
||||
{
|
||||
extensions: [".tsx", ".jsx"],
|
||||
},
|
||||
],
|
||||
|
||||
"react/require-default-props": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":dependencyDashboardApproval"
|
||||
],
|
||||
"labels": [ "Dependencies" ],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": [ "lockFileMaintenance" ]
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": { "enabled": true },
|
||||
"dependencyDashboard": true
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'Build pull request'
|
||||
name: Build pull request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -6,33 +6,33 @@ on:
|
||||
|
||||
jobs:
|
||||
build-pull-request:
|
||||
name: Build pull request
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PR_NUMBER: ${{github.event.number}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
node-version: 18.12.1
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build app
|
||||
run: npm ci && npm run build
|
||||
run: npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
uses: actions/upload-artifact@v3.1.1
|
||||
with:
|
||||
name: previewbuild
|
||||
name: preview
|
||||
path: dist
|
||||
retention-days: 1
|
||||
- name: Get PR info
|
||||
uses: actions/github-script@v6.2.0
|
||||
- name: Save pr number
|
||||
run: echo ${PR_NUMBER} > ./pr.txt
|
||||
- name: Upload pr number
|
||||
uses: actions/upload-artifact@v3.1.1
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
|
||||
- name: Upload PR Info
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: pr.json
|
||||
path: pr.json
|
||||
name: pr
|
||||
path: ./pr.txt
|
||||
retention-days: 1
|
||||
@@ -1,81 +1,56 @@
|
||||
name: Upload Preview Build to Netlify
|
||||
name: Deploy PR to Netlify
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build pull request"]
|
||||
types:
|
||||
- completed
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
get-build-and-deploy:
|
||||
deploy-pull-request:
|
||||
name: Deploy pull request
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
# There's a 'download artifact' action but it hasn't been updated for the
|
||||
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
|
||||
# so instead we get this mess:
|
||||
- name: Download pr number
|
||||
uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925
|
||||
with:
|
||||
workflow: ${{ github.event.workflow.id }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: pr
|
||||
- name: Output pr number
|
||||
id: pr
|
||||
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v6.2.0
|
||||
uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925
|
||||
with:
|
||||
script: |
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{github.event.workflow_run.id }},
|
||||
});
|
||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "previewbuild"
|
||||
})[0];
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data));
|
||||
var prInfoArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "pr.json"
|
||||
})[0];
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: prInfoArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
|
||||
- name: Extract Artifacts
|
||||
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
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
var pr = JSON.parse(fs.readFileSync('${{github.workspace}}/pr.json'));
|
||||
console.log(`::set-output name=prnumber::${pr.number}`);
|
||||
workflow: ${{ github.event.workflow.id }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: preview
|
||||
path: dist
|
||||
- name: Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
|
||||
uses: nwtgck/actions-netlify@5da65c9f74c7961c5501a3ba329b8d0912f39c03
|
||||
with:
|
||||
publish-dir: dist
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
||||
alias: ${{ steps.pr.outputs.id }}
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
enable-commit-comment: false
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
|
||||
timeout-minutes: 1
|
||||
- name: Edit PR Description
|
||||
uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409
|
||||
- name: Comment preview on PR
|
||||
uses: thollander/actions-comment-pull-request@c22fb302208b7b170d252a61a505d2ea27245eff
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
pull-request-number: ${{ steps.readctx.outputs.prnumber }}
|
||||
description-message: |
|
||||
pr_number: ${{ steps.pr.outputs.id }}
|
||||
comment_tag: ${{ steps.pr.outputs.id }}
|
||||
message: |
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
@@ -9,13 +9,11 @@ on:
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PR_NUMBER: ${{github.event.number}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -0,0 +1,26 @@
|
||||
name: NPM Lockfile Changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package-lock.json'
|
||||
|
||||
jobs:
|
||||
lockfile_changes:
|
||||
runs-on: ubuntu-latest
|
||||
# Permission overwrite is required for Dependabot PRs, see "Common issues" below.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: NPM Lockfile Changes
|
||||
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Optional inputs, can be deleted safely if you are happy with default values.
|
||||
collapsibleThreshold: 25
|
||||
failOnDowngrade: false
|
||||
path: package-lock.json
|
||||
updateComment: true
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'Deploy to Netlify (dev)'
|
||||
name: Deploy to Netlify (dev)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,23 +7,31 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy-to-netlify:
|
||||
name: 'Deploy'
|
||||
name: Deploy to Netlify
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
- name: Build and deploy to Netlify
|
||||
uses: jsmrcaga/action-netlify-deploy@53de32e559b0b3833615b9788c7a090cd2fddb03
|
||||
node-version: 18.12.1
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@5da65c9f74c7961c5501a3ba329b8d0912f39c03
|
||||
with:
|
||||
install_command: "npm ci"
|
||||
publish-dir: dist
|
||||
deploy-message: "Dev deploy ${{ github.sha }}"
|
||||
enable-commit-comment: false
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
production-deploy: true
|
||||
github-deployment-environment: nightly
|
||||
github-deployment-description: 'Nightly deployment on each commit to dev branch'
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE2_ID }}
|
||||
BUILD_DIRECTORY: "dist"
|
||||
NETLIFY_DEPLOY_MESSAGE: "Dev deploy v${{ github.ref }}"
|
||||
NETLIFY_DEPLOY_TO_PROD: true
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_DEV }}
|
||||
timeout-minutes: 1
|
||||
@@ -1,27 +1,42 @@
|
||||
name: 'Production deploy'
|
||||
name: Production deploy
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
create-release-tar:
|
||||
name: 'Create release tar'
|
||||
deploy-and-tarball:
|
||||
name: Netlify deploy and tarball
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
node-version: 18.12.1
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@5da65c9f74c7961c5501a3ba329b8d0912f39c03
|
||||
with:
|
||||
publish-dir: dist
|
||||
deploy-message: "Prod deploy ${{ github.ref_name }}"
|
||||
enable-commit-comment: false
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
production-deploy: true
|
||||
github-deployment-environment: stable
|
||||
github-deployment-description: 'Stable deployment on each release'
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
|
||||
timeout-minutes: 1
|
||||
- name: Get version from tag
|
||||
id: vars
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
|
||||
- name: Create tar.gz
|
||||
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
||||
- name: Sign tar.gz
|
||||
@@ -35,35 +50,13 @@ jobs:
|
||||
gpg --export | xxd -p
|
||||
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
- name: Upload tagged release
|
||||
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
|
||||
with:
|
||||
files: |
|
||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
|
||||
|
||||
deploy-to-netlify:
|
||||
name: 'Deploy to Netlify'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: 17.9.0
|
||||
- name: Build and deploy to Netlify
|
||||
uses: jsmrcaga/action-netlify-deploy@53de32e559b0b3833615b9788c7a090cd2fddb03
|
||||
with:
|
||||
install_command: "npm ci"
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
BUILD_DIRECTORY: "dist"
|
||||
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
|
||||
NETLIFY_DEPLOY_TO_PROD: true
|
||||
|
||||
push-to-dockerhub:
|
||||
publish-image:
|
||||
name: Push Docker image to Docker Hub, ghcr
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -71,31 +64,31 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to the Container registry
|
||||
uses: docker/login-action@v2.0.0
|
||||
uses: docker/login-action@v2.1.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
|
||||
uses: docker/metadata-action@v4.1.1
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||
ghcr.io/${{ github.repository }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -4,3 +4,4 @@ node_modules
|
||||
devAssets
|
||||
|
||||
.DS_Store
|
||||
android
|
||||
@@ -1 +1,3 @@
|
||||
legacy-peer-deps=true
|
||||
legacy-peer-deps=true
|
||||
save-exact=true
|
||||
@matrix-org:registry=https://gitlab.matrix.org/api/v4/projects/27/packages/npm/
|
||||
@@ -0,0 +1,6 @@
|
||||
dist
|
||||
node_modules
|
||||
package.json
|
||||
package-lock.json
|
||||
LICENSE
|
||||
README.md
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
@@ -41,4 +41,4 @@ Also, we use [ESLint](https://eslint.org/) for clean and stylistically consisten
|
||||
## 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)
|
||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||
@@ -1,16 +1,17 @@
|
||||
## Builder
|
||||
FROM node:17.9.0-alpine3.15 as builder
|
||||
FROM node:18.12.1-alpine3.15 as builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY .npmrc package.json package-lock.json /src/
|
||||
RUN npm ci
|
||||
COPY . /src/
|
||||
ENV NODE_OPTIONS=--max_old_space_size=4096
|
||||
RUN npm run build
|
||||
|
||||
|
||||
## App
|
||||
FROM nginx:1.23.1-alpine
|
||||
FROM nginx:1.23.3-alpine
|
||||
|
||||
COPY --from=builder /src/dist /app
|
||||
|
||||
|
||||
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
@@ -84,7 +84,7 @@ UeGsouhyuITLwEhScounZDqop+Dx
|
||||
|
||||
## Local development
|
||||
> 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.
|
||||
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 Hydrogen LTS (v18).
|
||||
|
||||
Execute the following commands to start a development server:
|
||||
```sh
|
||||
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,10 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'today.icarus.cinny',
|
||||
appName: 'Cinny',
|
||||
webDir: 'dist',
|
||||
bundledWebRuntime: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"defaultHomeserver": 3,
|
||||
"homeserverList": [
|
||||
"converser.eu",
|
||||
"envs.net",
|
||||
"halogen.city",
|
||||
"kde.org",
|
||||
"matrix.icarus.today",
|
||||
"matrix.org",
|
||||
"mozilla.org"
|
||||
],
|
||||
"allowCustomHomeservers": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cinny</title>
|
||||
<meta name="name" content="Cinny" />
|
||||
<meta name="author" content="Ajay Bura" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
|
||||
/>
|
||||
|
||||
<meta property="og:title" content="Cinny" />
|
||||
<meta property="og:url" content="https://cinny.in" />
|
||||
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="application-name" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="57x57"
|
||||
href="./public/res/apple/apple-touch-icon-57x57.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="60x60"
|
||||
href="./public/res/apple/apple-touch-icon-60x60.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="72x72"
|
||||
href="./public/res/apple/apple-touch-icon-72x72.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="./public/res/apple/apple-touch-icon-76x76.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="114x114"
|
||||
href="./public/res/apple/apple-touch-icon-114x114.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="120x120"
|
||||
href="./public/res/apple/apple-touch-icon-120x120.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="144x144"
|
||||
href="./public/res/apple/apple-touch-icon-144x144.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="./public/res/apple/apple-touch-icon-152x152.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="167x167"
|
||||
href="./public/res/apple/apple-touch-icon-167x167.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="./public/res/apple/apple-touch-icon-180x180.png"
|
||||
/>
|
||||
</head>
|
||||
<body id="appBody">
|
||||
<script>
|
||||
window.global ||= window;
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<audio id="notificationSound">
|
||||
<source src="./public/sound/notification.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
<audio id="inviteSound">
|
||||
<source src="./public/sound/invite.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
<script type="module" src="./src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,85 +1,87 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "2.1.3",
|
||||
"version": "2.2.6",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"npm": ">=6.14.8",
|
||||
"node": ">=14.15.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --config ./webpack.dev.js --open",
|
||||
"build": "webpack --config ./webpack.prod.js"
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "yarn check:eslint && yarn check:prettier",
|
||||
"check:eslint": "eslint src/*",
|
||||
"check:prettier": "prettier --check .",
|
||||
"fix:prettier": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"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",
|
||||
"@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",
|
||||
"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",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twemoji": "^14.0.2"
|
||||
"@capacitor/android": "4.7.1",
|
||||
"@capacitor/core": "4.7.1",
|
||||
"@fontsource/inter": "4.5.14",
|
||||
"@fontsource/roboto": "4.5.8",
|
||||
"@khanacademy/simple-markdown": "0.8.6",
|
||||
"@lzy1960/google-translate": "0.2.6",
|
||||
"@matrix-org/olm": "3.2.14",
|
||||
"@tippyjs/react": "4.2.6",
|
||||
"blurhash": "2.0.4",
|
||||
"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",
|
||||
"google-translate-api-browser": "3.0.0",
|
||||
"html-react-parser": "3.0.4",
|
||||
"i18next": "22.4.9",
|
||||
"i18next-browser-languagedetector": "7.0.1",
|
||||
"i18next-http-backend": "2.1.1",
|
||||
"katex": "0.16.4",
|
||||
"linkify-html": "4.0.2",
|
||||
"linkifyjs": "4.0.2",
|
||||
"matrix-js-sdk": "24.0.0",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-autosize-textarea": "7.1.0",
|
||||
"react-blurhash": "0.2.0",
|
||||
"react-dnd": "15.1.2",
|
||||
"react-dnd-html5-backend": "15.1.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-i18next": "12.1.5",
|
||||
"react-modal": "3.16.1",
|
||||
"sanitize-html": "2.8.0",
|
||||
"tippy.js": "6.3.7",
|
||||
"twemoji": "14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"assert": "^2.0.0",
|
||||
"babel-loader": "^8.2.5",
|
||||
"browserify-fs": "^1.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
"eslint": "^8.23.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-react-hooks": "^4.6.0",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-loader": "^4.1.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sass": "^1.54.5",
|
||||
"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-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.10.1",
|
||||
"webpack-merge": "^5.7.3"
|
||||
"@capacitor/assets": "2.0.4",
|
||||
"@capacitor/cli": "4.7.1",
|
||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||
"@typescript-eslint/parser": "5.46.1",
|
||||
"@vitejs/plugin-react": "3.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"eslint": "8.29.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"mini-svg-data-uri": "1.4.4",
|
||||
"prettier": "2.8.1",
|
||||
"sass": "1.56.2",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "4.0.1",
|
||||
"vite-plugin-static-copy": "0.13.0"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cinny</title>
|
||||
<meta name="name" content="Cinny">
|
||||
<meta name="author" content="Ajay Bura">
|
||||
<meta name="description" content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source.">
|
||||
<meta name="keywords" content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element">
|
||||
|
||||
<meta property="og:title" content="Cinny">
|
||||
<meta property="og:url" content="https://cinny.in">
|
||||
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png">
|
||||
<meta property="og:description" content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source.">
|
||||
<meta name="theme-color" content="#000000">
|
||||
</head>
|
||||
<body id="appBody">
|
||||
<div id="root"></div>
|
||||
<audio id="notificationSound">
|
||||
<source src="./sound/notification.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
<audio id="inviteSound">
|
||||
<source src="./sound/invite.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,889 @@
|
||||
{
|
||||
"translation": {
|
||||
"common": {
|
||||
"close": "Close",
|
||||
"open": "Open",
|
||||
"leave": "Leave",
|
||||
"options": "Options",
|
||||
"cinny": "Cinny",
|
||||
"slogan": "Yet another matrix client",
|
||||
"source_code": "Source code",
|
||||
"sponsor": "Support",
|
||||
"retry": "Retry",
|
||||
"delete": "Delete",
|
||||
"continue": "Continue",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"view_more": "View more",
|
||||
"view_less": "View less",
|
||||
"copy": "Copy",
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"or": "Or",
|
||||
"reset": "Reset",
|
||||
"setup": "Setup",
|
||||
"search": "Search",
|
||||
"loading": "Loading...",
|
||||
"joining": "Joining...",
|
||||
"join": "Join",
|
||||
"remove": "Remove",
|
||||
"send": "Send",
|
||||
"homeserver": "Homeserver",
|
||||
"invite": "Invite",
|
||||
"uninvite": "Uninvite",
|
||||
"invited": "Invited",
|
||||
"inviting": "Inviting...",
|
||||
"uninviting": "Uninviting...",
|
||||
"change": "Change",
|
||||
"edit": "Edit",
|
||||
"message_prompt": "Message"
|
||||
},
|
||||
"errors": {
|
||||
"browser_not_supported": "Not supported in this browser",
|
||||
"generic": "Something went wrong!"
|
||||
},
|
||||
"Organisms": {
|
||||
"Welcome": {
|
||||
"heading": "Welcome to Cinny!",
|
||||
"subheading": "Yet another Matrix client"
|
||||
},
|
||||
"ViewSource": {
|
||||
"title": "View Source",
|
||||
"original_source": "Original source",
|
||||
"decrypted_source": "Decrypted source"
|
||||
},
|
||||
"SpaceManage": {
|
||||
"subtitle": "manage rooms",
|
||||
"load_more": "Load more",
|
||||
"rooms_and_spaces": "Rooms and spaces",
|
||||
"private_rooms_message": "Either the space contains private rooms or you need to join space to view it's rooms.",
|
||||
"items_selected_zero": "No selected items",
|
||||
"items_selected_one": "{{count}} selected item",
|
||||
"items_selected_other": "{{count}} selected items",
|
||||
"room_members_zero": "No room members",
|
||||
"room_members_one": "{{count}} room member",
|
||||
"room_members_other": "{{count}} room members",
|
||||
"mark_suggested_zero": "Marking no rooms as suggested",
|
||||
"mark_suggested_one": "Marking {{count}} room as suggested",
|
||||
"mark_suggested_other": "Marking {{count}} rooms as suggested",
|
||||
"mark_not_suggested_zero": "Marking no rooms as suggested",
|
||||
"mark_not_suggested_one": "Marking {{count}} room as suggested",
|
||||
"mark_not_suggested_other": "Marking {{count}} rooms as suggested",
|
||||
"remove_zero": "Removing no items",
|
||||
"remove_one": "Removing {{count}} item",
|
||||
"remove_other": "Removing {{count}} items.",
|
||||
"suggested": "Suggested",
|
||||
"mark_as_suggested": "Mark as suggested",
|
||||
"mark_as_not_suggested": "Mark as not suggested"
|
||||
},
|
||||
"SpaceSettings": {
|
||||
"subtitle": "space settings",
|
||||
"leave": {
|
||||
"leave_space": "Leave Space",
|
||||
"leave_dialog_title": "Leave Space",
|
||||
"leave_dialog_message": "Are you sure you want to leave {{space}}?"
|
||||
},
|
||||
"visibility": {
|
||||
"header": "Space visibility (who can join)"
|
||||
},
|
||||
"addresses": {
|
||||
"header": "Space addresses"
|
||||
},
|
||||
"categorize_subspaces": "Categorize subspaces",
|
||||
"uncategorize_subspaces": "Uncategorize subspaces",
|
||||
"pin_sidebar": "Pin to sidebar",
|
||||
"unpin_sidebar": "Unpin from sidebar"
|
||||
},
|
||||
"Settings": {
|
||||
"title": "Settings",
|
||||
"theme": {
|
||||
"follow_system": {
|
||||
"title": "Follow system theme",
|
||||
"description": "Use light or dark mode based on the system settings."
|
||||
},
|
||||
"title": "Theme",
|
||||
"theme_light": "Light",
|
||||
"theme_silver": "Silver",
|
||||
"theme_dark": "Dark",
|
||||
"theme_butter": "Butter"
|
||||
},
|
||||
"markdown": {
|
||||
"title": "Markdown formatting",
|
||||
"description": "Format messages with markdown before sending"
|
||||
},
|
||||
"hide_membership_events": {
|
||||
"title": "Hide membership events",
|
||||
"description": "Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)"
|
||||
},
|
||||
"hide_nickname_avatar_events": {
|
||||
"title": "Hide nick/avatar events",
|
||||
"description": "Hide nickname and avatar change messages from the room timeline."
|
||||
},
|
||||
"notifications_and_sound": {
|
||||
"title": "Notifications & Sound",
|
||||
"desktop": {
|
||||
"title": "Desktop notifications",
|
||||
"description": "Show desktop notifications when new messages arrive."
|
||||
},
|
||||
"sound": {
|
||||
"title": "Notification sound",
|
||||
"description": "Play a sound when new messages arrive."
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"cross_signing": {
|
||||
"title": "Cross signing and backup"
|
||||
},
|
||||
"export_import_encryption_keys": {
|
||||
"title": "Export / Import encryption keys"
|
||||
},
|
||||
"export_encryption_keys": {
|
||||
"title": "Export E2E room keys",
|
||||
"description": "Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing."
|
||||
},
|
||||
"import_encryption_keys": {
|
||||
"title": "Import E2E room keys",
|
||||
"description": "To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\\'ll have to enter the password you set in order to decrypt it."
|
||||
}
|
||||
},
|
||||
"logout": {
|
||||
"title": "Logout",
|
||||
"dialog": {
|
||||
"title": "Logout",
|
||||
"description": "Are you sure that you want to logout your session?",
|
||||
"confirm": "Logout"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"application": "Application",
|
||||
"credits": "Credits"
|
||||
}
|
||||
},
|
||||
"ShortcutSpaces": {
|
||||
"header": "Pin Spaces",
|
||||
"pinned_spaces": "Pinned spaces",
|
||||
"no_pinned_spaces": "No pinned spaces",
|
||||
"unpinned_spaces": "Unpinned spaces",
|
||||
"no_unpinned_spaces": "No unpinned spaces",
|
||||
"spaces_selected_zero": "No selected spaces",
|
||||
"spaces_selected_one": "{{count}} selected space",
|
||||
"spaces_selected_other": "{{count}} selected spaces",
|
||||
"pin_button": "Pin"
|
||||
},
|
||||
"SecretStorageAccess": {
|
||||
"incorrect_security_key": "Incorrect security key",
|
||||
"incorrect_security_phrase": "Incorrect security phrase",
|
||||
"security_phrase": "Security Phrase",
|
||||
"security_key": "Security Key",
|
||||
"use_security_key": "Use Security Key",
|
||||
"use_security_phrase": "Use Security Phrase"
|
||||
},
|
||||
"KeyBackup": {
|
||||
"create_backup_title": "Create key backup",
|
||||
"create_backup_tooltip": "Create backup",
|
||||
"creating_backup": "Creating Backup...",
|
||||
"backup_created": "Successfully created backup",
|
||||
"backup_failed": "Failed to create backup",
|
||||
"restoring": "Restoring backup keys...",
|
||||
"restoring_progress": "Restoring backup keys... ({{progress}}/{{total}}",
|
||||
"restore_backup_title": "Restore Key Backup",
|
||||
"restore_backup_tooltip": "Restore Key Backup",
|
||||
"restore_complete": "Successfully restored backup keys ({{progress}}/{{total}})",
|
||||
"restore_failed_bad_key": "Failed to restore backup. Key is invalid!",
|
||||
"restore_failed_unknown": "Failed to restore backup.",
|
||||
"delete_key_backup_title": "Delete key backup",
|
||||
"delete_key_backup_tooltip": "Delete backup",
|
||||
"delete_key_backup_subtitle": "Deleting key backup is permanent.",
|
||||
"delete_key_backup_message": "All encrypted message keys stored on the server will be permanently deleted.",
|
||||
"encrypted_messages_backup_description": "Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.",
|
||||
"encrypted_messages_backup_title": "Encrypted messages backup",
|
||||
"encrypted_messages_backup_cross_signing_disabled": "Setup cross signing to backup your encrypted messages."
|
||||
},
|
||||
"DeviceManage": {
|
||||
"edit_session_name_title": "Edit session name",
|
||||
"edit_session_name_subtitle": "Session name",
|
||||
"edit_session_name_tooltip": "Edit session name",
|
||||
"current_device_label": "Current",
|
||||
"verify_session_button": "Verify",
|
||||
"unverified_sessions_title": "Unverified sessions",
|
||||
"unverified_sessions_none": "No unverified sessions",
|
||||
"unencrypted_sessions_title": "Sessions without encryption support",
|
||||
"verified_sessions_title": "Verified sessions",
|
||||
"verified_sessions_none": "No verified sessions",
|
||||
"setup_cross_signing_message": "Setup cross signing in case you lose all your sessions",
|
||||
"loading_devices": "Loading devices...",
|
||||
"logout_device_title": "Logout {{device}}",
|
||||
"logout_device_message": "You are about to log out the session for {{device}}",
|
||||
"logout_device_confirm": "Logout",
|
||||
"logout_device_tooltip": "Remove session",
|
||||
"session_verification_title": "Session Verification",
|
||||
"session_name_privacy_message": "Session names are visible to everyone, so do not put any private info here."
|
||||
},
|
||||
"CrossSigning": {
|
||||
"title": "Cross Signing",
|
||||
"setup_failed": "Failed to setup cross signing. Please try again",
|
||||
"setup": "Setup cross signing",
|
||||
"save_security_key_message": "Please save this security key somewhere safe",
|
||||
"security_key_dialog_title": "Security Key",
|
||||
"security_key_generation_message": "We will generate a Security Key, which you can use to manage message backups and session verification.",
|
||||
"security_key_generation_button": "Generate Key",
|
||||
"security_phrase_message": "Alternatively you can set a 'Security Phrase' so you don't have to remember the long Security Key, and optionally save the key as a backup",
|
||||
"security_phrase_label": "Security Phrase",
|
||||
"security_phrase_confirm_label": "Confirm Security Phrase",
|
||||
"security_phrase_set_button": "Set Phrase & Generate Key",
|
||||
"setup_dialog_title": "Setup cross signing",
|
||||
"setup_message": "Setup to verify and keep track of all your sessions. Also required to backup encrypted message.",
|
||||
"reset_keys_subtitle": "Resetting cross-signing keys is permanent.",
|
||||
"reset_keys_message": "Anyone you have verified with will see security alerts and your message backup will lost. You almost certainly do not want to do this, unless you have lost Security Key or Phrase and every session you can cross-sign from."
|
||||
},
|
||||
"AuthRequest": {
|
||||
"wrong_password": "Wrong password. Please enter the correct password",
|
||||
"request_failed": "Request failed!",
|
||||
"password_label": "Account password"
|
||||
},
|
||||
"Search": {
|
||||
"description": "Type # for rooms, @ for DMs and * for spaces. Hotkey: Ctrl + k"
|
||||
},
|
||||
"RoomViewInput": {
|
||||
"upload_progress": "Uploading: {{progress}}/{{total}} ({{percent}}%)",
|
||||
"tombstone_replaced": "This room has been replaced, and is no longer active.",
|
||||
"tombstone_permission_denied": "You do not have permission to post to this room",
|
||||
"send_message_placeholder": "Send a message...",
|
||||
"emoji_tooltip": "Emoji",
|
||||
"file_size": "Size: {{size}}",
|
||||
"cancel_reply_tooltip": "Cancel reply"
|
||||
},
|
||||
"RoomViewHeader": {
|
||||
"search_tooltip": "Search",
|
||||
"people_tooltip": "People",
|
||||
"members_tooltip": "Members"
|
||||
},
|
||||
"RoomViewFloating": {
|
||||
"jump_unread": "Jump to unread messages",
|
||||
"mark_read": "Mark as read",
|
||||
"jump_latest": "Jump to latest",
|
||||
"user_typing_1": "<bold><user_one/></bold> is typing...",
|
||||
"user_typing_2": "<bold><user_one/></bold> and <bold><user_two/></bold> are typing...",
|
||||
"user_typing_3": "<bold><user_one/></bold>, <bold><user_two/></bold> and <bold><user_three/></bold> are typing...",
|
||||
"user_typing_4": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and <bold><user_four/></bold> are typing...",
|
||||
"user_typing_other": "<bold>Several people</bold> are typing..."
|
||||
},
|
||||
"RoomViewContent": {
|
||||
"welcome_to_room": "Welcome to {{room_name}}!",
|
||||
"beginning_room": "This is the beginning of the <bold>{{room_name}}</bold> room.",
|
||||
"beginning_dm": "This is the beginning of your direct message history with <bold>@{{user_name}}</bold>.",
|
||||
"created_on": "Created on {{date, datetime}}",
|
||||
"new_messages": "New messages"
|
||||
},
|
||||
"RoomSettings": {
|
||||
"leave_room": "Leave room",
|
||||
"leave_room_confirm_message": "Are you sure you want to leave {{room_name}}?",
|
||||
"leave_room_confirm_button": "Leave",
|
||||
"notification_header": "Notifications (Changing this will only affect you)",
|
||||
"visibility_header": "Room visibility (Who can join)",
|
||||
"address_header": "Room addresses",
|
||||
"encryption_header": "Encryption",
|
||||
"message_history_header": "Message history visibility",
|
||||
"room_settings_subtitle": "room settings"
|
||||
},
|
||||
"RoomCommon": {
|
||||
"user_joined": "<bold><user/></bold> joined the room",
|
||||
"user_left": "<bold><user/></bold> left the room",
|
||||
"user_left_reason": "<bold><user/></bold> left the room: <reason/>",
|
||||
"user_invited": "<bold><inviter/></bold> invited <bold><user/></bold>",
|
||||
"invite_cancelled": "<bold><inviter/></bold> cancelled <bold><user/>'s</bold> invite",
|
||||
"invite_rejected": "<bold><user/></bold> rejected the invitation",
|
||||
"user_kicked": "<bold><actor/></bold> kicked <bold><user/></bold>",
|
||||
"user_kicked_reason": "<bold><actor/></bold> kicked <bold><user/></bold>: <reason/>",
|
||||
"user_banned": "<bold><actor/></bold> banned <bold><user/></bold>",
|
||||
"user_banned_reason": "<bold><actor/></bold> banned <bold><user/></bold>: <reason/>",
|
||||
"user_unbanned": "<bold><actor/> unbanned <bold><user/></bold>",
|
||||
"avatar_set": "<bold><user/></bold> set an avatar",
|
||||
"avatar_changed": "<bold><user/></bold> changed their avatar",
|
||||
"avatar_removed": "<bold><user/></bold> removed their avatar",
|
||||
"name_set": "<bold><user/></bold> set their display name to <bold><new_name/></bold>",
|
||||
"name_changed": "<bold><user/></bold> changed their display name to <bold><new_name/></bold>",
|
||||
"name_removed": "<bold><user/></bold> removed their display name <bold><last_name/></bold>"
|
||||
},
|
||||
"PublicRooms": {
|
||||
"could_not_join_alias": "Unable to join {{alias}}. Either the room is private or doesn't exist",
|
||||
"try_joining_alias": "Try joining {{alias}}",
|
||||
"joining_alias": "Joining {{alias}}...",
|
||||
"no_public_rooms": "No public rooms on {{homeserver}}",
|
||||
"no_result_found": "No result found for '{{input}}' on {{homeserver}}",
|
||||
"title": "Public Rooms",
|
||||
"search_room_name_alias": "Room name or alias",
|
||||
"search_button": "Search",
|
||||
"loading": "Loading public rooms from {{homeserver}}...",
|
||||
"searching": "Searching for '{{query}}' on {{homeserver}}...",
|
||||
"result_title": "Public rooms on {{homeserver}}",
|
||||
"search_result_title": "Search result for '{{query}}' on {{homeserver}}"
|
||||
},
|
||||
"ProfileViewer": {
|
||||
"kick_button": "Kick",
|
||||
"kick_reason_label": "Kick Reason",
|
||||
"ban_button": "Ban",
|
||||
"ban_reason_label": "Ban reason",
|
||||
"loading_sessions": "Loading sessions...",
|
||||
"no_sessions_found": "No sessions found.",
|
||||
"view_sessions_one": "View session",
|
||||
"view_sessions_other": "View {{count}} sessions",
|
||||
"send_direct_message_button": "Message",
|
||||
"creating_dm_room": "Creating room...",
|
||||
"ignore": "Ignore",
|
||||
"ignoring": "Ignoring...",
|
||||
"unignore": "Unignore",
|
||||
"unignoring": "Unignoring...",
|
||||
"change_power_level": "Change power level",
|
||||
"shared_power_message": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?",
|
||||
"demoting_self_message": "You will not be able to undo this change as you are demoting yourself. Are you sure?"
|
||||
},
|
||||
"PeopleDrawer": {
|
||||
"title": "People",
|
||||
"members_one": "{{count}} member",
|
||||
"members_other": "{{count}} members",
|
||||
"joined": "Joined",
|
||||
"invited": "Invited",
|
||||
"banned": "Banned",
|
||||
"search_no_results": "No results found",
|
||||
"view_more": "View more",
|
||||
"placeholder": "Search",
|
||||
"invite_tooltip": "Invite"
|
||||
},
|
||||
"ProfileEditor": {
|
||||
"remove_avatar": "Remove avatar",
|
||||
"remove_avatar_confirmation": "Are you sure that you want to remove your avatar?",
|
||||
"display_name_message": "Display name of {{user_name}}"
|
||||
},
|
||||
"DrawerBreadcrumb": {
|
||||
"home": "Home"
|
||||
},
|
||||
"DrawerHeader": {
|
||||
"add_rooms_or_spaces": "Add rooms or spaces",
|
||||
"create_new_space": "Create new space",
|
||||
"create_new_room": "Create new room",
|
||||
"join_public_room": "Join public room",
|
||||
"join_with_address": "Join with address",
|
||||
"add_existing": "Add existing",
|
||||
"manage_rooms": "Manage rooms",
|
||||
"home": "Home",
|
||||
"direct_messages": "Direct messages",
|
||||
"start_dm_tooltip": "Start DM",
|
||||
"add_rooms_spaces_tooltip": "Add rooms/spaces"
|
||||
},
|
||||
"SideBar": {
|
||||
"settings_tooltip": "Settings",
|
||||
"unverified_sessions_one": "{{count}} unverified session",
|
||||
"unverified_sessions_other": "{{count}} unverified sessions",
|
||||
"home_tooltip": "Home",
|
||||
"direct_messages_tooltip": "People",
|
||||
"pin_spaces_tooltip": "Pin spaces",
|
||||
"search_tooltip": "Search",
|
||||
"invites_tooltip": "Invites"
|
||||
},
|
||||
"JoinAlias": {
|
||||
"invalid_address": "Invalid address.",
|
||||
"looking_for_address": "Looking for address...",
|
||||
"joining_alias": "Joining {{alias_name}}...",
|
||||
"couldnt_find_room_or_space_alias": "Unable to find room/space with {{alias_name}}. Either the room/space is private or doesn't exist.",
|
||||
"couldnt_find_room_or_space": "Unable to join {{alias_name}}. Either the room/space is private or doesn't exist.",
|
||||
"address_label": "Address",
|
||||
"title": "Join with address"
|
||||
},
|
||||
"InviteUser": {
|
||||
"user_not_found": "{{user_name}} not found!",
|
||||
"no_matches_found": "No matches found for {{user_name}}",
|
||||
"invite_result": {
|
||||
"invited": "Invited",
|
||||
"already_joined": "Already joined",
|
||||
"already_invited": "Already invited",
|
||||
"banned": "Banned"
|
||||
},
|
||||
"search_label": "Name or User ID",
|
||||
"search_result_title": "Search result for user {{user_name}}",
|
||||
"searching_for_user": "Searching for user {{user_name}}...",
|
||||
"invite_to_room": "Invite to {{room}}",
|
||||
"invite_to_dm": "Direct Message"
|
||||
},
|
||||
"InviteList": {
|
||||
"accept_invite": "Accept",
|
||||
"reject_invite": "Reject",
|
||||
"direct_messages_title": "Direct Messages",
|
||||
"rooms_title": "Rooms",
|
||||
"spaces_title": "Spaces",
|
||||
"title": "Invites"
|
||||
},
|
||||
"EmojiVerification": {
|
||||
"waiting_for_response": "Waiting for response from other device...",
|
||||
"confirmation_prompt": "Confirm the emoji below are displayed on both devices, in the same order:",
|
||||
"emojis_match_button": "They match",
|
||||
"emojis_dont_match_button": "They don't match",
|
||||
"accept_request_from_other_device_message": "Please accept the request from other device.",
|
||||
"begin_verification_process_message": "Click accept to start the verification process.",
|
||||
"begin_verification_button_text": "Accept",
|
||||
"title": "Emoji Verification"
|
||||
},
|
||||
"DragDrop": {
|
||||
"drop_file_to_upload_prompt": "Drop file to upload"
|
||||
},
|
||||
"CreateRoom": {
|
||||
"private_room_short": "Private",
|
||||
"restricted_room_short": "Restricted",
|
||||
"public_room_short": "Public",
|
||||
"private_room_long": "Private (invite only)",
|
||||
"restricted_room_long": "Restricted (space member can join)",
|
||||
"public_room_long": "Public (anyone can join)",
|
||||
"visibility_title": "Visibility",
|
||||
"visibility_message": "Visibility (who can join)",
|
||||
"select_who_can_join_space": "Select who can join this space",
|
||||
"select_who_can_join_room": "Select who can join this room",
|
||||
"space_address": "Space address",
|
||||
"room_address": "Room address",
|
||||
"room_address_already_in_use": "{{room_address}} is already in use",
|
||||
"e2e_title": "Enable end-to-end encryption",
|
||||
"e2e_message": "You can’t disable this later. Bridges & most bots won’t work yet.",
|
||||
"role_title": "Select your role",
|
||||
"role_message": "Selecting 'Admin' sets your power level to 100 whereas 'Founder' sets it to 101.",
|
||||
"creating_room": "Creating room...",
|
||||
"creating_space": "Creating space...",
|
||||
"topic_label": "Topic (optional)",
|
||||
"space_name": "Space name",
|
||||
"room_name": "Room name",
|
||||
"role_admin": "Admin",
|
||||
"role_founder": "Founder",
|
||||
"create_room": "Create room",
|
||||
"create_space": "Create space",
|
||||
"home": "Home"
|
||||
}
|
||||
},
|
||||
"Molecules": {
|
||||
"ConfirmDialog": {
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"ReusableDialog": {
|
||||
"close_tooltip": "Close"
|
||||
},
|
||||
"FollowingMembers": {
|
||||
"users_following_1": "<bold><user_one/></bold> is following the conversation",
|
||||
"users_following_2": "<bold><user_one/></bold> and <bold><user_two/></bold> are following the conversation",
|
||||
"users_following_3": "<bold><user_one/></bold>, <bold><user_two/></bold>, and <bold><user_three/></bold> are following the conversation",
|
||||
"users_following_other": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and {{other_count}} others are following the conversation"
|
||||
},
|
||||
"ImageUpload": {
|
||||
"prompt": "Upload",
|
||||
"cancel": "Cancel",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"ExportE2ERoomKeys": {
|
||||
"getting_keys": "Getting keys...",
|
||||
"encrypting_keys": "Encrypting Keys...",
|
||||
"password_does_not_match": "Password does not match.",
|
||||
"export_success": "Successfully exported all keys.",
|
||||
"export_failed": "Failed to export keys. Please try again.",
|
||||
"button_text": "Export"
|
||||
},
|
||||
"ImportE2ERoomKeys": {
|
||||
"decrypting_file": "Decrypting file...",
|
||||
"decrypting_messages": "Decrypting messages...",
|
||||
"import_success": "Successfully imported all keys",
|
||||
"import_failed": "Failed tp decrypt keys. Please try again",
|
||||
"import_keys_button": "Import Keys",
|
||||
"decrypt_button": "Decrypt"
|
||||
},
|
||||
"Media": {
|
||||
"open_new_tab": "Open in new tab",
|
||||
"download": "Download",
|
||||
"play_audio": "Play audio",
|
||||
"play_video": "Play video"
|
||||
},
|
||||
"Message": {
|
||||
"message_deleted": "*** This message has been deleted ***",
|
||||
"unable_to_load_reply": "*** Unable to load reply ***",
|
||||
"unknown_user": "** Unknown user **",
|
||||
"edited": "(edited)",
|
||||
"edit_placeholder": "Edit message",
|
||||
"user_reacted_1": "<bold><user_one/></bold> reacted with <emoji/>",
|
||||
"user_reacted_2": "<bold><user_one/></bold> and <bold><user_two/></bold> reacted with <emoji/>",
|
||||
"user_reacted_3": "<bold><user_one/></bold>, <bold><user_two/></bold> and <bold><user_three/></bold> reacted with <emoji/>",
|
||||
"user_reacted_other": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and {{other_count}} others reacted with <emoji/>",
|
||||
"add_reaction_tooltip": "Add reaction",
|
||||
"reply_tooltip": "Reply",
|
||||
"edit_tooltip": "Edit",
|
||||
"options_header": "Options",
|
||||
"options_tooltip": "Options",
|
||||
"read_receipts": "Read receipts",
|
||||
"view_source": "View source",
|
||||
"delete_message_prompt": "Delete message",
|
||||
"delete_message_confirmation": "Are you sure that you want to delete this message?",
|
||||
"delete_message_button": "Delete"
|
||||
},
|
||||
"PopupWindow": {
|
||||
"close_tooltip": "Back"
|
||||
},
|
||||
"PowerLevelSelector": {
|
||||
"placeholder": "Power level"
|
||||
},
|
||||
"RoomAliases": {
|
||||
"invalid_characters": "Invalid character: only letter, numbers and _- are allowed.",
|
||||
"validating_alias": "validating {{alias}}...",
|
||||
"alias_available": "{{alias}} is available.",
|
||||
"alias_unavailable": "{{alias}} is unavailable.",
|
||||
"deleting_alias": "Deleting...",
|
||||
"set_main_alias": "Set as Main",
|
||||
"publish_alias": "Publish",
|
||||
"unpublish_alias": "Unpublish",
|
||||
"delete_alias": "Delete",
|
||||
"main_alias": "Main",
|
||||
"publish_to_room_directory": {
|
||||
"title": "Publish to room directory",
|
||||
"publish_room_message": "Publish this room to the {{homeserver}} publish directory?",
|
||||
"publish_space_message": "Publish this space to the {{homeserver}} publish directory?"
|
||||
},
|
||||
"published_addresses": {
|
||||
"title": "Published addresses",
|
||||
"none": "No published addresses",
|
||||
"no_main_address": "No main address (select one from below)",
|
||||
"message_room": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
|
||||
"message_space": "Published addresses can be used by anyone on any server to join your space. To publish an address, it needs to be set as a local address first."
|
||||
},
|
||||
"local_addresses": {
|
||||
"title": "Local addresses",
|
||||
"none": "No local addresses",
|
||||
"message_room": "Set local addresses for this room so users can find this room through your homeserver.",
|
||||
"message_space": "Set local addresses for this space so users can find this space through your homeserver.",
|
||||
"add": "Add local address",
|
||||
"add_button": "Add",
|
||||
"placeholder_room": "my_room_address",
|
||||
"placeholder_space": "my_space_address",
|
||||
"hide": "Hide local addresses",
|
||||
"view": "View local addresses"
|
||||
}
|
||||
},
|
||||
"RoomEncryption": {
|
||||
"encryption_public_room_message": "It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone should be able to read messages in them.",
|
||||
"encryption_message": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly",
|
||||
"encryption_cannot_be_disabled": "Once enabled, encryption cannot be disabled.",
|
||||
"enable_room_encryption": "Enable room encryption",
|
||||
"enable_encryption_prompt": "Enable Encryption",
|
||||
"enable_encryption_button": "Enable",
|
||||
"continue_button": "Continue"
|
||||
},
|
||||
"RoomHistoryVisibility": {
|
||||
"world_readable": "Anyone (including guests)",
|
||||
"shared": "Members (all messages)",
|
||||
"invited": "Members (messages after invite)",
|
||||
"joined": "Members (messages after join)",
|
||||
"changes_only_affect_future": "Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect."
|
||||
},
|
||||
"RoomMembers": {
|
||||
"search_title": "Search member",
|
||||
"found_members_one": "Found — one member",
|
||||
"found_members_other": "Found — {{count}} members",
|
||||
"no_results": "No results found for {{term}}",
|
||||
"no_members": "No members to display",
|
||||
"view_more": "View more",
|
||||
"joined": "Joined",
|
||||
"invited": "Invited",
|
||||
"banned": "Banned",
|
||||
"search_placeholder": "Search for name"
|
||||
},
|
||||
"RoomNotification": {
|
||||
"default": "Global",
|
||||
"all_messages": "All messages",
|
||||
"mentions_and_keywords": "Mentions & Keywords",
|
||||
"mute": "Mute"
|
||||
},
|
||||
"RoomOptions": {
|
||||
"title": "Options for {{room_name}}",
|
||||
"leave": {
|
||||
"title": "Leave room",
|
||||
"subtitle": "Are you sure you want to leave the {{room_name}} room?",
|
||||
"button_text": "Leave"
|
||||
},
|
||||
"mark_as_read": "Mark as read",
|
||||
"notifications_heading": "Notifications",
|
||||
"invite": "Invite"
|
||||
},
|
||||
"RoomPermissions": {
|
||||
"default_role": {
|
||||
"name": "Default role",
|
||||
"description": "Set default role for all members"
|
||||
},
|
||||
"send_messages": {
|
||||
"name": "Send messages",
|
||||
"description": "Set minimum power level to send messages in a room"
|
||||
},
|
||||
"reactions": {
|
||||
"name": "Send reactions",
|
||||
"description": "Set minimum power level to send reactions in a room"
|
||||
},
|
||||
"delete": {
|
||||
"name": "Delete messages sent by others",
|
||||
"description": "Set minumum power level to delete messages in a room"
|
||||
},
|
||||
"notifications": {
|
||||
"name": "Ping room",
|
||||
"description": "Set minimum power level to ping room"
|
||||
},
|
||||
"manage_rooms": {
|
||||
"name": "Manage rooms in space",
|
||||
"description": "Set minimum power level to manage rooms in space"
|
||||
},
|
||||
"invite": {
|
||||
"name": "Invite",
|
||||
"description": "Set minimum power level to invite members"
|
||||
},
|
||||
"kick": {
|
||||
"name": "Kick",
|
||||
"description": "Set minimum power level to kick members"
|
||||
},
|
||||
"ban": {
|
||||
"name": "Ban",
|
||||
"description": "Set minimum power level to ban members"
|
||||
},
|
||||
"change_avatar": {
|
||||
"name": "Change avatar",
|
||||
"description": "Set minimum power level to change room/space avatar"
|
||||
},
|
||||
"change_name": {
|
||||
"name": "Change name",
|
||||
"description": "Set minimum power evel to change room/space name"
|
||||
},
|
||||
"change_topic": {
|
||||
"name": "Change topic",
|
||||
"description": "Set minimum power level to change room/space topic"
|
||||
},
|
||||
"change_settings": {
|
||||
"name": "Change settings",
|
||||
"description": "Set minimum power level to change settings"
|
||||
},
|
||||
"change_published_address": {
|
||||
"name": "Change published address",
|
||||
"description": "Set minimum power level to publish and set main address"
|
||||
},
|
||||
"change_permissions": {
|
||||
"name": "Change permissions",
|
||||
"description": "Set minimum power level to change permissions"
|
||||
},
|
||||
"enable_room_encryption": {
|
||||
"name": "Enable room encryption",
|
||||
"description": "Set minimum power level to enable room encryption"
|
||||
},
|
||||
"change_history_visibility": {
|
||||
"name": "Change history visibility",
|
||||
"description": "Set minimum power level to change room message history visibility"
|
||||
},
|
||||
"upgrade_room": {
|
||||
"name": "Upgrade room",
|
||||
"description": "Set minimum power level to upgrade room"
|
||||
},
|
||||
"pin_messages": {
|
||||
"name": "Pin messages",
|
||||
"description": "Set minimum power level to pin messages in a room"
|
||||
},
|
||||
"change_acls": {
|
||||
"name": "Change server ACLs",
|
||||
"description": "Set minimum power level to change server ACLs"
|
||||
},
|
||||
"modify_widgets": {
|
||||
"name": "Modify widgets",
|
||||
"description": "Set minimum power level to modify room widgets"
|
||||
},
|
||||
"groups": {
|
||||
"general": "General permissions",
|
||||
"manage_members": "Manage members permissions",
|
||||
"room": "Room profile permissions",
|
||||
"space": "Space profile permissions",
|
||||
"other": "Other permissions",
|
||||
"settings": "Settings permissions"
|
||||
}
|
||||
},
|
||||
"RoomProfile": {
|
||||
"saving_room_name": "Saving room name...",
|
||||
"saving_room_topic": "Saving room topic...",
|
||||
"save_success": "Saved successfully",
|
||||
"save_failed": "Unable to save",
|
||||
"remove_avatar_title": "Remove avatar",
|
||||
"remove_avatar_subtitle": "Are you sure that you want to remove room avatar?",
|
||||
"remove_avatar_button": "Remove",
|
||||
"permission_change_room_name": "You have permission to change room name only",
|
||||
"permission_change_room_topic": "You have permission to change room topic only",
|
||||
"permission_change_space_name": "You have permission to change space name only",
|
||||
"permission_change_space_topic": "You have permission to change space topic only",
|
||||
"name_label": "Name",
|
||||
"topic_label": "Topic"
|
||||
},
|
||||
"RoomSearch": {
|
||||
"title": "Room search",
|
||||
"placeholder": "Search for keywords",
|
||||
"search_button": "Search",
|
||||
"searching": "Searching room messages...",
|
||||
"subtitle": "Search room messages",
|
||||
"failed": "Failed to search messages",
|
||||
"no_results": "No results found",
|
||||
"encrypted_room": "Search does not work in encrypted room",
|
||||
"load_more": "Load more",
|
||||
"results_one": "{{count}} result for {{term}}",
|
||||
"results_other": "{{count}} results for {{term}}"
|
||||
},
|
||||
"RoomTile": {
|
||||
"invited_by_user_zero": "Invited by {{inviter}} to {{id}}",
|
||||
"invited_by_user_one": "Invited by {{inviter}} to {{id}} • {{member_count}} member",
|
||||
"invited_by_user_other": "Invited by {{inviter}} to {{id}} • {{member_count}} members",
|
||||
"invited_zero": "{{id}}",
|
||||
"invited_one": "{{id}} • {{member_count}} member",
|
||||
"invited_other": "{{id}} • {{member_count}} members"
|
||||
},
|
||||
"RoomVisibility": {
|
||||
"private": "Private (invite only)",
|
||||
"restricted": "Restricted (space members can join)",
|
||||
"restricted_unsupported": "Restricted (Unsupported: room required upgrade)",
|
||||
"public": "Public (anyone can join)"
|
||||
},
|
||||
"RoomEmojis": {
|
||||
"header": "Create Pack",
|
||||
"submit_button": "Create pack",
|
||||
"name_placeholder": "Pack Name",
|
||||
"no_packs": "No emoji or sticker packs."
|
||||
},
|
||||
"ImagePack": {
|
||||
"rename_title": "Rename",
|
||||
"rename_button": "Rename",
|
||||
"shortcode_label": "Shortcode",
|
||||
"delete_item_title": "Delete",
|
||||
"delete_item_message": "Are you sure that you want to delete {{key}}?",
|
||||
"delete_item_button": "Delete",
|
||||
"delete_pack_title": "Delete Pack",
|
||||
"delete_pack_message": "Are you sure that you want to delete {{pack_name}}?",
|
||||
"delete_pack_button": "Delete",
|
||||
"image_title": "Image",
|
||||
"image_shortcode": "Shortcode",
|
||||
"image_usage": "Usage",
|
||||
"view_more": "View {{count}} more",
|
||||
"view_less": "View less",
|
||||
"use_globally": "Use globally",
|
||||
"use_globally_description": "Add this pack to your account to use in all rooms.",
|
||||
"no_global_packs": "No global packs",
|
||||
"default_personal_pack_name": "Personal"
|
||||
},
|
||||
"ImagePackItem": {
|
||||
"rename_tooltip": "Rename",
|
||||
"delete_tooltip": "Delete"
|
||||
},
|
||||
"ImagePackUsageSelector": {
|
||||
"header": "Usage",
|
||||
"type_emoji": "Emoji",
|
||||
"type_sticker": "Sticker",
|
||||
"type_both": "Both"
|
||||
},
|
||||
"ImagePackUpload": {
|
||||
"remove_file_tooltip": "Remove file",
|
||||
"import_image_button": "Import image",
|
||||
"shortcode_placeholder": "shortcode",
|
||||
"upload_button": "Upload",
|
||||
"upload_button_progress": "Uploading..."
|
||||
},
|
||||
"ImagePackProfile": {
|
||||
"attribution_prompt": "Attribution",
|
||||
"name_prompt": "Name",
|
||||
"edit_tooltip": "Edit",
|
||||
"usage_header": "Usage",
|
||||
"pack_usage": "Pack usage"
|
||||
},
|
||||
"SpaceAddExisting": {
|
||||
"adding_items_one": "Adding one item...",
|
||||
"adding_items_other": "Adding {{count}} items...",
|
||||
"items_selected_one": "{{count}} item selected",
|
||||
"items_selected_other": "{{count}} items selected",
|
||||
"search_rooms_placeholder": "Search rooms",
|
||||
"no_results": "No results found",
|
||||
"add_button": "Add",
|
||||
"subtitle": "add existing rooms"
|
||||
},
|
||||
"SpaceOptions": {
|
||||
"leave_space": "Leave Space",
|
||||
"leave_space_confirmation": "Are you sure that you want to leave the {{space}} space?",
|
||||
"leave_space_confirm": "Leave",
|
||||
"invite": "Invite",
|
||||
"manage_rooms": "Manage rooms",
|
||||
"settings": "Settings",
|
||||
"leave": "Leave"
|
||||
},
|
||||
"SSOButtons": {
|
||||
"login_with": "Login with {{idp_name}}"
|
||||
}
|
||||
},
|
||||
"Templates": {
|
||||
"Auth": {
|
||||
"loading_homeserver_list": "Loading homeserver list...",
|
||||
"looking_for_homeserver": "Looking for homeserver...",
|
||||
"connecting_to_homeserver": "Connecting to {{homeserver}}...",
|
||||
"unable_to_connect": "Unable to connect. Please check your input",
|
||||
"username_taken": "Username is already taken",
|
||||
"registration_in_progress": "Registration in progress...",
|
||||
"link_about": "About",
|
||||
"link_twitter": "Twitter",
|
||||
"link_matrix": "Powered by Matrix",
|
||||
"dont_have_an_account": "Dont have an account?",
|
||||
"already_have_an_account": "Already have an account?",
|
||||
"register_link": "Register",
|
||||
"login_link": "Login",
|
||||
"redirecting": "Redirecting...",
|
||||
"login_in_progress": "Login in progress...",
|
||||
"bad_localpart_error": "Username can only contain characters a-z, 0-9, or '=_-./'",
|
||||
"user_id_too_long_error": "Your user ID, including the hostname, can't be more than 255 characters long.",
|
||||
"bad_password_error": "Password must contain at least 1 lowercase, 1 uppercase, 1 number, 1 non-alphanumeric character, 8-127 characters with no space.",
|
||||
"confirm_password_error": "Passwords don't match.",
|
||||
"bad_email_error": "Invalid email address",
|
||||
"invalid_password": "Invalid password",
|
||||
"check_credentials": "Please check your credentials",
|
||||
"login_types": {
|
||||
"username": "Username",
|
||||
"email": "Email"
|
||||
},
|
||||
"homeserver_form": {
|
||||
"title": "Homeserver",
|
||||
"header": "Homeserver list"
|
||||
},
|
||||
"login_form": {
|
||||
"title": "Login",
|
||||
"prompt_email": "Email",
|
||||
"prompt_username": "Username",
|
||||
"prompt_password": "Password",
|
||||
"login_button": "Login"
|
||||
},
|
||||
"register_form": {
|
||||
"title": "Register",
|
||||
"prompt_email_required": "Email (Required)",
|
||||
"prompt_email_optional": "Email",
|
||||
"prompt_username": "Username",
|
||||
"prompt_password": "Password",
|
||||
"prompt_confirm_password": "Confirm password",
|
||||
"register_button": "Register"
|
||||
},
|
||||
"terms_and_conditions": {
|
||||
"title": "Agree with terms",
|
||||
"description": "In order to complete registration, you need to agree to the terms and conditions.",
|
||||
"accept": "I accept <a>Terms and Conditions</a>",
|
||||
"submit_button": "Submit"
|
||||
},
|
||||
"validate_email": {
|
||||
"title": "Verify email",
|
||||
"continue_button": "Continue",
|
||||
"message": "Please check your email <bold>{{email_address}}</bold> and validate before continuing further"
|
||||
},
|
||||
"captcha": {
|
||||
"message": "Pleace check the box below to proceed"
|
||||
}
|
||||
},
|
||||
"Client": {
|
||||
"loading_messages": {
|
||||
"default": "Heating up",
|
||||
"message_one": "Almost there...",
|
||||
"message_two": "Looks like you have a lot of stuff to heat up!"
|
||||
},
|
||||
"logout_prompt": "Logout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,889 @@
|
||||
{
|
||||
"translation": {
|
||||
"common": {
|
||||
"close": "Close",
|
||||
"open": "Open",
|
||||
"leave": "Leave",
|
||||
"options": "Options",
|
||||
"cinny": "Cinny",
|
||||
"slogan": "Yet another matrix client",
|
||||
"source_code": "Source code",
|
||||
"sponsor": "Support",
|
||||
"retry": "Retry",
|
||||
"delete": "Delete",
|
||||
"continue": "Continue",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"view_more": "View more",
|
||||
"view_less": "View less",
|
||||
"copy": "Copy",
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"or": "Or",
|
||||
"reset": "Reset",
|
||||
"setup": "Setup",
|
||||
"search": "Search",
|
||||
"loading": "Loading...",
|
||||
"joining": "Joining...",
|
||||
"join": "Join",
|
||||
"remove": "Remove",
|
||||
"send": "Send",
|
||||
"homeserver": "Homeserver",
|
||||
"invite": "Invite",
|
||||
"uninvite": "Uninvite",
|
||||
"invited": "Invited",
|
||||
"inviting": "Inviting...",
|
||||
"uninviting": "Uninviting...",
|
||||
"change": "Change",
|
||||
"edit": "Edit",
|
||||
"message_prompt": "Message"
|
||||
},
|
||||
"errors": {
|
||||
"browser_not_supported": "Not supported in this browser",
|
||||
"generic": "Something went wrong!"
|
||||
},
|
||||
"Organisms": {
|
||||
"Welcome": {
|
||||
"heading": "Welcome to Cinny!",
|
||||
"subheading": "Yet another Matrix client"
|
||||
},
|
||||
"ViewSource": {
|
||||
"title": "View Source",
|
||||
"original_source": "Original source",
|
||||
"decrypted_source": "Decrypted source"
|
||||
},
|
||||
"SpaceManage": {
|
||||
"subtitle": "manage rooms",
|
||||
"load_more": "Load more",
|
||||
"rooms_and_spaces": "Rooms and spaces",
|
||||
"private_rooms_message": "Either the space contains private rooms or you need to join space to view it's rooms.",
|
||||
"items_selected_zero": "No selected items",
|
||||
"items_selected_one": "{{count}} selected item",
|
||||
"items_selected_other": "{{count}} selected items",
|
||||
"room_members_zero": "No room members",
|
||||
"room_members_one": "{{count}} room member",
|
||||
"room_members_other": "{{count}} room members",
|
||||
"mark_suggested_zero": "Marking no rooms as suggested",
|
||||
"mark_suggested_one": "Marking {{count}} room as suggested",
|
||||
"mark_suggested_other": "Marking {{count}} rooms as suggested",
|
||||
"mark_not_suggested_zero": "Marking no rooms as suggested",
|
||||
"mark_not_suggested_one": "Marking {{count}} room as suggested",
|
||||
"mark_not_suggested_other": "Marking {{count}} rooms as suggested",
|
||||
"remove_zero": "Removing no items",
|
||||
"remove_one": "Removing {{count}} item",
|
||||
"remove_other": "Removing {{count}} items.",
|
||||
"suggested": "Suggested",
|
||||
"mark_as_suggested": "Mark as suggested",
|
||||
"mark_as_not_suggested": "Mark as not suggested"
|
||||
},
|
||||
"SpaceSettings": {
|
||||
"subtitle": "space settings",
|
||||
"leave": {
|
||||
"leave_space": "Leave Space",
|
||||
"leave_dialog_title": "Leave Space",
|
||||
"leave_dialog_message": "Are you sure you want to leave {{space}}?"
|
||||
},
|
||||
"visibility": {
|
||||
"header": "Space visibility (who can join)"
|
||||
},
|
||||
"addresses": {
|
||||
"header": "Space addresses"
|
||||
},
|
||||
"categorize_subspaces": "Categorize subspaces",
|
||||
"uncategorize_subspaces": "Uncategorize subspaces",
|
||||
"pin_sidebar": "Pin to sidebar",
|
||||
"unpin_sidebar": "Unpin from sidebar"
|
||||
},
|
||||
"Settings": {
|
||||
"title": "设置",
|
||||
"theme": {
|
||||
"follow_system": {
|
||||
"title": "Follow system theme",
|
||||
"description": "Use light or dark mode based on the system settings."
|
||||
},
|
||||
"title": "主题",
|
||||
"theme_light": "Light",
|
||||
"theme_silver": "Silver",
|
||||
"theme_dark": "Dark",
|
||||
"theme_butter": "Butter"
|
||||
},
|
||||
"markdown": {
|
||||
"title": "Markdown formatting",
|
||||
"description": "Format messages with markdown before sending"
|
||||
},
|
||||
"hide_membership_events": {
|
||||
"title": "Hide membership events",
|
||||
"description": "Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)"
|
||||
},
|
||||
"hide_nickname_avatar_events": {
|
||||
"title": "Hide nick/avatar events",
|
||||
"description": "Hide nickname and avatar change messages from the room timeline."
|
||||
},
|
||||
"notifications_and_sound": {
|
||||
"title": "Notifications & Sound",
|
||||
"desktop": {
|
||||
"title": "Desktop notifications",
|
||||
"description": "Show desktop notifications when new messages arrive."
|
||||
},
|
||||
"sound": {
|
||||
"title": "Notification sound",
|
||||
"description": "Play a sound when new messages arrive."
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"cross_signing": {
|
||||
"title": "Cross signing and backup"
|
||||
},
|
||||
"export_import_encryption_keys": {
|
||||
"title": "Export / Import encryption keys"
|
||||
},
|
||||
"export_encryption_keys": {
|
||||
"title": "Export E2E room keys",
|
||||
"description": "Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing."
|
||||
},
|
||||
"import_encryption_keys": {
|
||||
"title": "Import E2E room keys",
|
||||
"description": "To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\\'ll have to enter the password you set in order to decrypt it."
|
||||
}
|
||||
},
|
||||
"logout": {
|
||||
"title": "Logout",
|
||||
"dialog": {
|
||||
"title": "Logout",
|
||||
"description": "Are you sure that you want to logout your session?",
|
||||
"confirm": "Logout"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"application": "Application",
|
||||
"credits": "Credits"
|
||||
}
|
||||
},
|
||||
"ShortcutSpaces": {
|
||||
"header": "Pin Spaces",
|
||||
"pinned_spaces": "Pinned spaces",
|
||||
"no_pinned_spaces": "No pinned spaces",
|
||||
"unpinned_spaces": "Unpinned spaces",
|
||||
"no_unpinned_spaces": "No unpinned spaces",
|
||||
"spaces_selected_zero": "No selected spaces",
|
||||
"spaces_selected_one": "{{count}} selected space",
|
||||
"spaces_selected_other": "{{count}} selected spaces",
|
||||
"pin_button": "Pin"
|
||||
},
|
||||
"SecretStorageAccess": {
|
||||
"incorrect_security_key": "Incorrect security key",
|
||||
"incorrect_security_phrase": "Incorrect security phrase",
|
||||
"security_phrase": "Security Phrase",
|
||||
"security_key": "Security Key",
|
||||
"use_security_key": "Use Security Key",
|
||||
"use_security_phrase": "Use Security Phrase"
|
||||
},
|
||||
"KeyBackup": {
|
||||
"create_backup_title": "Create key backup",
|
||||
"create_backup_tooltip": "Create backup",
|
||||
"creating_backup": "Creating Backup...",
|
||||
"backup_created": "Successfully created backup",
|
||||
"backup_failed": "Failed to create backup",
|
||||
"restoring": "Restoring backup keys...",
|
||||
"restoring_progress": "Restoring backup keys... ({{progress}}/{{total}}",
|
||||
"restore_backup_title": "Restore Key Backup",
|
||||
"restore_backup_tooltip": "Restore Key Backup",
|
||||
"restore_complete": "Successfully restored backup keys ({{progress}}/{{total}})",
|
||||
"restore_failed_bad_key": "Failed to restore backup. Key is invalid!",
|
||||
"restore_failed_unknown": "Failed to restore backup.",
|
||||
"delete_key_backup_title": "Delete key backup",
|
||||
"delete_key_backup_tooltip": "Delete backup",
|
||||
"delete_key_backup_subtitle": "Deleting key backup is permanent.",
|
||||
"delete_key_backup_message": "All encrypted message keys stored on the server will be permanently deleted.",
|
||||
"encrypted_messages_backup_description": "Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.",
|
||||
"encrypted_messages_backup_title": "Encrypted messages backup",
|
||||
"encrypted_messages_backup_cross_signing_disabled": "Setup cross signing to backup your encrypted messages."
|
||||
},
|
||||
"DeviceManage": {
|
||||
"edit_session_name_title": "Edit session name",
|
||||
"edit_session_name_subtitle": "Session name",
|
||||
"edit_session_name_tooltip": "Edit session name",
|
||||
"current_device_label": "Current",
|
||||
"verify_session_button": "Verify",
|
||||
"unverified_sessions_title": "Unverified sessions",
|
||||
"unverified_sessions_none": "No unverified sessions",
|
||||
"unencrypted_sessions_title": "Sessions without encryption support",
|
||||
"verified_sessions_title": "Verified sessions",
|
||||
"verified_sessions_none": "No verified sessions",
|
||||
"setup_cross_signing_message": "Setup cross signing in case you lose all your sessions",
|
||||
"loading_devices": "Loading devices...",
|
||||
"logout_device_title": "Logout {{device}}",
|
||||
"logout_device_message": "You are about to log out the session for {{device}}",
|
||||
"logout_device_confirm": "Logout",
|
||||
"logout_device_tooltip": "Remove session",
|
||||
"session_verification_title": "Session Verification",
|
||||
"session_name_privacy_message": "Session names are visible to everyone, so do not put any private info here."
|
||||
},
|
||||
"CrossSigning": {
|
||||
"title": "Cross Signing",
|
||||
"setup_failed": "Failed to setup cross signing. Please try again",
|
||||
"setup": "Setup cross signing",
|
||||
"save_security_key_message": "Please save this security key somewhere safe",
|
||||
"security_key_dialog_title": "Security Key",
|
||||
"security_key_generation_message": "We will generate a Security Key, which you can use to manage message backups and session verification.",
|
||||
"security_key_generation_button": "Generate Key",
|
||||
"security_phrase_message": "Alternatively you can set a 'Security Phrase' so you don't have to remember the long Security Key, and optionally save the key as a backup",
|
||||
"security_phrase_label": "Security Phrase",
|
||||
"security_phrase_confirm_label": "Confirm Security Phrase",
|
||||
"security_phrase_set_button": "Set Phrase & Generate Key",
|
||||
"setup_dialog_title": "Setup cross signing",
|
||||
"setup_message": "Setup to verify and keep track of all your sessions. Also required to backup encrypted message.",
|
||||
"reset_keys_subtitle": "Resetting cross-signing keys is permanent.",
|
||||
"reset_keys_message": "Anyone you have verified with will see security alerts and your message backup will lost. You almost certainly do not want to do this, unless you have lost Security Key or Phrase and every session you can cross-sign from."
|
||||
},
|
||||
"AuthRequest": {
|
||||
"wrong_password": "Wrong password. Please enter the correct password",
|
||||
"request_failed": "Request failed!",
|
||||
"password_label": "Account password"
|
||||
},
|
||||
"Search": {
|
||||
"description": "Type # for rooms, @ for DMs and * for spaces. Hotkey: Ctrl + k"
|
||||
},
|
||||
"RoomViewInput": {
|
||||
"upload_progress": "Uploading: {{progress}}/{{total}} ({{percent}}%)",
|
||||
"tombstone_replaced": "This room has been replaced, and is no longer active.",
|
||||
"tombstone_permission_denied": "You do not have permission to post to this room",
|
||||
"send_message_placeholder": "Send a message...",
|
||||
"emoji_tooltip": "Emoji",
|
||||
"file_size": "Size: {{size}}",
|
||||
"cancel_reply_tooltip": "Cancel reply"
|
||||
},
|
||||
"RoomViewHeader": {
|
||||
"search_tooltip": "Search",
|
||||
"people_tooltip": "People",
|
||||
"members_tooltip": "Members"
|
||||
},
|
||||
"RoomViewFloating": {
|
||||
"jump_unread": "Jump to unread messages",
|
||||
"mark_read": "Mark as read",
|
||||
"jump_latest": "Jump to latest",
|
||||
"user_typing_1": "<bold><user_one/></bold> is typing...",
|
||||
"user_typing_2": "<bold><user_one/></bold> and <bold><user_two/></bold> are typing...",
|
||||
"user_typing_3": "<bold><user_one/></bold>, <bold><user_two/></bold> and <bold><user_three/></bold> are typing...",
|
||||
"user_typing_4": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and <bold><user_four/></bold> are typing...",
|
||||
"user_typing_other": "<bold>Several people</bold> are typing..."
|
||||
},
|
||||
"RoomViewContent": {
|
||||
"welcome_to_room": "Welcome to {{room_name}}!",
|
||||
"beginning_room": "This is the beginning of the <bold>{{room_name}}</bold> room.",
|
||||
"beginning_dm": "This is the beginning of your direct message history with <bold>@{{user_name}}</bold>.",
|
||||
"created_on": "Created on {{date, datetime}}",
|
||||
"new_messages": "New messages"
|
||||
},
|
||||
"RoomSettings": {
|
||||
"leave_room": "Leave room",
|
||||
"leave_room_confirm_message": "Are you sure you want to leave {{room_name}}?",
|
||||
"leave_room_confirm_button": "Leave",
|
||||
"notification_header": "Notifications (Changing this will only affect you)",
|
||||
"visibility_header": "Room visibility (Who can join)",
|
||||
"address_header": "Room addresses",
|
||||
"encryption_header": "Encryption",
|
||||
"message_history_header": "Message history visibility",
|
||||
"room_settings_subtitle": "room settings"
|
||||
},
|
||||
"RoomCommon": {
|
||||
"user_joined": "<bold><user/></bold> joined the room",
|
||||
"user_left": "<bold><user/></bold> left the room",
|
||||
"user_left_reason": "<bold><user/></bold> left the room: <reason/>",
|
||||
"user_invited": "<bold><inviter/></bold> invited <bold><user/></bold>",
|
||||
"invite_cancelled": "<bold><inviter/></bold> cancelled <bold><user/>'s</bold> invite",
|
||||
"invite_rejected": "<bold><user/></bold> rejected the invitation",
|
||||
"user_kicked": "<bold><actor/></bold> kicked <bold><user/></bold>",
|
||||
"user_kicked_reason": "<bold><actor/></bold> kicked <bold><user/></bold>: <reason/>",
|
||||
"user_banned": "<bold><actor/></bold> banned <bold><user/></bold>",
|
||||
"user_banned_reason": "<bold><actor/></bold> banned <bold><user/></bold>: <reason/>",
|
||||
"user_unbanned": "<bold><actor/> unbanned <bold><user/></bold>",
|
||||
"avatar_set": "<bold><user/></bold> set an avatar",
|
||||
"avatar_changed": "<bold><user/></bold> changed their avatar",
|
||||
"avatar_removed": "<bold><user/></bold> removed their avatar",
|
||||
"name_set": "<bold><user/></bold> set their display name to <bold><new_name/></bold>",
|
||||
"name_changed": "<bold><user/></bold> changed their display name to <bold><new_name/></bold>",
|
||||
"name_removed": "<bold><user/></bold> removed their display name <bold><last_name/></bold>"
|
||||
},
|
||||
"PublicRooms": {
|
||||
"could_not_join_alias": "Unable to join {{alias}}. Either the room is private or doesn't exist",
|
||||
"try_joining_alias": "Try joining {{alias}}",
|
||||
"joining_alias": "Joining {{alias}}...",
|
||||
"no_public_rooms": "No public rooms on {{homeserver}}",
|
||||
"no_result_found": "No result found for '{{input}}' on {{homeserver}}",
|
||||
"title": "Public Rooms",
|
||||
"search_room_name_alias": "Room name or alias",
|
||||
"search_button": "Search",
|
||||
"loading": "Loading public rooms from {{homeserver}}...",
|
||||
"searching": "Searching for '{{query}}' on {{homeserver}}...",
|
||||
"result_title": "Public rooms on {{homeserver}}",
|
||||
"search_result_title": "Search result for '{{query}}' on {{homeserver}}"
|
||||
},
|
||||
"ProfileViewer": {
|
||||
"kick_button": "Kick",
|
||||
"kick_reason_label": "Kick Reason",
|
||||
"ban_button": "Ban",
|
||||
"ban_reason_label": "Ban reason",
|
||||
"loading_sessions": "Loading sessions...",
|
||||
"no_sessions_found": "No sessions found.",
|
||||
"view_sessions_one": "View session",
|
||||
"view_sessions_other": "View {{count}} sessions",
|
||||
"send_direct_message_button": "Message",
|
||||
"creating_dm_room": "Creating room...",
|
||||
"ignore": "Ignore",
|
||||
"ignoring": "Ignoring...",
|
||||
"unignore": "Unignore",
|
||||
"unignoring": "Unignoring...",
|
||||
"change_power_level": "Change power level",
|
||||
"shared_power_message": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?",
|
||||
"demoting_self_message": "You will not be able to undo this change as you are demoting yourself. Are you sure?"
|
||||
},
|
||||
"PeopleDrawer": {
|
||||
"title": "People",
|
||||
"members_one": "{{count}} member",
|
||||
"members_other": "{{count}} members",
|
||||
"joined": "Joined",
|
||||
"invited": "Invited",
|
||||
"banned": "Banned",
|
||||
"search_no_results": "No results found",
|
||||
"view_more": "View more",
|
||||
"placeholder": "Search",
|
||||
"invite_tooltip": "Invite"
|
||||
},
|
||||
"ProfileEditor": {
|
||||
"remove_avatar": "Remove avatar",
|
||||
"remove_avatar_confirmation": "Are you sure that you want to remove your avatar?",
|
||||
"display_name_message": "Display name of {{user_name}}"
|
||||
},
|
||||
"DrawerBreadcrumb": {
|
||||
"home": "Home"
|
||||
},
|
||||
"DrawerHeader": {
|
||||
"add_rooms_or_spaces": "Add rooms or spaces",
|
||||
"create_new_space": "Create new space",
|
||||
"create_new_room": "Create new room",
|
||||
"join_public_room": "Join public room",
|
||||
"join_with_address": "Join with address",
|
||||
"add_existing": "Add existing",
|
||||
"manage_rooms": "Manage rooms",
|
||||
"home": "Home",
|
||||
"direct_messages": "Direct messages",
|
||||
"start_dm_tooltip": "Start DM",
|
||||
"add_rooms_spaces_tooltip": "Add rooms/spaces"
|
||||
},
|
||||
"SideBar": {
|
||||
"settings_tooltip": "Settings",
|
||||
"unverified_sessions_one": "{{count}} unverified session",
|
||||
"unverified_sessions_other": "{{count}} unverified sessions",
|
||||
"home_tooltip": "Home",
|
||||
"direct_messages_tooltip": "People",
|
||||
"pin_spaces_tooltip": "Pin spaces",
|
||||
"search_tooltip": "Search",
|
||||
"invites_tooltip": "Invites"
|
||||
},
|
||||
"JoinAlias": {
|
||||
"invalid_address": "Invalid address.",
|
||||
"looking_for_address": "Looking for address...",
|
||||
"joining_alias": "Joining {{alias_name}}...",
|
||||
"couldnt_find_room_or_space_alias": "Unable to find room/space with {{alias_name}}. Either the room/space is private or doesn't exist.",
|
||||
"couldnt_find_room_or_space": "Unable to join {{alias_name}}. Either the room/space is private or doesn't exist.",
|
||||
"address_label": "Address",
|
||||
"title": "Join with address"
|
||||
},
|
||||
"InviteUser": {
|
||||
"user_not_found": "{{user_name}} not found!",
|
||||
"no_matches_found": "No matches found for {{user_name}}",
|
||||
"invite_result": {
|
||||
"invited": "Invited",
|
||||
"already_joined": "Already joined",
|
||||
"already_invited": "Already invited",
|
||||
"banned": "Banned"
|
||||
},
|
||||
"search_label": "Name or User ID",
|
||||
"search_result_title": "Search result for user {{user_name}}",
|
||||
"searching_for_user": "Searching for user {{user_name}}...",
|
||||
"invite_to_room": "Invite to {{room}}",
|
||||
"invite_to_dm": "Direct Message"
|
||||
},
|
||||
"InviteList": {
|
||||
"accept_invite": "Accept",
|
||||
"reject_invite": "Reject",
|
||||
"direct_messages_title": "Direct Messages",
|
||||
"rooms_title": "Rooms",
|
||||
"spaces_title": "Spaces",
|
||||
"title": "Invites"
|
||||
},
|
||||
"EmojiVerification": {
|
||||
"waiting_for_response": "Waiting for response from other device...",
|
||||
"confirmation_prompt": "Confirm the emoji below are displayed on both devices, in the same order:",
|
||||
"emojis_match_button": "They match",
|
||||
"emojis_dont_match_button": "They don't match",
|
||||
"accept_request_from_other_device_message": "Please accept the request from other device.",
|
||||
"begin_verification_process_message": "Click accept to start the verification process.",
|
||||
"begin_verification_button_text": "Accept",
|
||||
"title": "Emoji Verification"
|
||||
},
|
||||
"DragDrop": {
|
||||
"drop_file_to_upload_prompt": "Drop file to upload"
|
||||
},
|
||||
"CreateRoom": {
|
||||
"private_room_short": "Private",
|
||||
"restricted_room_short": "Restricted",
|
||||
"public_room_short": "Public",
|
||||
"private_room_long": "Private (invite only)",
|
||||
"restricted_room_long": "Restricted (space member can join)",
|
||||
"public_room_long": "Public (anyone can join)",
|
||||
"visibility_title": "Visibility",
|
||||
"visibility_message": "Visibility (who can join)",
|
||||
"select_who_can_join_space": "Select who can join this space",
|
||||
"select_who_can_join_room": "Select who can join this room",
|
||||
"space_address": "Space address",
|
||||
"room_address": "Room address",
|
||||
"room_address_already_in_use": "{{room_address}} is already in use",
|
||||
"e2e_title": "Enable end-to-end encryption",
|
||||
"e2e_message": "You can’t disable this later. Bridges & most bots won’t work yet.",
|
||||
"role_title": "Select your role",
|
||||
"role_message": "Selecting 'Admin' sets your power level to 100 whereas 'Founder' sets it to 101.",
|
||||
"creating_room": "Creating room...",
|
||||
"creating_space": "Creating space...",
|
||||
"topic_label": "Topic (optional)",
|
||||
"space_name": "Space name",
|
||||
"room_name": "Room name",
|
||||
"role_admin": "Admin",
|
||||
"role_founder": "Founder",
|
||||
"create_room": "Create room",
|
||||
"create_space": "Create space",
|
||||
"home": "Home"
|
||||
}
|
||||
},
|
||||
"Molecules": {
|
||||
"ConfirmDialog": {
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"ReusableDialog": {
|
||||
"close_tooltip": "Close"
|
||||
},
|
||||
"FollowingMembers": {
|
||||
"users_following_1": "<bold><user_one/></bold> is following the conversation",
|
||||
"users_following_2": "<bold><user_one/></bold> and <bold><user_two/></bold> are following the conversation",
|
||||
"users_following_3": "<bold><user_one/></bold>, <bold><user_two/></bold>, and <bold><user_three/></bold> are following the conversation",
|
||||
"users_following_other": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and {{other_count}} others are following the conversation"
|
||||
},
|
||||
"ImageUpload": {
|
||||
"prompt": "Upload",
|
||||
"cancel": "Cancel",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"ExportE2ERoomKeys": {
|
||||
"getting_keys": "Getting keys...",
|
||||
"encrypting_keys": "Encrypting Keys...",
|
||||
"password_does_not_match": "Password does not match.",
|
||||
"export_success": "Successfully exported all keys.",
|
||||
"export_failed": "Failed to export keys. Please try again.",
|
||||
"button_text": "Export"
|
||||
},
|
||||
"ImportE2ERoomKeys": {
|
||||
"decrypting_file": "Decrypting file...",
|
||||
"decrypting_messages": "Decrypting messages...",
|
||||
"import_success": "Successfully imported all keys",
|
||||
"import_failed": "Failed tp decrypt keys. Please try again",
|
||||
"import_keys_button": "Import Keys",
|
||||
"decrypt_button": "Decrypt"
|
||||
},
|
||||
"Media": {
|
||||
"open_new_tab": "Open in new tab",
|
||||
"download": "Download",
|
||||
"play_audio": "Play audio",
|
||||
"play_video": "Play video"
|
||||
},
|
||||
"Message": {
|
||||
"message_deleted": "*** This message has been deleted ***",
|
||||
"unable_to_load_reply": "*** Unable to load reply ***",
|
||||
"unknown_user": "** Unknown user **",
|
||||
"edited": "(edited)",
|
||||
"edit_placeholder": "Edit message",
|
||||
"user_reacted_1": "<bold><user_one/></bold> reacted with <emoji/>",
|
||||
"user_reacted_2": "<bold><user_one/></bold> and <bold><user_two/></bold> reacted with <emoji/>",
|
||||
"user_reacted_3": "<bold><user_one/></bold>, <bold><user_two/></bold> and <bold><user_three/></bold> reacted with <emoji/>",
|
||||
"user_reacted_other": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and {{other_count}} others reacted with <emoji/>",
|
||||
"add_reaction_tooltip": "Add reaction",
|
||||
"reply_tooltip": "Reply",
|
||||
"edit_tooltip": "Edit",
|
||||
"options_header": "Options",
|
||||
"options_tooltip": "Options",
|
||||
"read_receipts": "Read receipts",
|
||||
"view_source": "View source",
|
||||
"delete_message_prompt": "Delete message",
|
||||
"delete_message_confirmation": "Are you sure that you want to delete this message?",
|
||||
"delete_message_button": "Delete"
|
||||
},
|
||||
"PopupWindow": {
|
||||
"close_tooltip": "Back"
|
||||
},
|
||||
"PowerLevelSelector": {
|
||||
"placeholder": "Power level"
|
||||
},
|
||||
"RoomAliases": {
|
||||
"invalid_characters": "Invalid character: only letter, numbers and _- are allowed.",
|
||||
"validating_alias": "validating {{alias}}...",
|
||||
"alias_available": "{{alias}} is available.",
|
||||
"alias_unavailable": "{{alias}} is unavailable.",
|
||||
"deleting_alias": "Deleting...",
|
||||
"set_main_alias": "Set as Main",
|
||||
"publish_alias": "Publish",
|
||||
"unpublish_alias": "Unpublish",
|
||||
"delete_alias": "Delete",
|
||||
"main_alias": "Main",
|
||||
"publish_to_room_directory": {
|
||||
"title": "Publish to room directory",
|
||||
"publish_room_message": "Publish this room to the {{homeserver}} publish directory?",
|
||||
"publish_space_message": "Publish this space to the {{homeserver}} publish directory?"
|
||||
},
|
||||
"published_addresses": {
|
||||
"title": "Published addresses",
|
||||
"none": "No published addresses",
|
||||
"no_main_address": "No main address (select one from below)",
|
||||
"message_room": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
|
||||
"message_space": "Published addresses can be used by anyone on any server to join your space. To publish an address, it needs to be set as a local address first."
|
||||
},
|
||||
"local_addresses": {
|
||||
"title": "Local addresses",
|
||||
"none": "No local addresses",
|
||||
"message_room": "Set local addresses for this room so users can find this room through your homeserver.",
|
||||
"message_space": "Set local addresses for this space so users can find this space through your homeserver.",
|
||||
"add": "Add local address",
|
||||
"add_button": "Add",
|
||||
"placeholder_room": "my_room_address",
|
||||
"placeholder_space": "my_space_address",
|
||||
"hide": "Hide local addresses",
|
||||
"view": "View local addresses"
|
||||
}
|
||||
},
|
||||
"RoomEncryption": {
|
||||
"encryption_public_room_message": "It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone should be able to read messages in them.",
|
||||
"encryption_message": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly",
|
||||
"encryption_cannot_be_disabled": "Once enabled, encryption cannot be disabled.",
|
||||
"enable_room_encryption": "Enable room encryption",
|
||||
"enable_encryption_prompt": "Enable Encryption",
|
||||
"enable_encryption_button": "Enable",
|
||||
"continue_button": "Continue"
|
||||
},
|
||||
"RoomHistoryVisibility": {
|
||||
"world_readable": "Anyone (including guests)",
|
||||
"shared": "Members (all messages)",
|
||||
"invited": "Members (messages after invite)",
|
||||
"joined": "Members (messages after join)",
|
||||
"changes_only_affect_future": "Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect."
|
||||
},
|
||||
"RoomMembers": {
|
||||
"search_title": "Search member",
|
||||
"found_members_one": "Found — one member",
|
||||
"found_members_other": "Found — {{count}} members",
|
||||
"no_results": "No results found for {{term}}",
|
||||
"no_members": "No members to display",
|
||||
"view_more": "View more",
|
||||
"joined": "Joined",
|
||||
"invited": "Invited",
|
||||
"banned": "Banned",
|
||||
"search_placeholder": "Search for name"
|
||||
},
|
||||
"RoomNotification": {
|
||||
"default": "Global",
|
||||
"all_messages": "All messages",
|
||||
"mentions_and_keywords": "Mentions & Keywords",
|
||||
"mute": "Mute"
|
||||
},
|
||||
"RoomOptions": {
|
||||
"title": "Options for {{room_name}}",
|
||||
"leave": {
|
||||
"title": "Leave room",
|
||||
"subtitle": "Are you sure you want to leave the {{room_name}} room?",
|
||||
"button_text": "Leave"
|
||||
},
|
||||
"mark_as_read": "Mark as read",
|
||||
"notifications_heading": "Notifications",
|
||||
"invite": "Invite"
|
||||
},
|
||||
"RoomPermissions": {
|
||||
"default_role": {
|
||||
"name": "Default role",
|
||||
"description": "Set default role for all members"
|
||||
},
|
||||
"send_messages": {
|
||||
"name": "Send messages",
|
||||
"description": "Set minimum power level to send messages in a room"
|
||||
},
|
||||
"reactions": {
|
||||
"name": "Send reactions",
|
||||
"description": "Set minimum power level to send reactions in a room"
|
||||
},
|
||||
"delete": {
|
||||
"name": "Delete messages sent by others",
|
||||
"description": "Set minumum power level to delete messages in a room"
|
||||
},
|
||||
"notifications": {
|
||||
"name": "Ping room",
|
||||
"description": "Set minimum power level to ping room"
|
||||
},
|
||||
"manage_rooms": {
|
||||
"name": "Manage rooms in space",
|
||||
"description": "Set minimum power level to manage rooms in space"
|
||||
},
|
||||
"invite": {
|
||||
"name": "Invite",
|
||||
"description": "Set minimum power level to invite members"
|
||||
},
|
||||
"kick": {
|
||||
"name": "Kick",
|
||||
"description": "Set minimum power level to kick members"
|
||||
},
|
||||
"ban": {
|
||||
"name": "Ban",
|
||||
"description": "Set minimum power level to ban members"
|
||||
},
|
||||
"change_avatar": {
|
||||
"name": "Change avatar",
|
||||
"description": "Set minimum power level to change room/space avatar"
|
||||
},
|
||||
"change_name": {
|
||||
"name": "Change name",
|
||||
"description": "Set minimum power evel to change room/space name"
|
||||
},
|
||||
"change_topic": {
|
||||
"name": "Change topic",
|
||||
"description": "Set minimum power level to change room/space topic"
|
||||
},
|
||||
"change_settings": {
|
||||
"name": "Change settings",
|
||||
"description": "Set minimum power level to change settings"
|
||||
},
|
||||
"change_published_address": {
|
||||
"name": "Change published address",
|
||||
"description": "Set minimum power level to publish and set main address"
|
||||
},
|
||||
"change_permissions": {
|
||||
"name": "Change permissions",
|
||||
"description": "Set minimum power level to change permissions"
|
||||
},
|
||||
"enable_room_encryption": {
|
||||
"name": "Enable room encryption",
|
||||
"description": "Set minimum power level to enable room encryption"
|
||||
},
|
||||
"change_history_visibility": {
|
||||
"name": "Change history visibility",
|
||||
"description": "Set minimum power level to change room message history visibility"
|
||||
},
|
||||
"upgrade_room": {
|
||||
"name": "Upgrade room",
|
||||
"description": "Set minimum power level to upgrade room"
|
||||
},
|
||||
"pin_messages": {
|
||||
"name": "Pin messages",
|
||||
"description": "Set minimum power level to pin messages in a room"
|
||||
},
|
||||
"change_acls": {
|
||||
"name": "Change server ACLs",
|
||||
"description": "Set minimum power level to change server ACLs"
|
||||
},
|
||||
"modify_widgets": {
|
||||
"name": "Modify widgets",
|
||||
"description": "Set minimum power level to modify room widgets"
|
||||
},
|
||||
"groups": {
|
||||
"general": "General permissions",
|
||||
"manage_members": "Manage members permissions",
|
||||
"room": "Room profile permissions",
|
||||
"space": "Space profile permissions",
|
||||
"other": "Other permissions",
|
||||
"settings": "Settings permissions"
|
||||
}
|
||||
},
|
||||
"RoomProfile": {
|
||||
"saving_room_name": "Saving room name...",
|
||||
"saving_room_topic": "Saving room topic...",
|
||||
"save_success": "Saved successfully",
|
||||
"save_failed": "Unable to save",
|
||||
"remove_avatar_title": "Remove avatar",
|
||||
"remove_avatar_subtitle": "Are you sure that you want to remove room avatar?",
|
||||
"remove_avatar_button": "Remove",
|
||||
"permission_change_room_name": "You have permission to change room name only",
|
||||
"permission_change_room_topic": "You have permission to change room topic only",
|
||||
"permission_change_space_name": "You have permission to change space name only",
|
||||
"permission_change_space_topic": "You have permission to change space topic only",
|
||||
"name_label": "Name",
|
||||
"topic_label": "Topic"
|
||||
},
|
||||
"RoomSearch": {
|
||||
"title": "Room search",
|
||||
"placeholder": "Search for keywords",
|
||||
"search_button": "Search",
|
||||
"searching": "Searching room messages...",
|
||||
"subtitle": "Search room messages",
|
||||
"failed": "Failed to search messages",
|
||||
"no_results": "No results found",
|
||||
"encrypted_room": "Search does not work in encrypted room",
|
||||
"load_more": "Load more",
|
||||
"results_one": "{{count}} result for {{term}}",
|
||||
"results_other": "{{count}} results for {{term}}"
|
||||
},
|
||||
"RoomTile": {
|
||||
"invited_by_user_zero": "Invited by {{inviter}} to {{id}}",
|
||||
"invited_by_user_one": "Invited by {{inviter}} to {{id}} • {{member_count}} member",
|
||||
"invited_by_user_other": "Invited by {{inviter}} to {{id}} • {{member_count}} members",
|
||||
"invited_zero": "{{id}}",
|
||||
"invited_one": "{{id}} • {{member_count}} member",
|
||||
"invited_other": "{{id}} • {{member_count}} members"
|
||||
},
|
||||
"RoomVisibility": {
|
||||
"private": "Private (invite only)",
|
||||
"restricted": "Restricted (space members can join)",
|
||||
"restricted_unsupported": "Restricted (Unsupported: room required upgrade)",
|
||||
"public": "Public (anyone can join)"
|
||||
},
|
||||
"RoomEmojis": {
|
||||
"header": "Create Pack",
|
||||
"submit_button": "Create pack",
|
||||
"name_placeholder": "Pack Name",
|
||||
"no_packs": "No emoji or sticker packs."
|
||||
},
|
||||
"ImagePack": {
|
||||
"rename_title": "Rename",
|
||||
"rename_button": "Rename",
|
||||
"shortcode_label": "Shortcode",
|
||||
"delete_item_title": "Delete",
|
||||
"delete_item_message": "Are you sure that you want to delete {{key}}?",
|
||||
"delete_item_button": "Delete",
|
||||
"delete_pack_title": "Delete Pack",
|
||||
"delete_pack_message": "Are you sure that you want to delete {{pack_name}}?",
|
||||
"delete_pack_button": "Delete",
|
||||
"image_title": "Image",
|
||||
"image_shortcode": "Shortcode",
|
||||
"image_usage": "Usage",
|
||||
"view_more": "View {{count}} more",
|
||||
"view_less": "View less",
|
||||
"use_globally": "Use globally",
|
||||
"use_globally_description": "Add this pack to your account to use in all rooms.",
|
||||
"no_global_packs": "No global packs",
|
||||
"default_personal_pack_name": "Personal"
|
||||
},
|
||||
"ImagePackItem": {
|
||||
"rename_tooltip": "Rename",
|
||||
"delete_tooltip": "Delete"
|
||||
},
|
||||
"ImagePackUsageSelector": {
|
||||
"header": "Usage",
|
||||
"type_emoji": "Emoji",
|
||||
"type_sticker": "Sticker",
|
||||
"type_both": "Both"
|
||||
},
|
||||
"ImagePackUpload": {
|
||||
"remove_file_tooltip": "Remove file",
|
||||
"import_image_button": "Import image",
|
||||
"shortcode_placeholder": "shortcode",
|
||||
"upload_button": "Upload",
|
||||
"upload_button_progress": "Uploading..."
|
||||
},
|
||||
"ImagePackProfile": {
|
||||
"attribution_prompt": "Attribution",
|
||||
"name_prompt": "Name",
|
||||
"edit_tooltip": "Edit",
|
||||
"usage_header": "Usage",
|
||||
"pack_usage": "Pack usage"
|
||||
},
|
||||
"SpaceAddExisting": {
|
||||
"adding_items_one": "Adding one item...",
|
||||
"adding_items_other": "Adding {{count}} items...",
|
||||
"items_selected_one": "{{count}} item selected",
|
||||
"items_selected_other": "{{count}} items selected",
|
||||
"search_rooms_placeholder": "Search rooms",
|
||||
"no_results": "No results found",
|
||||
"add_button": "Add",
|
||||
"subtitle": "add existing rooms"
|
||||
},
|
||||
"SpaceOptions": {
|
||||
"leave_space": "Leave Space",
|
||||
"leave_space_confirmation": "Are you sure that you want to leave the {{space}} space?",
|
||||
"leave_space_confirm": "Leave",
|
||||
"invite": "Invite",
|
||||
"manage_rooms": "Manage rooms",
|
||||
"settings": "Settings",
|
||||
"leave": "Leave"
|
||||
},
|
||||
"SSOButtons": {
|
||||
"login_with": "Login with {{idp_name}}"
|
||||
}
|
||||
},
|
||||
"Templates": {
|
||||
"Auth": {
|
||||
"loading_homeserver_list": "Loading homeserver list...",
|
||||
"looking_for_homeserver": "Looking for homeserver...",
|
||||
"connecting_to_homeserver": "Connecting to {{homeserver}}...",
|
||||
"unable_to_connect": "Unable to connect. Please check your input",
|
||||
"username_taken": "Username is already taken",
|
||||
"registration_in_progress": "Registration in progress...",
|
||||
"link_about": "About",
|
||||
"link_twitter": "Twitter",
|
||||
"link_matrix": "Powered by Matrix",
|
||||
"dont_have_an_account": "Dont have an account?",
|
||||
"already_have_an_account": "Already have an account?",
|
||||
"register_link": "Register",
|
||||
"login_link": "Login",
|
||||
"redirecting": "Redirecting...",
|
||||
"login_in_progress": "Login in progress...",
|
||||
"bad_localpart_error": "Username can only contain characters a-z, 0-9, or '=_-./'",
|
||||
"user_id_too_long_error": "Your user ID, including the hostname, can't be more than 255 characters long.",
|
||||
"bad_password_error": "Password must contain at least 1 lowercase, 1 uppercase, 1 number, 1 non-alphanumeric character, 8-127 characters with no space.",
|
||||
"confirm_password_error": "Passwords don't match.",
|
||||
"bad_email_error": "Invalid email address",
|
||||
"invalid_password": "Invalid password",
|
||||
"check_credentials": "Please check your credentials",
|
||||
"login_types": {
|
||||
"username": "Username",
|
||||
"email": "Email"
|
||||
},
|
||||
"homeserver_form": {
|
||||
"title": "Homeserver",
|
||||
"header": "Homeserver list"
|
||||
},
|
||||
"login_form": {
|
||||
"title": "Login",
|
||||
"prompt_email": "Email",
|
||||
"prompt_username": "Username",
|
||||
"prompt_password": "Password",
|
||||
"login_button": "Login"
|
||||
},
|
||||
"register_form": {
|
||||
"title": "Register",
|
||||
"prompt_email_required": "Email (Required)",
|
||||
"prompt_email_optional": "Email",
|
||||
"prompt_username": "Username",
|
||||
"prompt_password": "Password",
|
||||
"prompt_confirm_password": "Confirm password",
|
||||
"register_button": "Register"
|
||||
},
|
||||
"terms_and_conditions": {
|
||||
"title": "Agree with terms",
|
||||
"description": "In order to complete registration, you need to agree to the terms and conditions.",
|
||||
"accept": "I accept <a>Terms and Conditions</a>",
|
||||
"submit_button": "Submit"
|
||||
},
|
||||
"validate_email": {
|
||||
"title": "Verify email",
|
||||
"continue_button": "Continue",
|
||||
"message": "Please check your email <bold>{{email_address}}</bold> and validate before continuing further"
|
||||
},
|
||||
"captcha": {
|
||||
"message": "Pleace check the box below to proceed"
|
||||
}
|
||||
},
|
||||
"Client": {
|
||||
"loading_messages": {
|
||||
"default": "Heating up",
|
||||
"message_one": "Almost there...",
|
||||
"message_two": "Looks like you have a lot of stuff to heat up!"
|
||||
},
|
||||
"logout_prompt": "Logout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "Cinny",
|
||||
"short_name": "Cinny",
|
||||
"description": "Yet another matrix client",
|
||||
"dir": "auto",
|
||||
"lang": "en-US",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"start_url": "/",
|
||||
"background_color": "#fff",
|
||||
"theme_color": "#fff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/public/android/android-chrome-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/public/android/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 800 800" style="enable-background:new 0 0 800 800;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{stroke:#000000;stroke-width:9;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M405,442.5H244.8v160.2h-67.1V442.5H17.5v-67.1h160.2V215.2h67.1v160.2H405V442.5z"/>
|
||||
<path class="st0" d="M757,697.8h-75.1V237.2c-30.4,20.2-77.4,30.4-141,30.4v-59.1c91.9,0,144.6-35.4,158.2-106.3H757V697.8z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 688 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 8L10 13M4 14L10 8L12 5M2 5H14M7 2H8M12.913 17H20.087M12.913 17L11 21M12.913 17L15.7783 11.009C16.0092 10.5263 16.1246 10.2849 16.2826 10.2086C16.4199 10.1423 16.5801 10.1423 16.7174 10.2086C16.8754 10.2849 16.9908 10.5263 17.2217 11.009L20.087 17M20.087 17L22 21" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 586 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2707_1961)">
|
||||
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
|
||||
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
|
||||
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#45B83B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2707_1961">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2707_2015)">
|
||||
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
|
||||
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
|
||||
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#989898"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2707_2015">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,8 +1,4 @@
|
||||
import { avatarInitials } from '../../../util/common';
|
||||
|
||||
function cssVar(name) {
|
||||
return getComputedStyle(document.body).getPropertyValue(name);
|
||||
}
|
||||
import { avatarInitials, cssVar } from '../../../util/common';
|
||||
|
||||
// renders the avatar and returns it as an URL
|
||||
export default async function renderAvatar({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Math.scss';
|
||||
|
||||
import katex from 'katex';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.katex-display {
|
||||
margin: 0 !important;
|
||||
}
|
||||
@@ -2,20 +2,18 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RawIcon.scss';
|
||||
|
||||
function RawIcon({
|
||||
color, size, src, isImage,
|
||||
}) {
|
||||
function RawIcon({ color, size, src, isImage }) {
|
||||
const style = {};
|
||||
if (color !== null) style.backgroundColor = color;
|
||||
if (isImage) {
|
||||
style.backgroundColor = 'transparent';
|
||||
style.backgroundImage = `url(${src})`;
|
||||
style.backgroundImage = `url("${src}")`;
|
||||
} else {
|
||||
style.WebkitMaskImage = `url(${src})`;
|
||||
style.maskImage = `url(${src})`;
|
||||
style.WebkitMaskImage = `url("${src}")`;
|
||||
style.maskImage = `url("${src}")`;
|
||||
}
|
||||
|
||||
return <span className={`ic-raw ic-raw-${size}`} style={style}> </span>;
|
||||
return <span className={`ic-raw ic-raw-${size}`} style={style} />;
|
||||
}
|
||||
|
||||
RawIcon.defaultProps = {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import initMatrix from '../../client/initMatrix';
|
||||
|
||||
export function useAccountData(eventType) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const [event, setEvent] = useState(mx.getAccountData(eventType));
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = (mEvent) => {
|
||||
if (mEvent.getType() !== eventType) return;
|
||||
setEvent(mEvent);
|
||||
};
|
||||
mx.on('accountData', handleChange);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleChange);
|
||||
};
|
||||
}, [eventType]);
|
||||
|
||||
return event;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend'
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||
.use(LanguageDetector)
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
debug: true,
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/public/locales/{{lng}}/translation.json',
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
|
||||
import NotificationSelector from './NotificationSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
|
||||
import { useAccountData } from '../../hooks/useAccountData';
|
||||
|
||||
export const notifType = {
|
||||
ON: 'on',
|
||||
OFF: 'off',
|
||||
NOISY: 'noisy',
|
||||
};
|
||||
export const typeToLabel = {
|
||||
[notifType.ON]: 'On',
|
||||
[notifType.OFF]: 'Off',
|
||||
[notifType.NOISY]: 'Noisy',
|
||||
};
|
||||
Object.freeze(notifType);
|
||||
|
||||
const DM = '.m.rule.room_one_to_one';
|
||||
const ENC_DM = '.m.rule.encrypted_room_one_to_one';
|
||||
const ROOM = '.m.rule.message';
|
||||
const ENC_ROOM = '.m.rule.encrypted';
|
||||
|
||||
export function getActionType(rule) {
|
||||
const { actions } = rule;
|
||||
if (actions.find((action) => action?.set_tweak === 'sound')) return notifType.NOISY;
|
||||
if (actions.find((action) => action?.set_tweak === 'highlight')) return notifType.ON;
|
||||
if (actions.find((action) => action === 'dont_notify')) return notifType.OFF;
|
||||
return notifType.OFF;
|
||||
}
|
||||
|
||||
export function getTypeActions(type, highlightValue = false) {
|
||||
if (type === notifType.OFF) return ['dont_notify'];
|
||||
|
||||
const highlight = { set_tweak: 'highlight' };
|
||||
if (typeof highlightValue === 'boolean') highlight.value = highlightValue;
|
||||
if (type === notifType.ON) return ['notify', highlight];
|
||||
|
||||
const sound = { set_tweak: 'sound', value: 'default' };
|
||||
return ['notify', sound, highlight];
|
||||
}
|
||||
|
||||
function useGlobalNotif() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||
const underride = pushRules?.global?.underride ?? [];
|
||||
const rulesToType = {
|
||||
[DM]: notifType.ON,
|
||||
[ENC_DM]: notifType.ON,
|
||||
[ROOM]: notifType.NOISY,
|
||||
[ENC_ROOM]: notifType.NOISY,
|
||||
};
|
||||
|
||||
const getRuleCondition = (rule) => {
|
||||
const condition = [];
|
||||
if (rule === DM || rule === ENC_DM) {
|
||||
condition.push({ kind: 'room_member_count', is: '2' });
|
||||
}
|
||||
condition.push({
|
||||
kind: 'event_match',
|
||||
key: 'type',
|
||||
pattern: [ENC_DM, ENC_ROOM].includes(rule) ? 'm.room.encrypted' : 'm.room.message',
|
||||
});
|
||||
return condition;
|
||||
};
|
||||
|
||||
const setRule = (rule, type) => {
|
||||
const content = pushRules ?? {};
|
||||
if (!content.global) content.global = {};
|
||||
if (!content.global.underride) content.global.underride = [];
|
||||
const ur = content.global.underride;
|
||||
let ruleContent = ur.find((action) => action?.rule_id === rule);
|
||||
if (!ruleContent) {
|
||||
ruleContent = {
|
||||
conditions: getRuleCondition(type),
|
||||
actions: [],
|
||||
rule_id: rule,
|
||||
default: true,
|
||||
enabled: true,
|
||||
};
|
||||
ur.push(ruleContent);
|
||||
}
|
||||
ruleContent.actions = getTypeActions(type);
|
||||
|
||||
mx.setAccountData('m.push_rules', content);
|
||||
};
|
||||
|
||||
const dmRule = underride.find((rule) => rule.rule_id === DM);
|
||||
const encDmRule = underride.find((rule) => rule.rule_id === ENC_DM);
|
||||
const roomRule = underride.find((rule) => rule.rule_id === ROOM);
|
||||
const encRoomRule = underride.find((rule) => rule.rule_id === ENC_ROOM);
|
||||
|
||||
if (dmRule) rulesToType[DM] = getActionType(dmRule);
|
||||
if (encDmRule) rulesToType[ENC_DM] = getActionType(encDmRule);
|
||||
if (roomRule) rulesToType[ROOM] = getActionType(roomRule);
|
||||
if (encRoomRule) rulesToType[ENC_ROOM] = getActionType(encRoomRule);
|
||||
|
||||
return [rulesToType, setRule];
|
||||
}
|
||||
|
||||
function GlobalNotification() {
|
||||
const [rulesToType, setRule] = useGlobalNotif();
|
||||
|
||||
const onSelect = (evt, rule) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(evt, '.btn-surface'),
|
||||
(requestClose) => (
|
||||
<NotificationSelector
|
||||
value={rulesToType[rule]}
|
||||
onSelect={(value) => {
|
||||
if (rulesToType[rule] !== value) setRule(rule, value);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="global-notification">
|
||||
<MenuHeader>Global Notifications</MenuHeader>
|
||||
<SettingTile
|
||||
title="Direct messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, DM)} iconSrc={ChevronBottomIC}>
|
||||
{ typeToLabel[rulesToType[DM]] }
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all direct message.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Encrypted direct messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ENC_DM)} iconSrc={ChevronBottomIC}>
|
||||
{typeToLabel[rulesToType[ENC_DM]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all encrypted direct message.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Rooms messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ROOM)} iconSrc={ChevronBottomIC}>
|
||||
{typeToLabel[rulesToType[ROOM]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all room message.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Encrypted rooms messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ENC_ROOM)} iconSrc={ChevronBottomIC}>
|
||||
{typeToLabel[rulesToType[ENC_ROOM]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all encrypted room message.</Text>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalNotification;
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import './IgnoreUserList.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import * as roomActions from '../../../client/action/room';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Chip from '../../atoms/chip/Chip';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
import { useAccountData } from '../../hooks/useAccountData';
|
||||
|
||||
function IgnoreUserList() {
|
||||
useAccountData('m.ignored_user_list');
|
||||
const ignoredUsers = initMatrix.matrixClient.getIgnoredUsers();
|
||||
|
||||
const handleSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { ignoreInput } = evt.target.elements;
|
||||
const value = ignoreInput.value.trim();
|
||||
const userIds = value.split(' ').filter((v) => v.match(/^@\S+:\S+$/));
|
||||
if (userIds.length === 0) return;
|
||||
ignoreInput.value = '';
|
||||
roomActions.ignore(userIds);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ignore-user-list">
|
||||
<MenuHeader>Ignored users</MenuHeader>
|
||||
<SettingTile
|
||||
title="Ignore user"
|
||||
content={(
|
||||
<div className="ignore-user-list__users">
|
||||
<Text variant="b3">Ignore userId if you do not want to receive their messages or invites.</Text>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="ignoreInput" required />
|
||||
<Button variant="primary" type="submit">Ignore</Button>
|
||||
</form>
|
||||
{ignoredUsers.length > 0 && (
|
||||
<div>
|
||||
{ignoredUsers.map((uId) => (
|
||||
<Chip
|
||||
iconSrc={CrossIC}
|
||||
key={uId}
|
||||
text={uId}
|
||||
iconColor={CrossIC}
|
||||
onClick={() => roomActions.unignore([uId])}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IgnoreUserList;
|
||||
@@ -0,0 +1,17 @@
|
||||
.ignore-user-list {
|
||||
&__users {
|
||||
& form,
|
||||
& > div:last-child {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
|
||||
& form {
|
||||
margin: var(--sp-extra-tight) 0 var(--sp-normal);
|
||||
.input-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import React from 'react';
|
||||
import './KeywordNotification.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Chip from '../../atoms/chip/Chip';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
|
||||
import NotificationSelector from './NotificationSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
import { useAccountData } from '../../hooks/useAccountData';
|
||||
import {
|
||||
notifType, typeToLabel, getActionType, getTypeActions,
|
||||
} from './GlobalNotification';
|
||||
|
||||
const DISPLAY_NAME = '.m.rule.contains_display_name';
|
||||
const ROOM_PING = '.m.rule.roomnotif';
|
||||
const USERNAME = '.m.rule.contains_user_name';
|
||||
const KEYWORD = 'keyword';
|
||||
|
||||
function useKeywordNotif() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||
const override = pushRules?.global?.override ?? [];
|
||||
const content = pushRules?.global?.content ?? [];
|
||||
|
||||
const rulesToType = {
|
||||
[DISPLAY_NAME]: notifType.NOISY,
|
||||
[ROOM_PING]: notifType.NOISY,
|
||||
[USERNAME]: notifType.NOISY,
|
||||
};
|
||||
|
||||
const setRule = (rule, type) => {
|
||||
const evtContent = pushRules ?? {};
|
||||
if (!evtContent.global) evtContent.global = {};
|
||||
if (!evtContent.global.override) evtContent.global.override = [];
|
||||
if (!evtContent.global.content) evtContent.global.content = [];
|
||||
const or = evtContent.global.override;
|
||||
const ct = evtContent.global.content;
|
||||
|
||||
if (rule === DISPLAY_NAME || rule === ROOM_PING) {
|
||||
let orRule = or.find((r) => r?.rule_id === rule);
|
||||
if (!orRule) {
|
||||
orRule = {
|
||||
conditions: [],
|
||||
actions: [],
|
||||
rule_id: rule,
|
||||
default: true,
|
||||
enabled: true,
|
||||
};
|
||||
or.push(orRule);
|
||||
}
|
||||
if (rule === DISPLAY_NAME) {
|
||||
orRule.conditions = [{ kind: 'contains_display_name' }];
|
||||
orRule.actions = getTypeActions(type, true);
|
||||
} else {
|
||||
orRule.conditions = [
|
||||
{ kind: 'event_match', key: 'content.body', pattern: '@room' },
|
||||
{ kind: 'sender_notification_permission', key: 'room' },
|
||||
];
|
||||
orRule.actions = getTypeActions(type, true);
|
||||
}
|
||||
} else if (rule === USERNAME) {
|
||||
let usernameRule = ct.find((r) => r?.rule_id === rule);
|
||||
if (!usernameRule) {
|
||||
const userId = mx.getUserId();
|
||||
const username = userId.match(/^@?(\S+):(\S+)$/)?.[1] ?? userId;
|
||||
usernameRule = {
|
||||
actions: [],
|
||||
default: true,
|
||||
enabled: true,
|
||||
pattern: username,
|
||||
rule_id: rule,
|
||||
};
|
||||
ct.push(usernameRule);
|
||||
}
|
||||
usernameRule.actions = getTypeActions(type, true);
|
||||
} else {
|
||||
const keyRules = ct.filter((r) => r.rule_id !== USERNAME);
|
||||
keyRules.forEach((r) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
r.actions = getTypeActions(type, true);
|
||||
});
|
||||
}
|
||||
|
||||
mx.setAccountData('m.push_rules', evtContent);
|
||||
};
|
||||
|
||||
const addKeyword = (keyword) => {
|
||||
if (content.find((r) => r.rule_id === keyword)) return;
|
||||
content.push({
|
||||
rule_id: keyword,
|
||||
pattern: keyword,
|
||||
enabled: true,
|
||||
default: false,
|
||||
actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
|
||||
});
|
||||
mx.setAccountData('m.push_rules', pushRules);
|
||||
};
|
||||
const removeKeyword = (rule) => {
|
||||
pushRules.global.content = content.filter((r) => r.rule_id !== rule.rule_id);
|
||||
mx.setAccountData('m.push_rules', pushRules);
|
||||
};
|
||||
|
||||
const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
|
||||
const roomRule = override.find((rule) => rule.rule_id === ROOM_PING);
|
||||
const usernameRule = content.find((rule) => rule.rule_id === USERNAME);
|
||||
const keywordRule = content.find((rule) => rule.rule_id !== USERNAME);
|
||||
|
||||
if (dsRule) rulesToType[DISPLAY_NAME] = getActionType(dsRule);
|
||||
if (roomRule) rulesToType[ROOM_PING] = getActionType(roomRule);
|
||||
if (usernameRule) rulesToType[USERNAME] = getActionType(usernameRule);
|
||||
if (keywordRule) rulesToType[KEYWORD] = getActionType(keywordRule);
|
||||
|
||||
return {
|
||||
rulesToType,
|
||||
pushRules,
|
||||
setRule,
|
||||
addKeyword,
|
||||
removeKeyword,
|
||||
};
|
||||
}
|
||||
|
||||
function GlobalNotification() {
|
||||
const {
|
||||
rulesToType,
|
||||
pushRules,
|
||||
setRule,
|
||||
addKeyword,
|
||||
removeKeyword,
|
||||
} = useKeywordNotif();
|
||||
|
||||
const keywordRules = pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
|
||||
|
||||
const onSelect = (evt, rule) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(evt, '.btn-surface'),
|
||||
(requestClose) => (
|
||||
<NotificationSelector
|
||||
value={rulesToType[rule]}
|
||||
onSelect={(value) => {
|
||||
if (rulesToType[rule] !== value) setRule(rule, value);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { keywordInput } = evt.target.elements;
|
||||
const value = keywordInput.value.trim();
|
||||
if (value === '') return;
|
||||
addKeyword(value);
|
||||
keywordInput.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="keyword-notification">
|
||||
<MenuHeader>Mentions & keywords</MenuHeader>
|
||||
<SettingTile
|
||||
title="Message containing my display name"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, DISPLAY_NAME)} iconSrc={ChevronBottomIC}>
|
||||
{ typeToLabel[rulesToType[DISPLAY_NAME]] }
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all message containing your display name.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Message containing my username"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, USERNAME)} iconSrc={ChevronBottomIC}>
|
||||
{ typeToLabel[rulesToType[USERNAME]] }
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all message containing your username.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Message containing @room"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ROOM_PING)} iconSrc={ChevronBottomIC}>
|
||||
{typeToLabel[rulesToType[ROOM_PING]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all messages containing @room.</Text>}
|
||||
/>
|
||||
{ rulesToType[KEYWORD] && (
|
||||
<SettingTile
|
||||
title="Message containing keywords"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, KEYWORD)} iconSrc={ChevronBottomIC}>
|
||||
{typeToLabel[rulesToType[KEYWORD]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all message containing keywords.</Text>}
|
||||
/>
|
||||
)}
|
||||
<SettingTile
|
||||
title="Keywords"
|
||||
content={(
|
||||
<div className="keyword-notification__keyword">
|
||||
<Text variant="b3">Get notification when a message contains keyword.</Text>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="keywordInput" required />
|
||||
<Button variant="primary" type="submit">Add</Button>
|
||||
</form>
|
||||
{keywordRules.length > 0 && (
|
||||
<div>
|
||||
{keywordRules.map((rule) => (
|
||||
<Chip
|
||||
iconSrc={CrossIC}
|
||||
key={rule.rule_id}
|
||||
text={rule.pattern}
|
||||
iconColor={CrossIC}
|
||||
onClick={() => removeKeyword(rule)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalNotification;
|
||||
@@ -0,0 +1,17 @@
|
||||
.keyword-notification {
|
||||
&__keyword {
|
||||
& form,
|
||||
& > div:last-child {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
|
||||
& form {
|
||||
margin: var(--sp-extra-tight) 0 var(--sp-normal);
|
||||
.input-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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 NotificationSelector({
|
||||
value, onSelect,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<MenuHeader>Notification</MenuHeader>
|
||||
<MenuItem iconSrc={value === 'off' ? CheckIC : null} variant={value === 'off' ? 'positive' : 'surface'} onClick={() => onSelect('off')}>Off</MenuItem>
|
||||
<MenuItem iconSrc={value === 'on' ? CheckIC : null} variant={value === 'on' ? 'positive' : 'surface'} onClick={() => onSelect('on')}>On</MenuItem>
|
||||
<MenuItem iconSrc={value === 'noisy' ? CheckIC : null} variant={value === 'noisy' ? 'positive' : 'surface'} onClick={() => onSelect('noisy')}>Noisy</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NotificationSelector.propTypes = {
|
||||
value: PropTypes.oneOf(['off', 'on', 'noisy']).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default NotificationSelector;
|
||||
@@ -27,9 +27,7 @@ function ImagePackUpload({ onUpload }) {
|
||||
|
||||
setProgress(true);
|
||||
const image = await scaleDownImage(imgFile, 512, 512);
|
||||
const url = await mx.uploadContent(image, {
|
||||
onlyContentUri: true,
|
||||
});
|
||||
const { content_uri: url } = await mx.uploadContent(image);
|
||||
|
||||
onUpload(shortcode, url);
|
||||
setProgress(false);
|
||||
|
||||
@@ -22,7 +22,7 @@ function ImageUpload({
|
||||
const file = e.target.files.item(0);
|
||||
if (file === null) return;
|
||||
try {
|
||||
const uPromise = initMatrix.matrixClient.uploadContent(file, { onlyContentUri: false });
|
||||
const uPromise = initMatrix.matrixClient.uploadContent(file);
|
||||
setUploadPromise(uPromise);
|
||||
|
||||
const res = await uPromise;
|
||||
@@ -74,7 +74,7 @@ function ImageUpload({
|
||||
<Text variant="b3">{uploadPromise ? 'Cancel' : 'Remove'}</Text>
|
||||
</button>
|
||||
)}
|
||||
<input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" />
|
||||
<input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" accept="image/*" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,10 +41,7 @@ function getNativeHeight(width, height, maxWidth = 296) {
|
||||
return scale * height;
|
||||
}
|
||||
|
||||
function FileHeader({
|
||||
name, link, external,
|
||||
file, type,
|
||||
}) {
|
||||
function FileHeader({ name, link, external, file, type }) {
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
async function getFile() {
|
||||
@@ -61,19 +58,19 @@ function FileHeader({
|
||||
}
|
||||
return (
|
||||
<div className="file-header">
|
||||
<Text className="file-name" variant="b3">{name}</Text>
|
||||
{ link !== null && (
|
||||
<Text className="file-name" variant="b3">
|
||||
{name}
|
||||
</Text>
|
||||
{link !== null && (
|
||||
<>
|
||||
{
|
||||
external && (
|
||||
<IconButton
|
||||
size="extra-small"
|
||||
tooltip="Open in new tab"
|
||||
src={ExternalSVG}
|
||||
onClick={() => window.open(url || link)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{external && (
|
||||
<IconButton
|
||||
size="extra-small"
|
||||
tooltip="Open in new tab"
|
||||
src={ExternalSVG}
|
||||
onClick={() => window.open(url || link)}
|
||||
/>
|
||||
)}
|
||||
<a href={url || link} download={name} target="_blank" rel="noreferrer">
|
||||
<IconButton
|
||||
size="extra-small"
|
||||
@@ -100,9 +97,7 @@ FileHeader.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function File({
|
||||
name, link, file, type,
|
||||
}) {
|
||||
function File({ name, link, file, type }) {
|
||||
return (
|
||||
<div className="file-container">
|
||||
<FileHeader name={name} link={link} file={file} type={type} />
|
||||
@@ -120,9 +115,7 @@ File.propTypes = {
|
||||
file: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
function Image({
|
||||
name, width, height, link, file, type, blurhash,
|
||||
}) {
|
||||
function Image({ name, width, height, link, file, type, blurhash }) {
|
||||
const [url, setUrl] = useState(null);
|
||||
const [blur, setBlur] = useState(true);
|
||||
const [lightbox, setLightbox] = useState(false);
|
||||
@@ -156,8 +149,8 @@ function Image({
|
||||
onClick={toggleLightbox}
|
||||
onKeyDown={toggleLightbox}
|
||||
>
|
||||
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{ url !== null && (
|
||||
{blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{url !== null && (
|
||||
<img
|
||||
style={{ display: blur ? 'none' : 'unset' }}
|
||||
onLoad={() => setBlur(false)}
|
||||
@@ -168,12 +161,7 @@ function Image({
|
||||
</div>
|
||||
</div>
|
||||
{url && (
|
||||
<ImageLightbox
|
||||
url={url}
|
||||
alt={name}
|
||||
isOpen={lightbox}
|
||||
onRequestClose={toggleLightbox}
|
||||
/>
|
||||
<ImageLightbox url={url} alt={name} isOpen={lightbox} onRequestClose={toggleLightbox} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -195,9 +183,7 @@ Image.propTypes = {
|
||||
blurhash: PropTypes.string,
|
||||
};
|
||||
|
||||
function Sticker({
|
||||
name, height, width, link, file, type,
|
||||
}) {
|
||||
function Sticker({ name, height, width, link, file, type }) {
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -214,8 +200,11 @@ function Sticker({
|
||||
}, []);
|
||||
|
||||
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
|
||||
className="sticker-container"
|
||||
style={{ height: width !== null ? getNativeHeight(width, height, 256) : 'unset' }}
|
||||
>
|
||||
{url !== null && <img src={url || link} title={name} alt={name} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -234,9 +223,7 @@ Sticker.propTypes = {
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
function Audio({
|
||||
name, link, type, file,
|
||||
}) {
|
||||
function Audio({ name, link, type, file }) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
@@ -254,9 +241,11 @@ function Audio({
|
||||
<div className="file-container">
|
||||
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
|
||||
<div className="audio-container">
|
||||
{ url === null && isLoading && <Spinner size="small" /> }
|
||||
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
|
||||
{ url !== null && (
|
||||
{url === null && isLoading && <Spinner size="small" />}
|
||||
{url === null && !isLoading && (
|
||||
<IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />
|
||||
)}
|
||||
{url !== null && (
|
||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||
<audio autoPlay controls>
|
||||
<source src={url} type={getBlobSafeMimeType(type)} />
|
||||
@@ -278,8 +267,16 @@ Audio.propTypes = {
|
||||
};
|
||||
|
||||
function Video({
|
||||
name, link, thumbnail, thumbnailFile, thumbnailType,
|
||||
width, height, file, type, blurhash,
|
||||
name,
|
||||
link,
|
||||
thumbnail,
|
||||
thumbnailFile,
|
||||
thumbnailType,
|
||||
width,
|
||||
height,
|
||||
file,
|
||||
type,
|
||||
blurhash,
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [url, setUrl] = useState(null);
|
||||
@@ -319,14 +316,21 @@ function Video({
|
||||
}}
|
||||
className="video-container"
|
||||
>
|
||||
{ url === null ? (
|
||||
{url === null ? (
|
||||
<>
|
||||
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{ thumbUrl !== null && (
|
||||
<img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
|
||||
{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} />}
|
||||
{!isLoading && (
|
||||
<IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||
@@ -361,6 +365,4 @@ Video.propTypes = {
|
||||
blurhash: PropTypes.string,
|
||||
};
|
||||
|
||||
export {
|
||||
File, Image, Sticker, Audio, Video,
|
||||
};
|
||||
export { File, Image, Sticker, Audio, Video };
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
.sticker-container {
|
||||
display: inline-flex;
|
||||
max-width: 128px;
|
||||
max-width: 256px;
|
||||
width: 100%;
|
||||
& img {
|
||||
width: 100% !important;
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, {
|
||||
useState, useEffect, useCallback, useRef,
|
||||
} from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './Message.scss';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getUsername, getUsernameOfRoomMember, parseReply } from '../../../util/matrixUtil';
|
||||
import {
|
||||
getUsername,
|
||||
getUsernameOfRoomMember,
|
||||
parseReply,
|
||||
trimHTMLReply,
|
||||
} from '../../../util/matrixUtil';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
||||
import {
|
||||
openEmojiBoard, openProfileViewer, openReadReceipts, openViewSource, replyTo,
|
||||
openEmojiBoard,
|
||||
openProfileViewer,
|
||||
openReadReceipts,
|
||||
openTextTranslate,
|
||||
openViewSource,
|
||||
replyTo,
|
||||
} from '../../../client/action/navigation';
|
||||
import { sanitizeCustomHtml } from '../../../util/sanitize';
|
||||
|
||||
@@ -25,7 +34,11 @@ 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 ContextMenu, {
|
||||
MenuHeader,
|
||||
MenuItem,
|
||||
MenuBorder,
|
||||
} from '../../atoms/context-menu/ContextMenu';
|
||||
import * as Media from '../media/Media';
|
||||
|
||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
||||
@@ -33,11 +46,15 @@ import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
|
||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
|
||||
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
import TransIC from '../../../../public/res/ic/outlined/translate.svg';
|
||||
import repeatIC from '../../../../public/res/ic/outlined/repeat.svg';
|
||||
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
||||
import { html, plain } from '../../../util/markdown';
|
||||
|
||||
function PlaceholderMessage() {
|
||||
return (
|
||||
@@ -58,9 +75,7 @@ function PlaceholderMessage() {
|
||||
);
|
||||
}
|
||||
|
||||
const MessageAvatar = React.memo(({
|
||||
roomId, avatarSrc, userId, username,
|
||||
}) => (
|
||||
const MessageAvatar = React.memo(({ roomId, avatarSrc, userId, username }) => (
|
||||
<div className="message__avatar-container">
|
||||
<button type="button" onClick={() => openProfileViewer(userId, roomId)}>
|
||||
<Avatar imageSrc={avatarSrc} text={username} bgColor={colorMXID(userId)} size="small" />
|
||||
@@ -68,9 +83,7 @@ const MessageAvatar = React.memo(({
|
||||
</div>
|
||||
));
|
||||
|
||||
const MessageHeader = React.memo(({
|
||||
userId, username, timestamp, fullTime,
|
||||
}) => (
|
||||
const MessageHeader = React.memo(({ userId, username, timestamp, fullTime }) => (
|
||||
<div className="message__header">
|
||||
<Text
|
||||
style={{ color: colorMXID(userId) }}
|
||||
@@ -104,9 +117,7 @@ function MessageReply({ name, color, body }) {
|
||||
<div className="message__reply">
|
||||
<Text variant="b2">
|
||||
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
||||
<span style={{ color }}>{twemojify(name)}</span>
|
||||
{' '}
|
||||
{twemojify(body)}
|
||||
<span style={{ color }}>{twemojify(name)}</span> {twemojify(body)}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
@@ -140,7 +151,9 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||
const username = getUsernameOfRoomMember(mEvent.sender);
|
||||
|
||||
if (isMountedRef.current === false) return;
|
||||
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
|
||||
const fallbackBody = mEvent.isRedacted()
|
||||
? '*** This message has been deleted ***'
|
||||
: '*** Unable to load reply ***';
|
||||
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
|
||||
if (editedList && parsedBody.startsWith(' * ')) {
|
||||
parsedBody = parsedBody.slice(3);
|
||||
@@ -194,13 +207,7 @@ MessageReplyWrapper.propTypes = {
|
||||
eventId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const MessageBody = React.memo(({
|
||||
senderName,
|
||||
body,
|
||||
isCustomHTML,
|
||||
isEdited,
|
||||
msgType,
|
||||
}) => {
|
||||
const MessageBody = React.memo(({ senderName, body, isCustomHTML, isEdited, msgType }) => {
|
||||
// if body is not string it is a React element.
|
||||
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
|
||||
|
||||
@@ -212,7 +219,7 @@ const MessageBody = React.memo(({
|
||||
undefined,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
} catch {
|
||||
console.error('Malformed custom html: ', body);
|
||||
@@ -237,10 +244,14 @@ const MessageBody = React.memo(({
|
||||
const nEmojis = content.filter((e) => e.type === 'img').length;
|
||||
|
||||
// Make sure there's no text besides whitespace and variation selector U+FE0F
|
||||
if (nEmojis <= 10 && content.every((element) => (
|
||||
(typeof element === 'object' && element.type === 'img')
|
||||
|| (typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
||||
))) {
|
||||
if (
|
||||
nEmojis <= 10 &&
|
||||
content.every(
|
||||
(element) =>
|
||||
(typeof element === 'object' && element.type === 'img') ||
|
||||
(typeof element === 'string' && /^[\s\ufe0f]*$/g.test(element))
|
||||
)
|
||||
) {
|
||||
emojiOnly = true;
|
||||
}
|
||||
}
|
||||
@@ -248,22 +259,25 @@ const MessageBody = React.memo(({
|
||||
if (!isCustomHTML) {
|
||||
// If this is a plaintext message, wrap it in a <p> element (automatically applying
|
||||
// white-space: pre-wrap) in order to preserve newlines
|
||||
content = (<p>{content}</p>);
|
||||
content = <p className="message__body-plain">{content}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="message__body">
|
||||
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
||||
{ msgType === 'm.emote' && (
|
||||
{msgType === 'm.emote' && (
|
||||
<>
|
||||
{'* '}
|
||||
{twemojify(senderName)}
|
||||
{' '}
|
||||
{twemojify(senderName)}{' '}
|
||||
</>
|
||||
)}
|
||||
{ content }
|
||||
{content}
|
||||
</div>
|
||||
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
|
||||
{isEdited && (
|
||||
<Text className="message__body-edited" variant="b3">
|
||||
(edited)
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -297,12 +311,18 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
onSave(editInputRef.current.value);
|
||||
onSave(editInputRef.current.value, body);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value); }}>
|
||||
<form
|
||||
className="message__edit"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSave(editInputRef.current.value, body);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
forwardRef={editInputRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -313,7 +333,9 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||
autoFocus
|
||||
/>
|
||||
<div className="message__edit-btns">
|
||||
<Button type="submit" variant="primary">Save</Button>
|
||||
<Button type="submit" variant="primary">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -365,9 +387,7 @@ function genReactionMsg(userIds, reaction, shortcode) {
|
||||
<React.Fragment key={userId}>
|
||||
{twemojify(getUsername(userId))}
|
||||
{index < userIds.length - 1 && (
|
||||
<span style={{ opacity: '.6' }}>
|
||||
{index === userIds.length - 2 ? ' and ' : ', '}
|
||||
</span>
|
||||
<span style={{ opacity: '.6' }}>{index === userIds.length - 2 ? ' and ' : ', '}</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
@@ -377,9 +397,7 @@ function genReactionMsg(userIds, reaction, shortcode) {
|
||||
);
|
||||
}
|
||||
|
||||
function MessageReaction({
|
||||
reaction, shortcode, count, users, isActive, onClick,
|
||||
}) {
|
||||
function MessageReaction({ reaction, shortcode, count, users, isActive, onClick }) {
|
||||
let customEmojiUrl = null;
|
||||
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
||||
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
|
||||
@@ -387,19 +405,32 @@ function MessageReaction({
|
||||
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, shortcode)
|
||||
: 'Unable to load who has reacted'}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
|
||||
>
|
||||
{
|
||||
customEmojiUrl
|
||||
? <img className="react-emoji" draggable="false" alt={shortcode ?? reaction} src={customEmojiUrl} />
|
||||
: twemojify(reaction, { className: 'react-emoji' })
|
||||
}
|
||||
<Text variant="b3" className="msg__reaction-count">{count}</Text>
|
||||
{customEmojiUrl ? (
|
||||
<img
|
||||
className="react-emoji"
|
||||
draggable="false"
|
||||
alt={shortcode ?? reaction}
|
||||
src={customEmojiUrl}
|
||||
/>
|
||||
) : (
|
||||
twemojify(reaction, { className: 'react-emoji' })
|
||||
)}
|
||||
<Text variant="b3" className="msg__reaction-count">
|
||||
{count}
|
||||
</Text>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -465,21 +496,19 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
|
||||
return (
|
||||
<div className="message__reactions text text-b3 noselect">
|
||||
{
|
||||
Object.keys(reactions).map((key) => (
|
||||
<MessageReaction
|
||||
key={key}
|
||||
reaction={key}
|
||||
shortcode={reactions[key].shortcode}
|
||||
count={reactions[key].count}
|
||||
users={reactions[key].users}
|
||||
isActive={reactions[key].isActive}
|
||||
onClick={() => {
|
||||
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{Object.keys(reactions).map((key) => (
|
||||
<MessageReaction
|
||||
key={key}
|
||||
reaction={key}
|
||||
shortcode={reactions[key].shortcode}
|
||||
count={reactions[key].count}
|
||||
users={reactions[key].users}
|
||||
isActive={reactions[key].isActive}
|
||||
onClick={() => {
|
||||
toggleEmoji(roomId, mEvent.getId(), key, reactions[key].shortcode, roomTimeline);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{canSendReaction && (
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
@@ -500,11 +529,11 @@ MessageReactionGroup.propTypes = {
|
||||
|
||||
function isMedia(mE) {
|
||||
return (
|
||||
mE.getContent()?.msgtype === 'm.file'
|
||||
|| mE.getContent()?.msgtype === 'm.image'
|
||||
|| mE.getContent()?.msgtype === 'm.audio'
|
||||
|| mE.getContent()?.msgtype === 'm.video'
|
||||
|| mE.getType() === 'm.sticker'
|
||||
mE.getContent()?.msgtype === 'm.file' ||
|
||||
mE.getContent()?.msgtype === 'm.image' ||
|
||||
mE.getContent()?.msgtype === 'm.audio' ||
|
||||
mE.getContent()?.msgtype === 'm.video' ||
|
||||
mE.getType() === 'm.sticker'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -520,9 +549,19 @@ function handleOpenViewSource(mEvent, roomTimeline) {
|
||||
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
||||
}
|
||||
|
||||
const MessageOptions = React.memo(({
|
||||
roomTimeline, mEvent, edit, reply,
|
||||
}) => {
|
||||
function handleTextTranslate(mEvent) {
|
||||
openTextTranslate(mEvent);
|
||||
}
|
||||
|
||||
async function handleRepeat(mEvent){
|
||||
console.log(mEvent);
|
||||
const opt = {};
|
||||
opt.msgType = 'm.text';
|
||||
initMatrix.roomsInput.setMessage(mEvent.sender.roomId, mEvent.clearEvent.content.body);
|
||||
await initMatrix.roomsInput.sendInput(mEvent.sender.roomId, opt);
|
||||
}
|
||||
|
||||
const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
|
||||
const { roomId, room } = roomTimeline;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const senderId = mEvent.getSender();
|
||||
@@ -541,19 +580,9 @@ const MessageOptions = React.memo(({
|
||||
tooltip="Add reaction"
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => reply()}
|
||||
src={ReplyArrowIC}
|
||||
size="extra-small"
|
||||
tooltip="Reply"
|
||||
/>
|
||||
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
|
||||
<IconButton
|
||||
onClick={() => edit(true)}
|
||||
src={PencilIC}
|
||||
size="extra-small"
|
||||
tooltip="Edit"
|
||||
/>
|
||||
<IconButton onClick={() => reply()} src={ReplyArrowIC} size="extra-small" tooltip="Reply" />
|
||||
{senderId === mx.getUserId() && !isMedia(mEvent) && (
|
||||
<IconButton onClick={() => edit(true)} src={PencilIC} size="extra-small" tooltip="Edit" />
|
||||
)}
|
||||
<ContextMenu
|
||||
content={() => (
|
||||
@@ -565,12 +594,15 @@ const MessageOptions = React.memo(({
|
||||
>
|
||||
Read receipts
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={CmdIC}
|
||||
onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
|
||||
>
|
||||
<MenuItem iconSrc={CmdIC} onClick={() => handleOpenViewSource(mEvent, roomTimeline)}>
|
||||
View source
|
||||
</MenuItem>
|
||||
<MenuItem iconSrc={TransIC} onClick={() => handleTextTranslate(mEvent)}>
|
||||
Translate
|
||||
</MenuItem>
|
||||
<MenuItem iconSrc={repeatIC} onClick={() => handleRepeat(mEvent)}>
|
||||
Repeat
|
||||
</MenuItem>
|
||||
{(canIRedact || senderId === mx.getUserId()) && (
|
||||
<>
|
||||
<MenuBorder />
|
||||
@@ -582,7 +614,7 @@ const MessageOptions = React.memo(({
|
||||
'Delete message',
|
||||
'Are you sure that you want to delete this message?',
|
||||
'Delete',
|
||||
'danger',
|
||||
'danger'
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
redactEvent(roomId, mEvent.getId());
|
||||
@@ -616,7 +648,8 @@ MessageOptions.propTypes = {
|
||||
function genMediaContent(mE) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const mContent = mE.getContent();
|
||||
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
if (!mContent || !mContent.body)
|
||||
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
|
||||
let mediaMXC = mContent?.url;
|
||||
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
||||
@@ -624,7 +657,8 @@ function genMediaContent(mE) {
|
||||
|
||||
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
||||
|
||||
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
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);
|
||||
@@ -714,13 +748,19 @@ function getEditedBody(editedMEvent) {
|
||||
}
|
||||
|
||||
function Message({
|
||||
mEvent, isBodyOnly, roomTimeline,
|
||||
focus, fullTime, isEdit, setEdit, cancelEdit,
|
||||
mEvent,
|
||||
isBodyOnly,
|
||||
roomTimeline,
|
||||
focus,
|
||||
fullTime,
|
||||
isEdit,
|
||||
setEdit,
|
||||
cancelEdit,
|
||||
}) {
|
||||
const roomId = mEvent.getRoomId();
|
||||
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
|
||||
|
||||
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
|
||||
const className = ['message', isBodyOnly ? 'message--body-only' : 'message--full'];
|
||||
if (focus) className.push('message--focus');
|
||||
const content = mEvent.getContent();
|
||||
const eventId = mEvent.getId();
|
||||
@@ -728,51 +768,51 @@ function Message({
|
||||
const senderId = mEvent.getSender();
|
||||
let { body } = content;
|
||||
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
|
||||
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
|
||||
const avatarSrc =
|
||||
mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
|
||||
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
||||
let customHTML = isCustomHTML ? content.formatted_body : null;
|
||||
|
||||
const edit = useCallback(() => {
|
||||
setEdit(eventId);
|
||||
}, []);
|
||||
const reply = useCallback(() => {
|
||||
replyTo(senderId, mEvent.getId(), body);
|
||||
}, [body]);
|
||||
replyTo(senderId, mEvent.getId(), body, customHTML);
|
||||
}, [body, customHTML]);
|
||||
|
||||
if (body === undefined) return null;
|
||||
if (msgType === 'm.emote') className.push('message--type-emote');
|
||||
|
||||
let isCustomHTML = content.format === 'org.matrix.custom.html';
|
||||
const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
|
||||
const haveReactions = roomTimeline
|
||||
? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
|
||||
: false;
|
||||
const isReply = !!mEvent.replyEventId;
|
||||
let customHTML = isCustomHTML ? content.formatted_body : null;
|
||||
|
||||
if (isEdited) {
|
||||
const editedList = editedTimeline.get(eventId);
|
||||
const editedMEvent = editedList[editedList.length - 1];
|
||||
[body, isCustomHTML, customHTML] = getEditedBody(editedMEvent);
|
||||
if (typeof body !== 'string') return null;
|
||||
}
|
||||
|
||||
if (isReply) {
|
||||
body = parseReply(body)?.body ?? body;
|
||||
customHTML = trimHTMLReply(customHTML);
|
||||
}
|
||||
|
||||
if (typeof body !== 'string') body = '';
|
||||
|
||||
return (
|
||||
<div className={className.join(' ')}>
|
||||
{
|
||||
isBodyOnly
|
||||
? <div className="message__avatar-container" />
|
||||
: (
|
||||
<MessageAvatar
|
||||
roomId={roomId}
|
||||
avatarSrc={avatarSrc}
|
||||
userId={senderId}
|
||||
username={username}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{isBodyOnly ? (
|
||||
<div className="message__avatar-container" />
|
||||
) : (
|
||||
<MessageAvatar
|
||||
roomId={roomId}
|
||||
avatarSrc={avatarSrc}
|
||||
userId={senderId}
|
||||
username={username}
|
||||
/>
|
||||
)}
|
||||
<div className="message__main-container">
|
||||
{!isBodyOnly && (
|
||||
<MessageHeader
|
||||
@@ -783,10 +823,7 @@ function Message({
|
||||
/>
|
||||
)}
|
||||
{roomTimeline && isReply && (
|
||||
<MessageReplyWrapper
|
||||
roomTimeline={roomTimeline}
|
||||
eventId={mEvent.replyEventId}
|
||||
/>
|
||||
<MessageReplyWrapper roomTimeline={roomTimeline} eventId={mEvent.replyEventId} />
|
||||
)}
|
||||
{!isEdit && (
|
||||
<MessageBody
|
||||
@@ -799,9 +836,13 @@ function Message({
|
||||
)}
|
||||
{isEdit && (
|
||||
<MessageEdit
|
||||
body={body}
|
||||
onSave={(newBody) => {
|
||||
if (newBody !== body) {
|
||||
body={
|
||||
customHTML
|
||||
? html(customHTML, { kind: 'edit', onlyPlain: true }).plain
|
||||
: plain(body, { kind: 'edit', onlyPlain: true }).plain
|
||||
}
|
||||
onSave={(newBody, oldBody) => {
|
||||
if (newBody !== oldBody) {
|
||||
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
||||
}
|
||||
cancelEdit();
|
||||
@@ -809,16 +850,9 @@ function Message({
|
||||
onCancel={cancelEdit}
|
||||
/>
|
||||
)}
|
||||
{haveReactions && (
|
||||
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
|
||||
)}
|
||||
{haveReactions && <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />}
|
||||
{roomTimeline && !isEdit && (
|
||||
<MessageOptions
|
||||
roomTimeline={roomTimeline}
|
||||
mEvent={mEvent}
|
||||
edit={edit}
|
||||
reply={reply}
|
||||
/>
|
||||
<MessageOptions roomTimeline={roomTimeline} mEvent={mEvent} edit={edit} reply={reply} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
.message__body {
|
||||
word-break: break-word;
|
||||
|
||||
& > .text > * {
|
||||
& > .text > .message__body-plain {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -174,8 +174,8 @@
|
||||
white-space: initial !important;
|
||||
}
|
||||
|
||||
& p:not(:last-child) {
|
||||
margin-bottom: var(--sp-normal);
|
||||
& > .text > p + p {
|
||||
margin-top: var(--sp-normal);
|
||||
}
|
||||
|
||||
& span[data-mx-pill] {
|
||||
@@ -400,6 +400,7 @@
|
||||
& pre {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
direction: initial;
|
||||
@include scrollbar.scroll;
|
||||
@include scrollbar.scroll__h;
|
||||
@include scrollbar.scroll--auto-hide;
|
||||
|
||||
@@ -80,8 +80,7 @@ function RoomEmojis({ roomId }) {
|
||||
|
||||
const { usablePacks, createPack, deletePack } = useRoomPacks(room);
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
const canChange = room.currentState.maySendStateEvent('im.ponies.room_emote', mx.getUserId());
|
||||
|
||||
const handlePackCreate = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Text from '../../atoms/text/Text';
|
||||
|
||||
import './RoomTimezone.scss';
|
||||
|
||||
function RoomTimezone({ roomId }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const userId = mx.getUserId();
|
||||
|
||||
const timezoneEventId = room.currentState.getStateEvents('in.cinny.shared_timezone', userId)?.event?.content?.user_timezone_event;
|
||||
const timezoneEvent = room.findEventById(timezoneEventId);
|
||||
const timezoneContent = timezoneEvent?.getContent();
|
||||
|
||||
const [timezone, setTimezone] = useState(timezoneContent?.user_timezone);
|
||||
|
||||
const clearTimezone = () => {
|
||||
const eventKey = room.currentState.getStateEvents('in.cinny.shared_timezone', userId)?.event?.content?.user_timezone_event;
|
||||
mx.redactEvent(roomId, eventKey);
|
||||
mx.sendStateEvent(roomId, 'in.cinny.shared_timezone', { }, userId);
|
||||
setTimezone(null);
|
||||
};
|
||||
|
||||
const shareTimezone = () => {
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
setTimezone(userTimezone);
|
||||
const content = {
|
||||
user_timezone: userTimezone,
|
||||
msgtype: 'in.cinny.share_timezone',
|
||||
};
|
||||
mx.sendEvent(roomId, 'in.cinny.share_timezone', content).then((event) => {
|
||||
// Append the shared timezone event to the room state
|
||||
mx.sendStateEvent(roomId, 'in.cinny.shared_timezone', { user_timezone_event: event.event_id }, userId);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="room-timezone__content">
|
||||
<Text className="room-timezone__message">Share your timezone</Text>
|
||||
<Text className="room-timezone__message" variant="b3">
|
||||
Sharing your timezone will allow other users in this room to see your local time.
|
||||
Keep in mind that sharing your timezone will share your approximate location.
|
||||
</Text>
|
||||
<Text className="room-timezone__message" variant="b2">
|
||||
{timezone ? `Currently shared timezone: ${timezone}` : 'You are not currently sharing a timezone'}
|
||||
</Text>
|
||||
<Button onClick={() => shareTimezone()} variant={timezone ? 'surface' : 'danger'}>
|
||||
{timezone ? 'Update Timezone' : 'Share Timezone'}
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => clearTimezone()} disabled={!timezone}>
|
||||
Clear Shared Timezone
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomTimezone.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomTimezone;
|
||||
@@ -0,0 +1,84 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.room-timezone {
|
||||
&__message,
|
||||
& .setting-tile {
|
||||
margin: var(--sp-tight) var(--sp-normal);
|
||||
}
|
||||
& .setting-tile {
|
||||
margin-bottom: var(--sp-loose);
|
||||
}
|
||||
|
||||
&__alias-item {
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
@extend .cp-fx__row--s-c;
|
||||
&.checkbox {
|
||||
@include dir.side(margin, 0 , var(--sp-tight));
|
||||
}
|
||||
& .text {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-txt__ellipsis;
|
||||
color: var(--tc-surface-high);
|
||||
span {
|
||||
margin: 0 var(--sp-extra-tight);
|
||||
padding: 0 var(--sp-ultra-tight);
|
||||
color: var(--bg-surface);
|
||||
background-color: var(--tc-surface-low);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__item-btns {
|
||||
@include dir.side(margin, 48px, 0);
|
||||
& button {
|
||||
padding: var(--sp-ultra-tight) var(--sp-tight);
|
||||
margin-bottom: var(--sp-tight);
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-bottom: var(--sp-normal);
|
||||
|
||||
& .checkbox {
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
min-width: 20px;
|
||||
}
|
||||
& > button {
|
||||
margin: 0 var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
&-label {
|
||||
padding: var(--sp-normal) var(--sp-normal) var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
@extend .cp-fx__item-one;
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
|
||||
& .input-container {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
}
|
||||
|
||||
&__input-status {
|
||||
padding: 0 var(--sp-normal);
|
||||
}
|
||||
&__valid {
|
||||
color: var(--tc-positive-high);
|
||||
padding-bottom: var(--sp-normal);
|
||||
}
|
||||
&__invalid {
|
||||
color: var(--tc-danger-high);
|
||||
padding-bottom: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import AsyncSearch from '../../../util/AsyncSearch';
|
||||
import { addRecentEmoji, getRecentEmojis } from './recent';
|
||||
import { TWEMOJI_BASE_URL } from '../../../util/twemojify';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
@@ -46,45 +47,49 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
||||
const emoji = groupEmojis[emojiIndex];
|
||||
emojiRow.push(
|
||||
<span key={emojiIndex}>
|
||||
{
|
||||
emoji.hexcode
|
||||
// This is a unicode emoji, and should be rendered with twemoji
|
||||
? parse(twemoji.parse(
|
||||
emoji.unicode,
|
||||
{
|
||||
attributes: () => ({
|
||||
unicode: emoji.unicode,
|
||||
shortcodes: emoji.shortcodes?.toString(),
|
||||
hexcode: emoji.hexcode,
|
||||
loading: 'lazy',
|
||||
}),
|
||||
},
|
||||
))
|
||||
// This is a custom emoji, and should be render as an mxc
|
||||
: (
|
||||
<img
|
||||
className="emoji"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
alt={emoji.shortcode}
|
||||
unicode={`:${emoji.shortcode}:`}
|
||||
shortcodes={emoji.shortcode}
|
||||
src={initMatrix.matrixClient.mxcUrlToHttp(emoji.mxc)}
|
||||
data-mx-emoticon={emoji.mxc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</span>,
|
||||
{emoji.hexcode ? (
|
||||
// This is a unicode emoji, and should be rendered with twemoji
|
||||
parse(
|
||||
twemoji.parse(emoji.unicode, {
|
||||
attributes: () => ({
|
||||
unicode: emoji.unicode,
|
||||
shortcodes: emoji.shortcodes?.toString(),
|
||||
hexcode: emoji.hexcode,
|
||||
loading: 'lazy',
|
||||
}),
|
||||
base: TWEMOJI_BASE_URL,
|
||||
})
|
||||
)
|
||||
) : (
|
||||
// This is a custom emoji, and should be render as an mxc
|
||||
<img
|
||||
className="emoji"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
alt={emoji.shortcode}
|
||||
unicode={`:${emoji.shortcode}:`}
|
||||
shortcodes={emoji.shortcode}
|
||||
src={initMatrix.matrixClient.mxcUrlToHttp(emoji.mxc)}
|
||||
data-mx-emoticon={emoji.mxc}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
emojiBoard.push(<div key={r} className="emoji-row">{emojiRow}</div>);
|
||||
emojiBoard.push(
|
||||
<div key={r} className="emoji-row">
|
||||
{emojiRow}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return emojiBoard;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="emoji-group">
|
||||
<Text className="emoji-group__header" variant="b2" weight="bold">{name}</Text>
|
||||
<Text className="emoji-group__header" variant="b2" weight="bold">
|
||||
{name}
|
||||
</Text>
|
||||
{groupEmojis.length !== 0 && <div className="emoji-set noselect">{getEmojiBoard()}</div>}
|
||||
</div>
|
||||
);
|
||||
@@ -92,17 +97,16 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
||||
|
||||
EmojiGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
groupEmojis: PropTypes.arrayOf(PropTypes.shape({
|
||||
length: PropTypes.number,
|
||||
unicode: PropTypes.string,
|
||||
hexcode: PropTypes.string,
|
||||
mxc: PropTypes.string,
|
||||
shortcode: PropTypes.string,
|
||||
shortcodes: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
})).isRequired,
|
||||
groupEmojis: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
length: PropTypes.number,
|
||||
unicode: PropTypes.string,
|
||||
hexcode: PropTypes.string,
|
||||
mxc: PropTypes.string,
|
||||
shortcode: PropTypes.string,
|
||||
shortcodes: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
const asyncSearch = new AsyncSearch();
|
||||
@@ -128,7 +132,13 @@ function SearchedEmoji() {
|
||||
|
||||
if (searchedEmojis === null) return false;
|
||||
|
||||
return <EmojiGroup key="-1" name={searchedEmojis.emojis.length === 0 ? 'No search result found' : 'Search results'} groupEmojis={searchedEmojis.emojis} />;
|
||||
return (
|
||||
<EmojiGroup
|
||||
key="-1"
|
||||
name={searchedEmojis.emojis.length === 0 ? 'No search result found' : 'Search results'}
|
||||
groupEmojis={searchedEmojis.emojis}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiBoard({ onSelect, searchRef }) {
|
||||
@@ -146,7 +156,10 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
if (typeof shortcodes === 'undefined') shortcodes = undefined;
|
||||
else shortcodes = shortcodes.split(',');
|
||||
return {
|
||||
unicode, hexcode, shortcodes, mxc,
|
||||
unicode,
|
||||
hexcode,
|
||||
shortcodes,
|
||||
mxc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -211,10 +224,9 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
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);
|
||||
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) {
|
||||
@@ -263,44 +275,41 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
/>
|
||||
)}
|
||||
<div className="emoji-board__nav-custom">
|
||||
{
|
||||
availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient
|
||||
.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
src={src}
|
||||
key={pack.packIndex}
|
||||
tooltip={pack.displayName ?? 'Unknown'}
|
||||
tooltipPlacement="left"
|
||||
isImage
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
{availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient.mxcUrlToHttp(
|
||||
pack.avatarUrl ?? pack.getEmojis()[0].mxc
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
src={src}
|
||||
key={pack.packIndex}
|
||||
tooltip={pack.displayName ?? 'Unknown'}
|
||||
tooltipPlacement="left"
|
||||
isImage
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="emoji-board__nav-twemoji">
|
||||
{
|
||||
[
|
||||
[0, EmojiIC, 'Smilies'],
|
||||
[1, DogIC, 'Animals'],
|
||||
[2, CupIC, 'Food'],
|
||||
[3, BallIC, 'Activities'],
|
||||
[4, PhotoIC, 'Travel'],
|
||||
[5, BulbIC, 'Objects'],
|
||||
[6, PeaceIC, 'Symbols'],
|
||||
[7, FlagIC, 'Flags'],
|
||||
].map(([indx, ico, name]) => (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
|
||||
key={indx}
|
||||
src={ico}
|
||||
tooltip={name}
|
||||
tooltipPlacement="left"
|
||||
/>
|
||||
))
|
||||
}
|
||||
{[
|
||||
[0, EmojiIC, 'Smilies'],
|
||||
[1, DogIC, 'Animals'],
|
||||
[2, CupIC, 'Food'],
|
||||
[3, BallIC, 'Activities'],
|
||||
[4, PhotoIC, 'Travel'],
|
||||
[5, BulbIC, 'Objects'],
|
||||
[6, PeaceIC, 'Symbols'],
|
||||
[7, FlagIC, 'Flags'],
|
||||
].map(([indx, ico, name]) => (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
|
||||
key={indx}
|
||||
src={ico}
|
||||
tooltip={name}
|
||||
tooltipPlacement="left"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
@@ -313,27 +322,25 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
<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} />
|
||||
))
|
||||
}
|
||||
{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>
|
||||
<div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
|
||||
<Text>:slight_smile:</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
@extend .cp-fx__column;
|
||||
}
|
||||
&__nav-twemoji {
|
||||
background: inherit;
|
||||
background-color: var(--bg-surface);
|
||||
position: sticky;
|
||||
bottom: -70%;
|
||||
z-index: 999;
|
||||
|
||||
@@ -56,7 +56,9 @@ function Drawer() {
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}, [selectedTab]);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg
|
||||
function DrawerBreadcrumb({ spaceId }) {
|
||||
const [, forceUpdate] = useState({});
|
||||
const scrollRef = useRef(null);
|
||||
const { roomList, notifications } = initMatrix;
|
||||
const { roomList, notifications, accountData } = initMatrix;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const spacePath = navigation.selectedSpacePath;
|
||||
|
||||
@@ -49,9 +49,9 @@ function DrawerBreadcrumb({ spaceId }) {
|
||||
}, [spaceId]);
|
||||
|
||||
function getHomeNotiExcept(childId) {
|
||||
const orphans = roomList.getOrphans();
|
||||
const childIndex = orphans.indexOf(childId);
|
||||
if (childId !== -1) orphans.splice(childIndex, 1);
|
||||
const orphans = roomList.getOrphans()
|
||||
.filter((id) => (id !== childId))
|
||||
.filter((id) => !accountData.spaceShortcut.has(id));
|
||||
|
||||
let noti = null;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
|
||||
iconSrc={HashGlobeIC}
|
||||
onClick={() => { afterOptionSelect(); openPublicRooms(); }}
|
||||
>
|
||||
Join public room
|
||||
Explore public rooms
|
||||
</MenuItem>
|
||||
)}
|
||||
{ !spaceId && (
|
||||
|
||||
@@ -25,7 +25,7 @@ function Home({ spaceId }) {
|
||||
let directIds = [];
|
||||
|
||||
if (spaceId) {
|
||||
const spaceChildIds = roomList.getSpaceChildren(spaceId);
|
||||
const spaceChildIds = roomList.getSpaceChildren(spaceId) ?? [];
|
||||
spaceIds = spaceChildIds.filter((roomId) => spaces.has(roomId));
|
||||
roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId));
|
||||
directIds = spaceChildIds.filter((roomId) => directs.has(roomId));
|
||||
|
||||
@@ -25,8 +25,9 @@ function Selector({
|
||||
const noti = initMatrix.notifications;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
|
||||
const imageSrc = (isDM
|
||||
? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop')
|
||||
: room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop')) || null;
|
||||
|
||||
const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
|
||||
|
||||
@@ -57,8 +58,8 @@ function Selector({
|
||||
key={roomId}
|
||||
name={room.name}
|
||||
roomId={roomId}
|
||||
imageSrc={isDM ? imageSrc : null}
|
||||
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
|
||||
imageSrc={imageSrc}
|
||||
iconSrc={imageSrc ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
|
||||
isSelected={navigation.selectedRoomId === roomId}
|
||||
isMuted={isMuted}
|
||||
isUnread={!isMuted && noti.hasNoti(roomId)}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
|
||||
import './ProfileEditor.scss';
|
||||
|
||||
// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
|
||||
function ProfileEditor({ userId }) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const mx = initMatrix.matrixClient;
|
||||
@@ -96,7 +95,7 @@ function ProfileEditor({ userId }) {
|
||||
const renderInfo = () => (
|
||||
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
|
||||
<div>
|
||||
<Text variant="h2" primary weight="medium">{twemojify(username)}</Text>
|
||||
<Text variant="h2" primary weight="medium">{twemojify(username) ?? userId}</Text>
|
||||
<IconButton
|
||||
src={PencilIC}
|
||||
size="extra-small"
|
||||
@@ -111,7 +110,7 @@ function ProfileEditor({ userId }) {
|
||||
return (
|
||||
<div className="profile-editor">
|
||||
<ImageUpload
|
||||
text={username}
|
||||
text={username ?? userId}
|
||||
bgColor={colorMXID(userId)}
|
||||
imageSrc={avatarSrc}
|
||||
onUpload={handleAvatarUpload}
|
||||
|
||||
@@ -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,19 +209,18 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||
};
|
||||
|
||||
const toggleIgnore = async () => {
|
||||
const ignoredUsers = mx.getIgnoredUsers();
|
||||
const uIndex = ignoredUsers.indexOf(userId);
|
||||
if (uIndex >= 0) {
|
||||
if (uIndex === -1) return;
|
||||
ignoredUsers.splice(uIndex, 1);
|
||||
} else ignoredUsers.push(userId);
|
||||
const isIgnored = mx.getIgnoredUsers().includes(userId);
|
||||
|
||||
try {
|
||||
setIsIgnoring(true);
|
||||
await mx.setIgnoredUsers(ignoredUsers);
|
||||
if (isIgnored) {
|
||||
await roomActions.unignore([userId]);
|
||||
} else {
|
||||
await roomActions.ignore([userId]);
|
||||
}
|
||||
|
||||
if (isMountedRef.current === false) return;
|
||||
setIsUserIgnored(uIndex < 0);
|
||||
setIsUserIgnored(!isIgnored);
|
||||
setIsIgnoring(false);
|
||||
} catch {
|
||||
setIsIgnoring(false);
|
||||
|
||||
@@ -11,12 +11,14 @@ import JoinAlias from '../join-alias/JoinAlias';
|
||||
import EmojiVerification from '../emoji-verification/EmojiVerification';
|
||||
|
||||
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
|
||||
import TextTranslate from '../text-translate/TextTranslate';
|
||||
|
||||
function Dialogs() {
|
||||
return (
|
||||
<>
|
||||
<ReadReceipts />
|
||||
<ViewSource />
|
||||
<TextTranslate />
|
||||
<ProfileViewer />
|
||||
<ShortcutSpaces />
|
||||
<CreateRoom />
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import Text from '../../atoms/text/Text';
|
||||
|
||||
import './RoomBanner.scss';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
|
||||
import RecentClockIC from '../../../../public/res/ic/outlined/recent-clock.svg';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
import { getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
||||
|
||||
function RoomBanner({ roomId }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const [time, setTime] = useState(null);
|
||||
|
||||
let partner = null;
|
||||
|
||||
if (isDM) {
|
||||
partner = room.getAvatarFallbackMember();
|
||||
}
|
||||
|
||||
const timezoneEventId = room.currentState.getStateEvents('in.cinny.shared_timezone', partner?.userId)?.event?.content?.user_timezone_event;
|
||||
const timezoneEvent = room.findEventById(timezoneEventId);
|
||||
|
||||
let timezoneContent = null;
|
||||
|
||||
const updateTime = () => {
|
||||
if (isDM) {
|
||||
const timezone = timezoneContent?.user_timezone;
|
||||
const date = new Date();
|
||||
|
||||
try {
|
||||
const partnerLocalTime = date.toLocaleTimeString([], { timeZone: timezone, hour: '2-digit', minute: '2-digit' });
|
||||
const userTime = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
// if the partner time and user time are the same, we wont show the time
|
||||
if (userTime !== partnerLocalTime) { setTime(`${getUsernameOfRoomMember(partner)}'s local time is: ${partnerLocalTime}`); }
|
||||
} catch {
|
||||
setTime(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (timezoneEvent) {
|
||||
if (!timezoneEvent.getClearContent()) {
|
||||
timezoneEvent.attemptDecryption(mx.crypto);
|
||||
}
|
||||
|
||||
timezoneContent = timezoneEvent.getContent();
|
||||
timezoneEvent.getDecryptionPromise()?.then(() => {
|
||||
timezoneContent = timezoneEvent.getContent();
|
||||
updateTime();
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateTime();
|
||||
const interval = setInterval(() => updateTime(), 20000);
|
||||
return () => {
|
||||
setTime(null);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [room]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ time ? (
|
||||
<div className="room-view__banner">
|
||||
<RawIcon src={RecentClockIC} size="small" />
|
||||
<Text>
|
||||
{ twemojify(time) }
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
: null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomBanner.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomBanner;
|
||||
@@ -0,0 +1,28 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/text';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-view {
|
||||
&__banner {
|
||||
background-color: var(--bg-surface-low);
|
||||
width: calc(100% - var(--sp-extra-loose));
|
||||
|
||||
padding: var(--sp-ultra-tight);
|
||||
border-radius: var(--bo-radius);
|
||||
align-items: center;
|
||||
|
||||
position: relative;
|
||||
@include dir.prop(left, var(--sp-normal), unset);
|
||||
@include dir.prop(right, unset, var(--sp-normal));
|
||||
display: flex;
|
||||
z-index: 999;
|
||||
transform: translate(0%, 10%);
|
||||
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
|
||||
|
||||
& .ic-raw {
|
||||
opacity: 0.4;
|
||||
margin: 0 var(--sp-extra-tight);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg';
|
||||
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
import RoomTimezone from '../../molecules/room-timezone/RoomTimeZone';
|
||||
|
||||
const tabText = {
|
||||
GENERAL: 'General',
|
||||
@@ -120,6 +121,10 @@ function GeneralSettings({ roomId }) {
|
||||
<MenuHeader>Room addresses</MenuHeader>
|
||||
<RoomAliases roomId={roomId} />
|
||||
</div>
|
||||
<div className="room-settings__card">
|
||||
<MenuHeader>Timezones</MenuHeader>
|
||||
<RoomTimezone roomId={roomId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import RoomViewContent from './RoomViewContent';
|
||||
import RoomViewFloating from './RoomViewFloating';
|
||||
import RoomViewInput from './RoomViewInput';
|
||||
import RoomViewCmdBar from './RoomViewCmdBar';
|
||||
import RoomBanner from './RoomBanner';
|
||||
|
||||
const viewEvent = new EventEmitter();
|
||||
|
||||
@@ -42,6 +43,7 @@ function RoomView({ roomTimeline, eventId }) {
|
||||
return (
|
||||
<div className="room-view" ref={roomViewRef}>
|
||||
<RoomViewHeader roomId={roomId} />
|
||||
<RoomBanner roomId={roomId} />
|
||||
<div className="room-view__content-wrapper">
|
||||
<div className="room-view__scrollable">
|
||||
<RoomViewContent
|
||||
|
||||
@@ -5,16 +5,9 @@ import './RoomViewCmdBar.scss';
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
import { twemojify, TWEMOJI_BASE_URL } 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';
|
||||
|
||||
@@ -22,37 +15,7 @@ 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';
|
||||
|
||||
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),
|
||||
}];
|
||||
import commands from './commands';
|
||||
|
||||
function CmdItem({ onClick, children }) {
|
||||
return (
|
||||
@@ -68,19 +31,19 @@ CmdItem.propTypes = {
|
||||
|
||||
function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||
function renderCmdSuggestions(cmdPrefix, cmds) {
|
||||
const cmdOptString = (typeof option === 'string') ? `/${option}` : '/?';
|
||||
const cmdOptString = typeof option === 'string' ? `/${option}` : '/?';
|
||||
return cmds.map((cmd) => (
|
||||
<CmdItem
|
||||
key={cmd.name}
|
||||
key={cmd}
|
||||
onClick={() => {
|
||||
fireCmd({
|
||||
prefix: cmdPrefix,
|
||||
option,
|
||||
result: cmd,
|
||||
result: commands[cmd],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text variant="b2">{`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`}</Text>
|
||||
<Text variant="b2">{`${cmd}${cmd.isOptions ? cmdOptString : ''}`}</Text>
|
||||
</CmdItem>
|
||||
));
|
||||
}
|
||||
@@ -90,15 +53,15 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||
|
||||
// Renders a small Twemoji
|
||||
function renderTwemoji(emoji) {
|
||||
return parse(twemoji.parse(
|
||||
emoji.unicode,
|
||||
{
|
||||
return parse(
|
||||
twemoji.parse(emoji.unicode, {
|
||||
attributes: () => ({
|
||||
unicode: emoji.unicode,
|
||||
shortcodes: emoji.shortcodes?.toString(),
|
||||
}),
|
||||
},
|
||||
));
|
||||
base: TWEMOJI_BASE_URL,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Render a custom emoji
|
||||
@@ -124,10 +87,12 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||
return emos.map((emoji) => (
|
||||
<CmdItem
|
||||
key={emoji.shortcode}
|
||||
onClick={() => fireCmd({
|
||||
prefix: emPrefix,
|
||||
result: emoji,
|
||||
})}
|
||||
onClick={() =>
|
||||
fireCmd({
|
||||
prefix: emPrefix,
|
||||
result: emoji,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text variant="b1">{renderEmoji(emoji)}</Text>
|
||||
<Text variant="b2">{`:${emoji.shortcode}:`}</Text>
|
||||
@@ -209,8 +174,8 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const setupSearch = {
|
||||
'/': () => {
|
||||
asyncSearch.setup(commands, { keys: ['name'], isContain: true });
|
||||
setCmd({ prefix, suggestions: commands });
|
||||
asyncSearch.setup(Object.keys(commands), { isContain: true });
|
||||
setCmd({ prefix, suggestions: Object.keys(commands) });
|
||||
},
|
||||
':': () => {
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(roomId);
|
||||
@@ -224,10 +189,13 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
});
|
||||
},
|
||||
'@': () => {
|
||||
const members = mx.getRoom(roomId).getJoinedMembers().map((member) => ({
|
||||
name: member.name,
|
||||
userId: member.userId.slice(1),
|
||||
}));
|
||||
const members = mx
|
||||
.getRoom(roomId)
|
||||
.getJoinedMembers()
|
||||
.map((member) => ({
|
||||
name: member.name,
|
||||
userId: member.userId.slice(1),
|
||||
}));
|
||||
asyncSearch.setup(members, { keys: ['name', 'userId'], limit: 20 });
|
||||
const endIndex = members.length > 20 ? 20 : members.length;
|
||||
setCmd({ prefix, suggestions: members.slice(0, endIndex) });
|
||||
@@ -242,8 +210,9 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
}
|
||||
function fireCmd(myCmd) {
|
||||
if (myCmd.prefix === '/') {
|
||||
myCmd.result.exe(roomId, myCmd.option);
|
||||
viewEvent.emit('cmd_fired');
|
||||
viewEvent.emit('cmd_fired', {
|
||||
replace: `/${myCmd.result.name}`,
|
||||
});
|
||||
}
|
||||
if (myCmd.prefix === ':') {
|
||||
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
|
||||
@@ -313,9 +282,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||
</div>
|
||||
<div className="cmd-bar__content">
|
||||
<ScrollView horizontal vertical={false} invisible>
|
||||
<div className="cmd-bar__content-suggestions">
|
||||
{ renderSuggestions(cmd, fireCmd) }
|
||||
</div>
|
||||
<div className="cmd-bar__content-suggestions">{renderSuggestions(cmd, fireCmd)}</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,7 @@ import { blurOnBubbling } from '../../atoms/button/script';
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
|
||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||
import { toggleNavigation, toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
@@ -24,6 +23,7 @@ import RoomOptions from '../../molecules/room-options/RoomOptions';
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import HomeIC from '../../../../public/res/ic/outlined/home.svg';
|
||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
|
||||
|
||||
@@ -33,9 +33,10 @@ function RoomViewHeader({ roomId }) {
|
||||
const [, forceUpdate] = useForceUpdate();
|
||||
const mx = initMatrix.matrixClient;
|
||||
const isDM = initMatrix.roomList.directs.has(roomId);
|
||||
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||
avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
|
||||
const roomName = mx.getRoom(roomId).name;
|
||||
const room = mx.getRoom(roomId);
|
||||
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
|
||||
const roomName = room.name;
|
||||
|
||||
const roomHeaderBtnRef = useRef(null);
|
||||
useEffect(() => {
|
||||
@@ -75,11 +76,17 @@ function RoomViewHeader({ roomId }) {
|
||||
return (
|
||||
<Header>
|
||||
<IconButton
|
||||
src={BackArrowIC}
|
||||
src={HomeIC}
|
||||
className="room-header__back-btn"
|
||||
tooltip="Return to navigation"
|
||||
onClick={() => openNavigation()}
|
||||
/>
|
||||
<IconButton
|
||||
src={BackArrowIC}
|
||||
className="room-header__hide-navigation-btn"
|
||||
tooltip="Hide Navigation"
|
||||
onClick={() => toggleNavigation()}
|
||||
/>
|
||||
<button
|
||||
ref={roomHeaderBtnRef}
|
||||
className="room-header__btn"
|
||||
@@ -93,7 +100,7 @@ function RoomViewHeader({ roomId }) {
|
||||
</TitleWrapper>
|
||||
<RawIcon src={ChevronBottomIC} />
|
||||
</button>
|
||||
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />
|
||||
{mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
|
||||
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
||||
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
|
||||
<IconButton
|
||||
|
||||
@@ -45,3 +45,10 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.room-header__hide-navigation-btn {
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
@include screen.smallerThan(mobileBreakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomViewInput.scss';
|
||||
import { generateRequestUrl, normaliseResponse } from 'google-translate-api-browser';
|
||||
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
|
||||
@@ -21,6 +22,7 @@ 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';
|
||||
@@ -29,19 +31,17 @@ 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';
|
||||
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;
|
||||
let cmdCursorPos = null;
|
||||
function RoomViewInput({
|
||||
roomId, roomTimeline, viewEvent,
|
||||
}) {
|
||||
function RoomViewInput({ roomId, roomTimeline, viewEvent }) {
|
||||
const [attachment, setAttachment] = useState(null);
|
||||
const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
|
||||
const [replyTo, setReplyTo] = useState(null);
|
||||
|
||||
const textAreaRef = useRef(null);
|
||||
@@ -60,11 +60,9 @@ function RoomViewInput({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
settings.on(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
||||
roomsInput.on(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
|
||||
viewEvent.on('focus_msg_input', requestFocusInput);
|
||||
return () => {
|
||||
settings.removeListener(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
||||
roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_SET, setAttachment);
|
||||
viewEvent.removeListener('focus_msg_input', requestFocusInput);
|
||||
};
|
||||
@@ -84,7 +82,9 @@ function RoomViewInput({
|
||||
function uploadingProgress(myRoomId, { loaded, total }) {
|
||||
if (myRoomId !== roomId) return;
|
||||
const progressPer = Math.round((loaded * 100) / total);
|
||||
uploadProgressRef.current.textContent = `Uploading: ${bytesToSize(loaded)}/${bytesToSize(total)} (${progressPer}%)`;
|
||||
uploadProgressRef.current.textContent = `Uploading: ${bytesToSize(loaded)}/${bytesToSize(
|
||||
total
|
||||
)} (${progressPer}%)`;
|
||||
inputBaseRef.current.style.backgroundImage = `linear-gradient(90deg, var(--bg-surface-hover) ${progressPer}%, var(--bg-surface-low) ${progressPer}%)`;
|
||||
}
|
||||
function clearAttachment(myRoomId) {
|
||||
@@ -134,7 +134,7 @@ function RoomViewInput({
|
||||
textAreaRef.current.value = replaceCmdWith(
|
||||
msg,
|
||||
cmdCursorPos,
|
||||
typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '',
|
||||
typeof cmdData?.replace !== 'undefined' ? cmdData.replace : ''
|
||||
);
|
||||
deactivateCmd();
|
||||
}
|
||||
@@ -144,9 +144,14 @@ function RoomViewInput({
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
|
||||
function setUpReply(userId, eventId, body) {
|
||||
function setUpReply(userId, eventId, body, formattedBody) {
|
||||
setReplyTo({ userId, eventId, body });
|
||||
roomsInput.setReplyTo(roomId, { userId, eventId, body });
|
||||
roomsInput.setReplyTo(roomId, {
|
||||
userId,
|
||||
eventId,
|
||||
body,
|
||||
formattedBody,
|
||||
});
|
||||
focusInput();
|
||||
}
|
||||
|
||||
@@ -182,30 +187,107 @@ function RoomViewInput({
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
const sendMessage = async () => {
|
||||
requestAnimationFrame(() => deactivateCmdAndEmit());
|
||||
const msgBody = textAreaRef.current.value;
|
||||
const sendBody = async (body, options) => {
|
||||
console.log(options)
|
||||
const opt = options ?? {};
|
||||
if (!opt.msgType) opt.msgType = 'm.text';
|
||||
if (typeof opt.autoMarkdown !== 'boolean') opt.autoMarkdown = true;
|
||||
if (roomsInput.isSending(roomId)) return;
|
||||
if (msgBody.trim() === '' && attachment === null) return;
|
||||
sendIsTyping(false);
|
||||
|
||||
roomsInput.setMessage(roomId, msgBody);
|
||||
roomsInput.setMessage(roomId, body);
|
||||
if (attachment !== null) {
|
||||
roomsInput.setAttachment(roomId, attachment);
|
||||
}
|
||||
textAreaRef.current.disabled = true;
|
||||
textAreaRef.current.style.cursor = 'not-allowed';
|
||||
await roomsInput.sendInput(roomId);
|
||||
await roomsInput.sendInput(roomId, opt);
|
||||
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', 'plain', 'rainbow'].includes(cmdName)) {
|
||||
commands[cmdName].exe(roomId, cmdData, sendBody);
|
||||
return;
|
||||
}
|
||||
commands[cmdName].exe(roomId, cmdData);
|
||||
};
|
||||
|
||||
async function translate(translateUrl) {
|
||||
const response = await fetch(`${translateUrl}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
const body = await response.json();
|
||||
if (settings.getTranslationAPIIndex() === 1) {
|
||||
let result = '';
|
||||
for (let i = 0; i < body.translations.length; i += 1) {
|
||||
result += body.translations[i].text;
|
||||
}
|
||||
if (result === undefined) {
|
||||
result = body.message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return normaliseResponse(body, settings.getTranslationSelfLanguage());
|
||||
}
|
||||
|
||||
async function translateText(text) {
|
||||
const origin = text.replace(/> <.*> .*\n/gms, '').trim();
|
||||
let url = generateRequestUrl(origin, { to: settings.getTranslationSelfLanguage() }).replace(
|
||||
'https://translate.google.com/',
|
||||
'https://panel.icarus.today/'
|
||||
);
|
||||
|
||||
if (settings.getTranslationAPIIndex() === 1) {
|
||||
url = `https://rwwhwqaduuvdbswpmq6p.icarus.today/&text=${origin}&target_lang=${settings.getTranslationSelfLanguage()}`;
|
||||
}
|
||||
|
||||
return translate(url);
|
||||
}
|
||||
|
||||
const sendMessage = async () => {
|
||||
requestAnimationFrame(() => deactivateCmdAndEmit());
|
||||
const originalMsg = textAreaRef.current.value.trim();
|
||||
|
||||
if (originalMsg.startsWith('/')) {
|
||||
processCommand(originalMsg.trim());
|
||||
textAreaRef.current.value = '';
|
||||
textAreaRef.current.style.height = 'unset';
|
||||
return;
|
||||
}
|
||||
if (originalMsg === '' && attachment === null) return;
|
||||
|
||||
if (originalMsg.length > 0 && settings.enableSelfTranslate) {
|
||||
document.getElementById('message-textarea').disabled = true;
|
||||
translateText(originalMsg).then((result) => {
|
||||
let msgBody = '';
|
||||
if (settings.getTranslationAPIIndex() === 0) msgBody = result.text;
|
||||
else msgBody = result;
|
||||
msgBody += ` (Translated to: ${settings.getTranslationSelfLanguage()})`;
|
||||
msgBody += `\nOriginal Text: \n\`${originalMsg}\``;
|
||||
sendBody(msgBody);
|
||||
document.getElementById('message-textarea').disabled = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendBody(originalMsg);
|
||||
};
|
||||
|
||||
const handleSendSticker = async (data) => {
|
||||
roomsInput.sendSticker(roomId, data);
|
||||
};
|
||||
@@ -323,19 +405,31 @@ function RoomViewInput({
|
||||
if (!canISend || tombstoneEvent) {
|
||||
return (
|
||||
<Text className="room-input__alert">
|
||||
{
|
||||
tombstoneEvent
|
||||
? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.'
|
||||
: 'You do not have permission to post to this room'
|
||||
}
|
||||
{tombstoneEvent
|
||||
? tombstoneEvent.getContent()?.body ??
|
||||
'This room has been replaced and is no longer active.'
|
||||
: 'You do not have permission to post to this room'}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
|
||||
<input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" />
|
||||
<IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
|
||||
<div
|
||||
className={`room-input__option-container${
|
||||
attachment === null ? '' : ' room-attachment__option'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
onChange={uploadFileChange}
|
||||
style={{ display: 'none' }}
|
||||
ref={uploadInputRef}
|
||||
type="file"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={handleUploadClick}
|
||||
tooltip={attachment === null ? 'Upload' : 'Cancel'}
|
||||
src={CirclePlusIC}
|
||||
/>
|
||||
</div>
|
||||
<div ref={inputBaseRef} className="room-input__input-container">
|
||||
{roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
|
||||
@@ -352,7 +446,6 @@ function RoomViewInput({
|
||||
/>
|
||||
</Text>
|
||||
</ScrollView>
|
||||
{isMarkdown && <RawIcon size="extra-small" src={MarkdownIC} />}
|
||||
</div>
|
||||
<div ref={rightOptionsRef} className="room-input__option-container">
|
||||
<IconButton
|
||||
@@ -372,7 +465,7 @@ function RoomViewInput({
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)
|
||||
);
|
||||
}}
|
||||
tooltip="Sticker"
|
||||
@@ -381,7 +474,7 @@ function RoomViewInput({
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
const cords = getEventCords(e);
|
||||
cords.x += (document.dir === 'rtl' ? -80 : 80);
|
||||
cords.x += document.dir === 'rtl' ? -80 : 80;
|
||||
cords.y -= 250;
|
||||
openEmojiBoard(cords, addEmoji);
|
||||
}}
|
||||
@@ -398,15 +491,25 @@ function RoomViewInput({
|
||||
const fileType = attachment.type.slice(0, attachment.type.indexOf('/'));
|
||||
return (
|
||||
<div className="room-attachment">
|
||||
<div className={`room-attachment__preview${fileType !== 'image' ? ' room-attachment__icon' : ''}`}>
|
||||
{fileType === 'image' && <img alt={attachment.name} src={URL.createObjectURL(attachment)} />}
|
||||
<div
|
||||
className={`room-attachment__preview${
|
||||
fileType !== 'image' ? ' room-attachment__icon' : ''
|
||||
}`}
|
||||
>
|
||||
{fileType === 'image' && (
|
||||
<img alt={attachment.name} src={URL.createObjectURL(attachment)} />
|
||||
)}
|
||||
{fileType === 'video' && <RawIcon src={VLCIC} />}
|
||||
{fileType === 'audio' && <RawIcon src={VolumeFullIC} />}
|
||||
{fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && <RawIcon src={FileIC} />}
|
||||
{fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && (
|
||||
<RawIcon src={FileIC} />
|
||||
)}
|
||||
</div>
|
||||
<div className="room-attachment__info">
|
||||
<Text variant="b1">{attachment.name}</Text>
|
||||
<Text variant="b3"><span ref={uploadProgressRef}>{`size: ${bytesToSize(attachment.size)}`}</span></Text>
|
||||
<Text variant="b3">
|
||||
<span ref={uploadProgressRef}>{`size: ${bytesToSize(attachment.size)}`}</span>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -437,12 +540,15 @@ function RoomViewInput({
|
||||
|
||||
return (
|
||||
<>
|
||||
{ replyTo !== null && attachReply()}
|
||||
{ attachment !== null && attachFile() }
|
||||
<form className="room-input" onSubmit={(e) => { e.preventDefault(); }}>
|
||||
{
|
||||
renderInputs()
|
||||
}
|
||||
{replyTo !== null && attachReply()}
|
||||
{attachment !== null && attachFile()}
|
||||
<form
|
||||
className="room-input"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{renderInputs()}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function rainbowText(str) {
|
||||
// 定义渐变颜色数组
|
||||
const startColor = [255, 0, 0]; // 红色
|
||||
const middleColor = [0, 255, 0]; // 绿色
|
||||
const endColor = [0, 0, 255]; // 蓝色
|
||||
const numSteps = str.length - 1;
|
||||
const stepsToGreen = Math.round(numSteps / 2);
|
||||
const stepsToBlue = numSteps - stepsToGreen;
|
||||
const stepToGreen = [
|
||||
(middleColor[0] - startColor[0]) / stepsToGreen,
|
||||
(middleColor[1] - startColor[1]) / stepsToGreen,
|
||||
(middleColor[2] - startColor[2]) / stepsToGreen
|
||||
];
|
||||
const stepToBlue = [
|
||||
(endColor[0] - middleColor[0]) / stepsToBlue,
|
||||
(endColor[1] - middleColor[1]) / stepsToBlue,
|
||||
(endColor[2] - middleColor[2]) / stepsToBlue
|
||||
];
|
||||
|
||||
// 生成 HTML 字符串
|
||||
let html = '';
|
||||
const currentColor = startColor.slice();
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
// 转换颜色为 16 进制字符串
|
||||
const hexColor = `#${ currentColor.map(c => {
|
||||
const hex = Math.round(c).toString(16);
|
||||
return hex.length === 1 ? `0${ hex}` : hex;
|
||||
}).join('')}`;
|
||||
|
||||
// 将字符包装在 <font> 标签中,并设置颜色
|
||||
const fontTag = `<font color="${hexColor}">${str[i]}</font>`;
|
||||
html += fontTag;
|
||||
|
||||
// 计算下一个颜色
|
||||
if (i < stepsToGreen) {
|
||||
currentColor[0] += stepToGreen[0];
|
||||
currentColor[1] += stepToGreen[1];
|
||||
currentColor[2] += stepToGreen[2];
|
||||
} else {
|
||||
currentColor[0] += stepToBlue[0];
|
||||
currentColor[1] += stepToBlue[1];
|
||||
currentColor[2] += stepToBlue[2];
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const commands = {
|
||||
me: {
|
||||
name: 'me',
|
||||
description: 'Display action',
|
||||
exe: (roomId, data, onSuccess) => {
|
||||
const body = data.trim();
|
||||
if (body === '') return;
|
||||
onSuccess(body, { msgType: 'm.emote' });
|
||||
},
|
||||
},
|
||||
shrug: {
|
||||
name: 'shrug',
|
||||
description: 'Send ¯\\_(ツ)_/¯ as message',
|
||||
exe: (roomId, data, onSuccess) => onSuccess(
|
||||
`¯\\_(ツ)_/¯${data.trim() !== '' ? ` ${data}` : ''}`,
|
||||
{ msgType: 'm.text' },
|
||||
),
|
||||
},
|
||||
plain: {
|
||||
name: 'plain',
|
||||
description: 'Send plain text message',
|
||||
exe: (roomId, data, onSuccess) => {
|
||||
const body = data.trim();
|
||||
if (body === '') return;
|
||||
onSuccess(body, { msgType: 'm.text', autoMarkdown: false });
|
||||
},
|
||||
},
|
||||
help: {
|
||||
name: 'help',
|
||||
description: 'View all commands',
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
exe: () => openHelpDialog(),
|
||||
},
|
||||
startdm: {
|
||||
name: 'startdm',
|
||||
description: 'Start direct message with user. Example: /startdm userId1',
|
||||
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 address. Example: /join address1 address2',
|
||||
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 nick in current room.',
|
||||
exe: (roomId, data) => {
|
||||
const nick = data.trim();
|
||||
if (nick === '') return;
|
||||
roomActions.setMyRoomNick(roomId, nick);
|
||||
},
|
||||
},
|
||||
myroomavatar: {
|
||||
name: 'myroomavatar',
|
||||
description: 'Change profile picture in current room. 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);
|
||||
},
|
||||
},
|
||||
rainbow: {
|
||||
name: 'rainbow',
|
||||
description: 'Rainbow text',
|
||||
exe: (roomId, data, onSuccess) => onSuccess(
|
||||
data,
|
||||
{ msgtype:'m.text', format: 'org.matrix.custom.html', formatted_body: rainbowText(data) },
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,10 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +168,7 @@ function Search() {
|
||||
}
|
||||
};
|
||||
|
||||
const notifs = initMatrix.notifications;
|
||||
const noti = initMatrix.notifications;
|
||||
const renderRoomSelector = (item) => {
|
||||
let imageSrc = null;
|
||||
let iconSrc = null;
|
||||
@@ -178,9 +178,6 @@ function Search() {
|
||||
iconSrc = joinRuleToIconSrc(item.room.getJoinRule(), item.type === 'space');
|
||||
}
|
||||
|
||||
const isUnread = notifs.hasNoti(item.roomId);
|
||||
const noti = notifs.getNoti(item.roomId);
|
||||
|
||||
return (
|
||||
<RoomSelector
|
||||
key={item.roomId}
|
||||
@@ -189,9 +186,9 @@ function Search() {
|
||||
roomId={item.roomId}
|
||||
imageSrc={imageSrc}
|
||||
iconSrc={iconSrc}
|
||||
isUnread={isUnread}
|
||||
notificationCount={noti.total}
|
||||
isAlert={noti.highlight > 0}
|
||||
isUnread={noti.hasNoti(item.roomId)}
|
||||
notificationCount={noti.getTotalNoti(item.roomId)}
|
||||
isAlert={noti.getHighlightNoti(item.roomId) > 0}
|
||||
onClick={() => openItem(item.roomId, item.type)}
|
||||
/>
|
||||
);
|
||||
@@ -207,7 +204,7 @@ function Search() {
|
||||
size="small"
|
||||
>
|
||||
<div className="search-dialog">
|
||||
<form className="search-dialog__input" onSubmit={(e) => { e.preventDefault(); openFirstResult()}}>
|
||||
<form className="search-dialog__input" onSubmit={(e) => { e.preventDefault(); openFirstResult(); }}>
|
||||
<RawIcon src={SearchIC} size="small" />
|
||||
<Input
|
||||
onChange={handleOnChange}
|
||||
|
||||
@@ -180,13 +180,15 @@ function DeviceManage() {
|
||||
}
|
||||
content={(
|
||||
<>
|
||||
<Text variant="b3">
|
||||
Last activity
|
||||
<span style={{ color: 'var(--tc-surface-normal)' }}>
|
||||
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
|
||||
</span>
|
||||
{lastIP ? ` at ${lastIP}` : ''}
|
||||
</Text>
|
||||
{lastTS && (
|
||||
<Text variant="b3">
|
||||
Last activity
|
||||
<span style={{ color: 'var(--tc-surface-normal)' }}>
|
||||
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
|
||||
</span>
|
||||
{lastIP ? ` at ${lastIP}` : ''}
|
||||
</Text>
|
||||
)}
|
||||
{isCurrentDevice && (
|
||||
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
|
||||
{`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './Settings.scss';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import settings from '../../../client/state/settings';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import {
|
||||
toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
|
||||
toggleNotifications, toggleNotificationSounds,
|
||||
toggleNotifications, toggleNotificationSounds, toggleSelfTranslate, toggleReadReceipts, toggleHideNavigation
|
||||
} from '../../../client/action/settings';
|
||||
import logout from '../../../client/action/logout';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Toggle from '../../atoms/button/Toggle';
|
||||
import Tabs from '../../atoms/tabs/Tabs';
|
||||
@@ -25,6 +26,9 @@ 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 GlobalNotification from '../../molecules/global-notification/GlobalNotification';
|
||||
import KeywordNotification from '../../molecules/global-notification/KeywordNotification';
|
||||
import IgnoreUserList from '../../molecules/global-notification/IgnoreUserList';
|
||||
|
||||
import ProfileEditor from '../profile-editor/ProfileEditor';
|
||||
import CrossSigning from './CrossSigning';
|
||||
@@ -38,17 +42,49 @@ import BellIC from '../../../../public/res/ic/outlined/bell.svg';
|
||||
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
|
||||
import PowerIC from '../../../../public/res/ic/outlined/power.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||
|
||||
import CinnySVG from '../../../../public/res/svg/cinny.svg';
|
||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||
|
||||
import '../../i18n.jsx'
|
||||
import PeopleSelector from '../../molecules/people-selector/PeopleSelector';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { openReusableDialog } from '../../../client/action/navigation';
|
||||
|
||||
let capabilities = {
|
||||
privateReadReceipts: false,
|
||||
}
|
||||
|
||||
|
||||
function AppearanceSection() {
|
||||
const [, updateState] = useState({});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSetLanguage = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { keywordInput } = evt.target.elements;
|
||||
const value = keywordInput.value.trim();
|
||||
if (value === '') return;
|
||||
settings.setTranslationLanguage(value);
|
||||
document.getElementById('set-button').getElementsByClassName("text-normal")[0].textContent = "Done!";
|
||||
};
|
||||
|
||||
const handleSetSelfLanguage = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { keywordInput } = evt.target.elements;
|
||||
const value = keywordInput.value.trim();
|
||||
if (value === '') return;
|
||||
settings.setTranslationSelfLanguage(value);
|
||||
document.getElementById('set-self-button').getElementsByClassName("text-normal")[0].textContent = "Done!";
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="settings-appearance">
|
||||
<div className="settings-appearance__card">
|
||||
<MenuHeader>Theme</MenuHeader>
|
||||
<MenuHeader>{t("Theme")}</MenuHeader>
|
||||
<SettingTile
|
||||
title="Follow system theme"
|
||||
options={(
|
||||
@@ -69,6 +105,9 @@ function AppearanceSection() {
|
||||
{ text: 'Silver' },
|
||||
{ text: 'Dark' },
|
||||
{ text: 'Butter' },
|
||||
{ text: 'NGA'},
|
||||
{ text: 'NGA Green'},
|
||||
{ text: '?'}
|
||||
]}
|
||||
onSelect={(index) => {
|
||||
if (settings.useSystemTheme) toggleSystemTheme();
|
||||
@@ -101,6 +140,16 @@ function AppearanceSection() {
|
||||
)}
|
||||
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Hide navigation by default"
|
||||
options={(
|
||||
<Toggle
|
||||
isActive={settings.hideNavigation}
|
||||
onToggle={() => { toggleHideNavigation(); updateState({}); }}
|
||||
/>
|
||||
)}
|
||||
content={<Text variant="b3">Hide the navigation bar by default, when cinny is opened.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Hide nick/avatar events"
|
||||
options={(
|
||||
@@ -112,6 +161,62 @@ function AppearanceSection() {
|
||||
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-appearance__card">
|
||||
<MenuHeader>Translation</MenuHeader>
|
||||
<SettingTile
|
||||
title="Select API"
|
||||
content={(
|
||||
<SegmentedControls
|
||||
selected={settings.getTranslationAPIIndex()}
|
||||
segments={[
|
||||
{ text: 'Google' },
|
||||
{ text: 'DeepL' }
|
||||
]}
|
||||
onSelect={(index) => {
|
||||
settings.setTranslationAPI(index);
|
||||
updateState({});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Custom target language"
|
||||
content={(
|
||||
<div>
|
||||
<Text variant="b3">Set a language which you prefer. e.g.: English=EN</Text>
|
||||
<form onSubmit={handleSetLanguage}>
|
||||
<Input name="keywordInput" required value={settings.getTranslationLanguage()} />
|
||||
<Button variant="primary" type="submit" id="set-button">Set</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Translate self messages"
|
||||
options={(
|
||||
<Toggle
|
||||
isActive={settings.enableSelfTranslate}
|
||||
onToggle={() => { toggleSelfTranslate(); updateState({}); }}
|
||||
/>
|
||||
)}
|
||||
content={<Text variant="b3">When you send message, we will automaticly translate to you prefered language</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Self message target language"
|
||||
content={(
|
||||
<div>
|
||||
<Text variant="b3">Set a language which you prefer. e.g.: English=EN</Text>
|
||||
<form onSubmit={handleSetSelfLanguage}>
|
||||
<Input name="keywordInput" required value={settings.getTranslationSelfLanguage()}
|
||||
disabled={!settings.enableSelfTranslate}/>
|
||||
<Button variant="primary" type="submit" id="set-self-button"
|
||||
disabled={!settings.enableSelfTranslate}
|
||||
>Set</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -150,24 +255,29 @@ function NotificationsSection() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-notifications">
|
||||
<MenuHeader>Notification & Sound</MenuHeader>
|
||||
<SettingTile
|
||||
title="Desktop notification"
|
||||
options={renderOptions()}
|
||||
content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Notification Sound"
|
||||
options={(
|
||||
<Toggle
|
||||
isActive={settings.isNotificationSounds}
|
||||
onToggle={() => { toggleNotificationSounds(); updateState({}); }}
|
||||
/>
|
||||
)}
|
||||
content={<Text variant="b3">Play sound when new messages arrive.</Text>}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="settings-notifications">
|
||||
<MenuHeader>Notification & Sound</MenuHeader>
|
||||
<SettingTile
|
||||
title="Desktop notification"
|
||||
options={renderOptions()}
|
||||
content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Notification Sound"
|
||||
options={(
|
||||
<Toggle
|
||||
isActive={settings.isNotificationSounds}
|
||||
onToggle={() => { toggleNotificationSounds(); updateState({}); }}
|
||||
/>
|
||||
)}
|
||||
content={<Text variant="b3">Play sound when new messages arrive.</Text>}
|
||||
/>
|
||||
</div>
|
||||
<GlobalNotification />
|
||||
<KeywordNotification />
|
||||
<IgnoreUserList />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,6 +291,7 @@ function EmojiSection() {
|
||||
}
|
||||
|
||||
function SecuritySection() {
|
||||
const [, updateState] = useState({});
|
||||
return (
|
||||
<div className="settings-security">
|
||||
<div className="settings-security__card">
|
||||
@@ -210,6 +321,33 @@ function SecuritySection() {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-security__card">
|
||||
<MenuHeader>Presence</MenuHeader>
|
||||
<SettingTile
|
||||
title="Send read receipts"
|
||||
options={(
|
||||
<Toggle
|
||||
/** Always allow to switch receipts on. */
|
||||
disabled={!capabilities.privateReadReceipts && settings.sendReadReceipts}
|
||||
isActive={settings.sendReadReceipts}
|
||||
onToggle={() => { toggleReadReceipts(); updateState({}); }}
|
||||
/>
|
||||
)}
|
||||
content={(
|
||||
<>
|
||||
<Text variant="b3">
|
||||
Let other people know what messages you read.
|
||||
</Text>
|
||||
{!capabilities.privateReadReceipts
|
||||
&& (
|
||||
<Text variant="b3">
|
||||
Making your read receipts private requires a compatible homeserver.
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -223,14 +361,16 @@ function AboutSection() {
|
||||
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
|
||||
<div>
|
||||
<Text variant="h2" weight="medium">
|
||||
Cinny
|
||||
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
|
||||
Cinny Mod
|
||||
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}.5114514`}</span>
|
||||
</Text>
|
||||
<Text>Yet another matrix client</Text>
|
||||
<Text>Modded By WHK(a.k.a CroiX) with magic & love</Text>
|
||||
|
||||
<div className="settings-about__btns">
|
||||
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
|
||||
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
|
||||
<Button onClick={() => initMatrix.clearCacheAndReload()} variant="danger">Clear cache & reload</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,24 +452,84 @@ function useWindowToggle(setSelectedTab) {
|
||||
return [isOpen, requestClose];
|
||||
}
|
||||
|
||||
async function getCapabilities() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
capabilities = {
|
||||
privateReadReceipts: (await (Promise.all([
|
||||
mx.doesServerSupportUnstableFeature('org.matrix.msc2285.stable'),
|
||||
mx.isVersionSupported('v1.4')])
|
||||
)).some((res) => res === true),
|
||||
};
|
||||
}
|
||||
|
||||
function Settings() {
|
||||
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
|
||||
const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
|
||||
useEffect(getCapabilities, []);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleTabChange = (tabItem) => setSelectedTab(tabItem);
|
||||
const handleLogout = async () => {
|
||||
if (await confirmDialog('Logout', 'Are you sure that you want to logout your session?', 'Logout', 'danger')) {
|
||||
logout();
|
||||
initMatrix.logout();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchUsers = async () => {
|
||||
const loggedInUsers = JSON.parse(window.localStorage.getItem("loggedInUsers"));
|
||||
|
||||
const renderUserSwitcher = () => (
|
||||
<div>
|
||||
{
|
||||
loggedInUsers.map((userId) => {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const user = mx.getUser(userId);
|
||||
const avatarUrl = user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null;
|
||||
return (
|
||||
<PeopleSelector
|
||||
key={userId}
|
||||
onClick={() => {
|
||||
if (userId !== window.localStorage.getItem("currentUser")) {
|
||||
window.localStorage.setItem("currentUser", userId);
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
name={user.displayName}
|
||||
avatarSrc={avatarUrl}
|
||||
color={colorMXID(userId)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
<Button
|
||||
variant="caution"
|
||||
iconSrc={AddUserIC}
|
||||
onClick={() => {
|
||||
window.localStorage.removeItem("currentUser");
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Add user
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
await openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Switch users</Text>,
|
||||
renderUserSwitcher,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
isOpen={isOpen}
|
||||
className="settings-window"
|
||||
title={<Text variant="s1" weight="medium" primary>Settings</Text>}
|
||||
title={<Text variant="s1" weight="medium" primary>{t("Settings")}</Text>}
|
||||
contentOptions={(
|
||||
<>
|
||||
<Button variant="caution" iconSrc={AddUserIC} onClick={handleSwitchUsers}>
|
||||
Switch users
|
||||
</Button>
|
||||
<Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
|
||||
Logout
|
||||
</Button>
|
||||
|
||||
@@ -24,6 +24,17 @@
|
||||
padding: 0 var(--sp-normal);
|
||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||
}
|
||||
|
||||
& form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-tight);
|
||||
margin: var(--sp-extra-tight) 0 var(--sp-normal);
|
||||
.input-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.settings-window__card {
|
||||
margin: var(--sp-normal) 0;
|
||||
@@ -38,6 +49,9 @@
|
||||
}
|
||||
.settings-appearance__card,
|
||||
.settings-notifications,
|
||||
.global-notification,
|
||||
.keyword-notification,
|
||||
.ignore-user-list,
|
||||
.settings-security__card,
|
||||
.settings-security .device-manage,
|
||||
.settings-about__card,
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './TextTranslate.scss';
|
||||
import { generateRequestUrl, normaliseResponse } from 'google-translate-api-browser';
|
||||
import settings from '../../../client/state/settings';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import PopupWindow from '../../molecules/popup-window/PopupWindow';
|
||||
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
|
||||
function BeforeTextTranslateBlock({ title, json }) {
|
||||
return (
|
||||
<div className="view-source__card">
|
||||
<MenuHeader>{title}</MenuHeader>
|
||||
<ScrollView horizontal vertical={false} autoHide>
|
||||
<pre className="text text-b1">
|
||||
<code className="language-json">{json}</code>
|
||||
</pre>
|
||||
</ScrollView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BeforeTextTranslateBlock.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
json: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
function AfterTextTranslateBlock({ title, json }) {
|
||||
return (
|
||||
<div className="view-source__card">
|
||||
<MenuHeader>{title}</MenuHeader>
|
||||
<ScrollView horizontal vertical={false} autoHide>
|
||||
<pre className="text text-b1">
|
||||
<code className="language-json" id="translatedText">
|
||||
{json}
|
||||
</code>
|
||||
</pre>
|
||||
</ScrollView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
AfterTextTranslateBlock.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
json: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
function TextTranslate() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [event, setEvent] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTextTranslate = (e) => {
|
||||
setEvent(e);
|
||||
setIsOpen(true);
|
||||
};
|
||||
navigation.on(cons.events.navigation.TEXTTRANSLATE_OPENED, loadTextTranslate);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.TEXTTRANSLATE_OPENED, loadTextTranslate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAfterClose = () => {
|
||||
setEvent(null);
|
||||
};
|
||||
|
||||
async function translate(translateUrl) {
|
||||
const response = await fetch(`${translateUrl}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
if(settings.getTranslationAPIIndex() === 1){
|
||||
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < body.translations.length; i += 1) {
|
||||
result += body.translations[i].text;
|
||||
}
|
||||
|
||||
|
||||
if(result === undefined){
|
||||
result = body.message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return normaliseResponse(body, settings.getTranslationLanguage());
|
||||
}
|
||||
|
||||
const translateText = (text) => {
|
||||
|
||||
const origin = text.replace(/> <.*> .*\n/gms, "").trim();
|
||||
|
||||
let url = generateRequestUrl(origin, { to: settings.getTranslationLanguage() }).replace(
|
||||
'https://translate.google.com/',
|
||||
'https://panel.icarus.today/'
|
||||
);
|
||||
|
||||
if(settings.getTranslationAPIIndex() === 1) {
|
||||
url = `https://rwwhwqaduuvdbswpmq6p.icarus.today/&text=${origin}&target_lang=${settings.getTranslationLanguage()}`;
|
||||
}
|
||||
|
||||
translate(url).then((result) => {
|
||||
|
||||
if(settings.getTranslationAPIIndex() === 0) document.getElementById('translatedText').innerHTML = result.text;
|
||||
else document.getElementById('translatedText').innerHTML = result;
|
||||
return "Setting Error!"
|
||||
});
|
||||
|
||||
return 'Translating...';
|
||||
};
|
||||
|
||||
const renderTextTranslate = () => (
|
||||
<div className="text-translate">
|
||||
<BeforeTextTranslateBlock
|
||||
title="Original text"
|
||||
json={event.getEffectiveEvent().content.body}
|
||||
/>
|
||||
<AfterTextTranslateBlock
|
||||
title={settings.getTranslationAPIIndex() === 1 ? "DeepL Translated Text" : "Google Translated Text"}
|
||||
json={translateText(event.getEffectiveEvent().content.body)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
isOpen={isOpen}
|
||||
title="Translate"
|
||||
onAfterClose={handleAfterClose}
|
||||
onRequestClose={() => setIsOpen(false)}
|
||||
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
|
||||
>
|
||||
{event ? renderTextTranslate() : <div />}
|
||||
</PopupWindow>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextTranslate;
|
||||