Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0f6f65045d | ||
|
|
843b9ddffd | ||
|
|
e283d4cd21 | ||
|
|
4fe02d27c8 | ||
|
|
5a269c68ae | ||
|
|
570a1c85af | ||
|
|
f7ff1ef2bd | ||
|
|
04530a801c | ||
|
|
756cd0921b | ||
|
|
292eee33b9 | ||
|
|
583c834ba8 | ||
|
|
33949dbdb1 | ||
|
|
fd79ea4b9b | ||
|
|
b3bff6b43f | ||
|
|
80aa55b706 | ||
|
|
76c16ce294 | ||
|
|
715e6648ae | ||
|
|
984fad811c | ||
|
|
eafd2c8a13 | ||
|
|
82fb243ffe | ||
|
|
6cbf1b2eac | ||
|
|
043e411a98 | ||
|
|
f93b666bbf | ||
|
|
7e28aa1474 | ||
|
|
1deef51df0 | ||
|
|
6f7934badc | ||
|
|
2f2680be3c | ||
|
|
4cd8f4a94c | ||
|
|
a417980a81 | ||
|
|
af69955801 | ||
|
|
214d49f1d9 | ||
|
|
c4e36a1f97 | ||
|
|
5de6a1bea6 | ||
|
|
74a20a0e14 | ||
|
|
820d08017a | ||
|
|
258afec391 | ||
|
|
0cf5aac591 | ||
|
|
59fd34a4b4 | ||
|
|
1da3d252e8 | ||
|
|
3c1cc59d59 | ||
|
|
1692098d5d | ||
|
|
fbab53af22 | ||
|
|
ce1e263d57 | ||
|
|
9f99320fda | ||
|
|
20e1df43d0 | ||
|
|
728c5434bb | ||
|
|
542ac4f4e1 | ||
|
|
d23fc228d7 | ||
|
|
4ff3e47d54 | ||
|
|
96b22eb557 | ||
|
|
9187107751 | ||
|
|
c6812b5b11 | ||
|
|
adb584623e | ||
|
|
120e8de9d1 | ||
|
|
21726b63f8 | ||
|
|
04f910ee03 | ||
|
|
edace32213 | ||
|
|
5e527e434a | ||
|
|
1d90f7588b | ||
|
|
f8b8a35152 | ||
|
|
fa4c95a9b6 | ||
|
|
a478fc4805 | ||
|
|
febb28e9c4 | ||
|
|
c78a39af50 | ||
|
|
21e6049c16 | ||
|
|
6e418337cc | ||
|
|
48f34053ab | ||
|
|
abfe263750 | ||
|
|
9ba003b16d | ||
|
|
48793f3a95 | ||
|
|
d8cf98fd64 | ||
|
|
78e12d5bee | ||
|
|
bdb8bdf76c | ||
|
|
88b79eb3a5 | ||
|
|
b6428197ac | ||
|
|
a46138c8b9 | ||
|
|
1211ca277b | ||
|
|
e6f395c643 | ||
|
|
1979646b4b | ||
|
|
5c0eb20cb4 | ||
|
|
009966a5c7 | ||
|
|
4427b3b291 | ||
|
|
3dda4d6540 | ||
|
|
c9df0be874 | ||
|
|
ca2627d3cf | ||
|
|
47e6527b0e | ||
|
|
7decbb6eef | ||
|
|
68da1d0551 | ||
|
|
a6f21b6606 | ||
|
|
06a4e0c93b | ||
|
|
0ca1df24ed | ||
|
|
1d12a906d4 | ||
|
|
7bd7518963 | ||
|
|
a9c5765be5 | ||
|
|
2292f63fb6 | ||
|
|
db92b9f5ff | ||
|
|
f538639882 | ||
|
|
56bc8c2890 | ||
|
|
1cba4d3fa7 | ||
|
|
4c7820ceac | ||
|
|
118dcd8fa0 | ||
|
|
a142ade923 | ||
|
|
57ab10a87c | ||
|
|
8c1c3cd634 | ||
|
|
2d3634d6bf | ||
|
|
217f29f068 | ||
|
|
58c3eee153 | ||
|
|
d9e1fb620b | ||
|
|
a3f5b92484 | ||
|
|
8b96e0ab98 | ||
|
|
eef2d451b7 | ||
|
|
371e66a6df | ||
|
|
0d12144744 | ||
|
|
ba39724813 | ||
|
|
af6e6bfc67 | ||
|
|
315b5a1048 | ||
|
|
c410d4e9f5 | ||
|
|
299d976622 | ||
|
|
e8587f99c9 | ||
|
|
63ab96b71b | ||
|
|
e998438135 | ||
|
|
5fd7d64d21 |
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",
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
|
||||
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
||||
|
||||
### Description
|
||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
@@ -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,29 +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
|
||||
- name: Build app
|
||||
run: npm ci && npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
name: previewbuild
|
||||
node-version: 18.12.1
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3.1.1
|
||||
with:
|
||||
name: preview
|
||||
path: dist
|
||||
retention-days: 1
|
||||
- name: Get PR info
|
||||
uses: actions/github-script@v6.1.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.1.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.1.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.0.0
|
||||
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,19 +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
|
||||
- name: Build and deploy to Netlify
|
||||
uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
install_command: "npm ci"
|
||||
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: "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,23 +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
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
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
|
||||
@@ -31,54 +50,45 @@ 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'
|
||||
publish-image:
|
||||
name: Push Docker image to Docker Hub, ghcr
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Build and deploy to Netlify
|
||||
uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
|
||||
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:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
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@v1.2.0
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.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.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: ajbura/cinny
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||
ghcr.io/${{ github.repository }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
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"
|
||||
}
|
||||
@@ -36,4 +36,9 @@ It is not always possible to phrase every change in such a manner, but it is des
|
||||
|
||||
Also, we use [ESLint](https://eslint.org/) for clean and stylistically consistent code syntax, so make sure your pull request follow it.
|
||||
|
||||
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
||||
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
||||
|
||||
## Helpful links
|
||||
- [BEM methodology](http://getbem.com/introduction/)
|
||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||
@@ -1,16 +1,16 @@
|
||||
## Builder
|
||||
FROM node:17.9.0-alpine3.15 as builder
|
||||
FROM node:18.12.1-alpine3.15 as builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY package.json package-lock.json /src/
|
||||
COPY .npmrc package.json package-lock.json /src/
|
||||
RUN npm ci
|
||||
COPY . /src/
|
||||
RUN npm run build
|
||||
|
||||
|
||||
## App
|
||||
FROM nginx:1.21.6-alpine
|
||||
FROM nginx:1.23.3-alpine
|
||||
|
||||
COPY --from=builder /src/dist /app
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Ajay Bura (ajbura)
|
||||
Copyright (c) 2021-present Ajay Bura (ajbura)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -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.
|
||||
@@ -1,9 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/ajbura/cinny/dev/public/res/svg/cinny.svg?sanitize=true"
|
||||
height="16">
|
||||
<span><b>Cinny</b></span>
|
||||
</p>
|
||||
<p align="center">
|
||||
# Cinny
|
||||
<p>
|
||||
<a href="https://github.com/ajbura/cinny/releases">
|
||||
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
|
||||
<a href="https://hub.docker.com/r/ajbura/cinny">
|
||||
@@ -16,21 +12,30 @@
|
||||
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
|
||||
</p>
|
||||
|
||||
**Cinny** is a Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have a client that is easy on end user
|
||||
and feels a modern chat application.
|
||||
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
|
||||
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
## Building and Running
|
||||
## Getting started
|
||||
Web app is available at https://app.cinny.in and gets updated on each new release. The `dev` branch is continuously deployed at https://dev.cinny.in but keep in mind that it could have things broken.
|
||||
|
||||
### Running pre-compiled
|
||||
You can also download our desktop app from [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||
|
||||
A tarball of pre-compiled version of the app is provided with each [release](https://github.com/ajbura/cinny/releases).
|
||||
You can serve the application with a webserver of your choosing by simply copying `dist/` directory to the webroot.
|
||||
To host Cinny on your own, download tarball of the app from [GitHub release](https://github.com/cinnyapp/cinny/releases/latest).
|
||||
You can serve the application with a webserver of your choice by simply copying `dist/` directory to the webroot.
|
||||
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
|
||||
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
|
||||
```
|
||||
docker pull ajbura/cinny
|
||||
```
|
||||
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
|
||||
```
|
||||
docker pull ghcr.io/cinnyapp/cinny:latest
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>PGP Public Key to verify pre-compiled tarball</summary>
|
||||
<summary>PGP Public Key to verify tarball</summary>
|
||||
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
@@ -77,48 +82,39 @@ UeGsouhyuITLwEhScounZDqop+Dx
|
||||
```
|
||||
</details>
|
||||
|
||||
### Building from source
|
||||
## 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.
|
||||
|
||||
Execute the following commands to compile the app from its source code:
|
||||
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
|
||||
npm ci # Installs all dependencies
|
||||
npm start # Serve a development version
|
||||
```
|
||||
|
||||
To build the app:
|
||||
```sh
|
||||
npm run build # Compiles the app into the dist/ directory
|
||||
```
|
||||
|
||||
You can then copy the files to a webserver's webroot of your choice.
|
||||
|
||||
To serve a development version of the app locally for testing, you need to use the command `npm start`.
|
||||
|
||||
### Running with Docker
|
||||
|
||||
This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
|
||||
use this locally, you can build the container like so:
|
||||
|
||||
```
|
||||
docker build -t cinny:latest .
|
||||
```
|
||||
|
||||
You can then run the container you've built with a command similar to this:
|
||||
|
||||
```
|
||||
docker run -p 8080:80 cinny:latest
|
||||
```
|
||||
|
||||
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by
|
||||
navigating to `http://localhost:8080`.
|
||||
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
|
||||
|
||||
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by `docker pull ajbura/cinny`.
|
||||
|
||||
### Configuring default Homeserver
|
||||
|
||||
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2021 Ajay Bura (ajbura)
|
||||
Copyright (c) 2021-present Ajay Bura (ajbura)
|
||||
|
||||
Code licensed under the MIT License: <http://opensource.org/licenses/MIT>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"defaultHomeserver": 4,
|
||||
"defaultHomeserver": 3,
|
||||
"homeserverList": [
|
||||
"converser.eu",
|
||||
"envs.net",
|
||||
"halogen.city",
|
||||
"kde.org",
|
||||
"matrix.icarus.today",
|
||||
"matrix.org",
|
||||
"chat.mozilla.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,87 +1,83 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "2.0.4",
|
||||
"version": "2.2.4",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"npm": ">=6.14.8 <=8.5.5",
|
||||
"node": ">=14.15.0 <=17.9.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.10",
|
||||
"@fontsource/roboto": "^4.5.7",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"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": "^1.4.12",
|
||||
"katex": "^0.15.6",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"matrix-js-sdk": "^17.2.0",
|
||||
"micromark": "^3.0.10",
|
||||
"micromark-extension-gfm": "^2.0.1",
|
||||
"micromark-extension-math": "^2.0.2",
|
||||
"micromark-util-chunked": "^1.0.0",
|
||||
"micromark-util-resolve-all": "^1.0.0",
|
||||
"micromark-util-symbol": "^1.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-autosize-textarea": "^7.1.0",
|
||||
"react-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.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"twemoji": "^14.0.2"
|
||||
"@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": "22.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.0",
|
||||
"@babel/preset-env": "^7.18.0",
|
||||
"@babel/preset-react": "^7.17.12",
|
||||
"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.16.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.30.0",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-loader": "^3.1.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sass": "^1.52.1",
|
||||
"sass-loader": "^13.0.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.72.1",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.9.0",
|
||||
"webpack-merge": "^5.7.3"
|
||||
"@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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Graphics (c) by Ajay Bura (ajbura)
|
||||
Copyright (c) 2021-present Ajay Bura (ajbura)
|
||||
|
||||
Graphic content is licensed under a
|
||||
Creative Commons Attribution 4.0 International License.
|
||||
|
||||
|
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 |
@@ -1,12 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<g>
|
||||
<polygon points="14,3 14,5 17.8,5 12.9,9.9 14.3,11.3 19,6.6 19,10.2 21,10.2 21,3 "/>
|
||||
<path d="M3,10.2h2V5h5V3H5C3.9,3,3,3.9,3,5V10.2z"/>
|
||||
<path d="M5,14.2H3V19c0,1.1,0.9,2,2,2h5v-2H5V14.2z"/>
|
||||
<path d="M19,19h-5v2h5c1.1,0,2-0.9,2-2v-4.8h-2V19z"/>
|
||||
</g>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 3V5H17.8L12.9 9.9L14.3 11.3L19 6.6V10.2H21V3H14Z" fill="black"/>
|
||||
<path d="M5 5H10V3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V14.2H19V19H5V5Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 297 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.92896 3.51471L3.51474 4.92892L5.97515 7.38933C4.46742 8.5776 3.32116 9.93994 2.7 10.8C2.1 11.5 2.1 12.5 2.7 13.2C4 15 7.6 19 12 19C13.5709 19 15.0398 18.4902 16.3384 17.7526L19.0711 20.4853L20.4853 19.0711L4.92896 3.51471ZM4.2 12C4.68291 11.3561 5.85678 9.9637 7.39721 8.81139L9.29238 10.7066C9.10496 11.0982 9 11.5368 9 12C9 13.6569 10.3431 15 12 15C12.4632 15 12.9018 14.895 13.2934 14.7076L14.8573 16.2715C13.9566 16.7128 12.9896 17 12 17C8.4 17 5.1 13.2 4.2 12Z" fill="black"/>
|
||||
<path d="M9.6226 5.37995L11.2906 7.04797C11.5254 7.01661 11.762 7 12 7C15.6 7 18.9 10.8 19.8 12C19.493 12.4094 18.9066 13.1213 18.1244 13.8817L19.5194 15.2768C20.2973 14.4974 20.9049 13.7471 21.3 13.2C21.9 12.5 21.9 11.5 21.3 10.8C20 9 16.4 5 12 5C11.1762 5 10.3805 5.14021 9.6226 5.37995Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 943 B |
@@ -1,13 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M12,19c-4.4,0-8-4-9.3-5.8c-0.6-0.7-0.6-1.7,0-2.4C4,9,7.6,5,12,5s8,4,9.3,5.8c0.6,0.7,0.6,1.7,0,2.4C20,15,16.4,19,12,19
|
||||
z M12,7c-3.6,0-6.9,3.8-7.8,5c0.9,1.2,4.2,5,7.8,5s6.9-3.8,7.8-5C18.9,10.8,15.6,7,12,7z"/>
|
||||
</g>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</g>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 19C7.6 19 4 15 2.7 13.2C2.1 12.5 2.1 11.5 2.7 10.8C4 9 7.6 5 12 5C16.4 5 20 9 21.3 10.8C21.9 11.5 21.9 12.5 21.3 13.2C20 15 16.4 19 12 19ZM12 7C8.4 7 5.1 10.8 4.2 12C5.1 13.2 8.4 17 12 17C15.6 17 18.9 13.2 19.8 12C18.9 10.8 15.6 7 12 7Z" fill="black"/>
|
||||
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 718 B After Width: | Height: | Size: 508 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 3L21 8V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V5C3 3.89543 3.89543 3 5 3H16ZM19 9H17C15.8954 9 15 8.10457 15 7V5H5V19H19V9Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12H17C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12H9Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 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({
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
&--icon {
|
||||
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
||||
|
||||
.ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ function Input({
|
||||
{ resizable
|
||||
? (
|
||||
<TextareaAutosize
|
||||
dir="auto"
|
||||
style={{ minHeight: `${minHeight}px` }}
|
||||
name={name}
|
||||
id={id}
|
||||
@@ -34,6 +35,7 @@ function Input({
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
dir="auto"
|
||||
ref={forwardRef}
|
||||
id={id}
|
||||
name={name}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Math.scss';
|
||||
|
||||
import katex from 'katex';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import 'katex/dist/contrib/copy-tex';
|
||||
import 'katex/dist/contrib/copy-tex.css';
|
||||
|
||||
const Math = React.memo(({
|
||||
content, throwOnError, errorColor, displayMode,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.katex-display {
|
||||
margin: 0 !important;
|
||||
}
|
||||
@@ -42,7 +42,6 @@ function RawModal({
|
||||
shouldCloseOnEsc={closeFromOutside}
|
||||
shouldCloseOnOverlayClick={closeFromOutside}
|
||||
shouldReturnFocusAfterClose={false}
|
||||
closeTimeoutMS={300}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
|
||||
@@ -1,33 +1,9 @@
|
||||
.ReactModal__Overlay {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms var(--fluid-slide-up);
|
||||
}
|
||||
.ReactModal__Overlay--after-open{
|
||||
opacity: 1;
|
||||
}
|
||||
.ReactModal__Overlay--before-close{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ReactModal__Content {
|
||||
transform: translateY(100%);
|
||||
transition: transform 200ms var(--fluid-slide-up);
|
||||
}
|
||||
|
||||
.ReactModal__Content--after-open{
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ReactModal__Content--before-close{
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.raw-modal {
|
||||
--small-modal-width: 525px;
|
||||
--medium-modal-width: 712px;
|
||||
--large-modal-width: 1024px;
|
||||
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
@@ -60,4 +36,31 @@
|
||||
height: 100%;
|
||||
background-color: var(--bg-overlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ReactModal__Overlay {
|
||||
animation: raw-modal--overlay 150ms;
|
||||
}
|
||||
|
||||
.ReactModal__Content {
|
||||
animation: raw-modal--content 150ms;
|
||||
}
|
||||
|
||||
@keyframes raw-modal--content {
|
||||
0% {
|
||||
transform: translateY(100px);
|
||||
opacity: .5;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes raw-modal--overlay {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -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,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import dateFormat from 'dateformat';
|
||||
import { isInSameDay } from '../../../util/common';
|
||||
|
||||
function Time({ timestamp, fullTime }) {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
||||
let formattedDate = formattedFullTime;
|
||||
|
||||
if (!fullTime) {
|
||||
const compareDate = new Date();
|
||||
const isToday = isInSameDay(date, compareDate);
|
||||
compareDate.setDate(compareDate.getDate() - 1);
|
||||
const isYesterday = isInSameDay(date, compareDate);
|
||||
|
||||
formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
|
||||
if (isYesterday) {
|
||||
formattedDate = `Yesterday, ${formattedDate}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<time
|
||||
dateTime={date.toISOString()}
|
||||
title={formattedFullTime}
|
||||
>
|
||||
{formattedDate}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
|
||||
Time.defaultProps = {
|
||||
fullTime: false,
|
||||
};
|
||||
|
||||
Time.propTypes = {
|
||||
timestamp: PropTypes.number.isRequired,
|
||||
fullTime: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Time;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImageLightbox.scss';
|
||||
import FileSaver from 'file-saver';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawModal from '../../atoms/modal/RawModal';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
|
||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
||||
|
||||
function ImageLightbox({
|
||||
url, alt, isOpen, onRequestClose,
|
||||
}) {
|
||||
const handleDownload = () => {
|
||||
FileSaver.saveAs(url, alt);
|
||||
};
|
||||
|
||||
return (
|
||||
<RawModal
|
||||
className="image-lightbox__modal"
|
||||
overlayClassName="image-lightbox__overlay"
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
size="large"
|
||||
>
|
||||
<div className="image-lightbox__header">
|
||||
<Text variant="b2" weight="medium">{alt}</Text>
|
||||
<IconButton onClick={() => window.open(url)} size="small" src={ExternalSVG} />
|
||||
<IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
|
||||
</div>
|
||||
<div className="image-lightbox__content">
|
||||
<img src={url} alt={alt} />
|
||||
</div>
|
||||
</RawModal>
|
||||
);
|
||||
}
|
||||
|
||||
ImageLightbox.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onRequestClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImageLightbox;
|
||||
@@ -0,0 +1,50 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/text';
|
||||
|
||||
.image-lightbox__modal {
|
||||
box-shadow: none;
|
||||
width: unset;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
border-radius: 0;
|
||||
pointer-events: none;
|
||||
|
||||
& .text {
|
||||
color: white;
|
||||
}
|
||||
& .ic-raw {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.image-lightbox__overlay {
|
||||
background-color: var(--bg-overlay-low);
|
||||
}
|
||||
|
||||
|
||||
.image-lightbox__header > *,
|
||||
.image-lightbox__content > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
.image-lightbox__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > .text {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-txt__ellipsis;
|
||||
}
|
||||
}
|
||||
.image-lightbox__content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-height: 80vh;
|
||||
|
||||
& img {
|
||||
background-color: var(--bg-surface-low);
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
import React, {
|
||||
useState, useMemo, useReducer, useEffect,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePack.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openReusableDialog } from '../../../client/action/navigation';
|
||||
import { suffixRename } from '../../../util/common';
|
||||
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Checkbox from '../../atoms/button/Checkbox';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import ImagePackProfile from './ImagePackProfile';
|
||||
import ImagePackItem from './ImagePackItem';
|
||||
import ImagePackUpload from './ImagePackUpload';
|
||||
|
||||
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Rename</Text>,
|
||||
(requestClose) => (
|
||||
<div style={{ padding: 'var(--sp-normal)' }}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const sc = e.target.shortcode.value;
|
||||
if (sc.trim() === '') return;
|
||||
isCompleted = true;
|
||||
resolve(sc.trim());
|
||||
requestClose();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={shortcode}
|
||||
name="shortcode"
|
||||
label="Shortcode"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<div style={{ height: 'var(--sp-normal)' }} />
|
||||
<Button variant="primary" type="submit">Rename</Button>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
() => {
|
||||
if (!isCompleted) resolve(null);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function getUsage(usage) {
|
||||
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
||||
if (usage.includes('emoticon')) return 'emoticon';
|
||||
if (usage.includes('sticker')) return 'sticker';
|
||||
|
||||
return 'both';
|
||||
}
|
||||
|
||||
function isGlobalPack(roomId, stateKey) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||
if (typeof globalContent !== 'object') return false;
|
||||
|
||||
const { rooms } = globalContent;
|
||||
if (typeof rooms !== 'object') return false;
|
||||
|
||||
return rooms[roomId]?.[stateKey] !== undefined;
|
||||
}
|
||||
|
||||
function useRoomImagePack(roomId, stateKey) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = useMemo(() => (
|
||||
ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent())
|
||||
), [room, stateKey]);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
||||
};
|
||||
|
||||
return {
|
||||
pack,
|
||||
sendPackContent,
|
||||
};
|
||||
}
|
||||
|
||||
function useUserImagePack() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
||||
const pack = useMemo(() => (
|
||||
ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? {
|
||||
pack: { display_name: 'Personal' },
|
||||
images: {},
|
||||
})
|
||||
), []);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.setAccountData('im.ponies.user_emotes', content);
|
||||
};
|
||||
|
||||
return {
|
||||
pack,
|
||||
sendPackContent,
|
||||
};
|
||||
}
|
||||
|
||||
function useImagePackHandles(pack, sendPackContent) {
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
|
||||
const getNewKey = (key) => {
|
||||
if (typeof key !== 'string') return undefined;
|
||||
let newKey = key?.replace(/\s/g, '_');
|
||||
if (pack.getImages().get(newKey)) {
|
||||
newKey = suffixRename(
|
||||
newKey,
|
||||
(suffixedKey) => pack.getImages().get(suffixedKey),
|
||||
);
|
||||
}
|
||||
return newKey;
|
||||
};
|
||||
|
||||
const handleAvatarChange = (url) => {
|
||||
pack.setAvatarUrl(url);
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleEditProfile = (name, attribution) => {
|
||||
pack.setDisplayName(name);
|
||||
pack.setAttribution(attribution);
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleUsageChange = (newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
pack.setUsage(usage);
|
||||
pack.getImages().forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
const handleRenameItem = async (key) => {
|
||||
const newKey = getNewKey(await renameImagePackItem(key));
|
||||
|
||||
if (!newKey || newKey === key) return;
|
||||
pack.updateImageKey(key, newKey);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleDeleteItem = async (key) => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete',
|
||||
`Are you sure that you want to delete "${key}"?`,
|
||||
'Delete',
|
||||
'danger',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
pack.removeImage(key);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleUsageItem = (key, newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
pack.setImageUsage(key, usage);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
const handleAddItem = (key, url) => {
|
||||
const newKey = getNewKey(key);
|
||||
if (!newKey || !url) return;
|
||||
|
||||
pack.addImage(newKey, {
|
||||
url,
|
||||
});
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
return {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
};
|
||||
}
|
||||
|
||||
function addGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
if (!content.rooms) content.rooms = {};
|
||||
if (!content.rooms[roomId]) content.rooms[roomId] = {};
|
||||
content.rooms[roomId][stateKey] = {};
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
}
|
||||
function removeGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
if (!content.rooms) return Promise.resolve();
|
||||
if (!content.rooms[roomId]) return Promise.resolve();
|
||||
delete content.rooms[roomId][stateKey];
|
||||
if (Object.keys(content.rooms[roomId]).length === 0) {
|
||||
delete content.rooms[roomId];
|
||||
}
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
}
|
||||
|
||||
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
const [isGlobal, setIsGlobal] = useState(isGlobalPack(roomId, stateKey));
|
||||
|
||||
const { pack, sendPackContent } = useRoomImagePack(roomId, stateKey);
|
||||
|
||||
const {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
} = useImagePackHandles(pack, sendPackContent);
|
||||
|
||||
const handleGlobalChange = (isG) => {
|
||||
setIsGlobal(isG);
|
||||
if (isG) addGlobalImagePack(mx, roomId, stateKey);
|
||||
else removeGlobalImagePack(mx, roomId, stateKey);
|
||||
};
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
|
||||
const handleDeletePack = async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete Pack',
|
||||
`Are you sure that you want to delete "${pack.displayName}"?`,
|
||||
'Delete',
|
||||
'danger',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
handlePackDelete(stateKey);
|
||||
};
|
||||
|
||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
||||
|
||||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||
displayName={pack.displayName ?? 'Unknown'}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={canChange ? handleUsageChange : null}
|
||||
onAvatarChange={canChange ? handleAvatarChange : null}
|
||||
onEditProfile={canChange ? handleEditProfile : null}
|
||||
/>
|
||||
{ canChange && (
|
||||
<ImagePackUpload onUpload={handleAddItem} />
|
||||
)}
|
||||
{ images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
<Text variant="b3">Shortcode</Text>
|
||||
<Text variant="b3">Usage</Text>
|
||||
</div>
|
||||
{images.map(([shortcode, image]) => (
|
||||
<ImagePackItem
|
||||
key={shortcode}
|
||||
url={mx.mxcUrlToHttp(image.mxc)}
|
||||
shortcode={shortcode}
|
||||
usage={getUsage(image.usage)}
|
||||
onUsageChange={canChange ? handleUsageItem : undefined}
|
||||
onDelete={canChange ? handleDeleteItem : undefined}
|
||||
onRename={canChange ? handleRenameItem : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(pack.images.size > 2 || handlePackDelete) && (
|
||||
<div className="image-pack__footer">
|
||||
{pack.images.size > 2 && (
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{
|
||||
viewMore
|
||||
? 'View less'
|
||||
: `View ${pack.images.size - 2} more`
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
{ handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
|
||||
</div>
|
||||
)}
|
||||
<div className="image-pack__global">
|
||||
<Checkbox variant="positive" onToggle={handleGlobalChange} isActive={isGlobal} />
|
||||
<div>
|
||||
<Text variant="b2">Use globally</Text>
|
||||
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePack.defaultProps = {
|
||||
handlePackDelete: null,
|
||||
};
|
||||
ImagePack.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
stateKey: PropTypes.string.isRequired,
|
||||
handlePackDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
function ImagePackUser() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const [viewMore, setViewMore] = useState(false);
|
||||
|
||||
const { pack, sendPackContent } = useUserImagePack();
|
||||
|
||||
const {
|
||||
handleAvatarChange,
|
||||
handleEditProfile,
|
||||
handleUsageChange,
|
||||
handleRenameItem,
|
||||
handleDeleteItem,
|
||||
handleUsageItem,
|
||||
handleAddItem,
|
||||
} = useImagePackHandles(pack, sendPackContent);
|
||||
|
||||
const images = [...pack.images].slice(0, viewMore ? pack.images.size : 2);
|
||||
|
||||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||
displayName={pack.displayName ?? 'Personal'}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={handleUsageChange}
|
||||
onAvatarChange={handleAvatarChange}
|
||||
onEditProfile={handleEditProfile}
|
||||
/>
|
||||
<ImagePackUpload onUpload={handleAddItem} />
|
||||
{ images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
<Text variant="b3">Shortcode</Text>
|
||||
<Text variant="b3">Usage</Text>
|
||||
</div>
|
||||
{images.map(([shortcode, image]) => (
|
||||
<ImagePackItem
|
||||
key={shortcode}
|
||||
url={mx.mxcUrlToHttp(image.mxc)}
|
||||
shortcode={shortcode}
|
||||
usage={getUsage(image.usage)}
|
||||
onUsageChange={handleUsageItem}
|
||||
onDelete={handleDeleteItem}
|
||||
onRename={handleRenameItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(pack.images.size > 2) && (
|
||||
<div className="image-pack__footer">
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{
|
||||
viewMore
|
||||
? 'View less'
|
||||
: `View ${pack.images.size - 2} more`
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useGlobalImagePack() {
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
const roomIdToStateKeys = new Map();
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
||||
const { rooms } = globalContent;
|
||||
|
||||
Object.keys(rooms).forEach((roomId) => {
|
||||
if (typeof rooms[roomId] !== 'object') return;
|
||||
const room = mx.getRoom(roomId);
|
||||
const stateKeys = Object.keys(rooms[roomId]);
|
||||
if (!room || stateKeys.length === 0) return;
|
||||
roomIdToStateKeys.set(roomId, stateKeys);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleEvent = (event) => {
|
||||
if (event.getType() === 'im.ponies.emote_rooms') forceUpdate();
|
||||
};
|
||||
mx.addListener('accountData', handleEvent);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return roomIdToStateKeys;
|
||||
}
|
||||
|
||||
function ImagePackGlobal() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const roomIdToStateKeys = useGlobalImagePack();
|
||||
|
||||
const handleChange = (roomId, stateKey) => {
|
||||
removeGlobalImagePack(mx, roomId, stateKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-global">
|
||||
<MenuHeader>Global packs</MenuHeader>
|
||||
<div>
|
||||
{
|
||||
roomIdToStateKeys.size > 0
|
||||
? [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
return (
|
||||
stateKeys.map((stateKey) => {
|
||||
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
|
||||
if (!pack) return null;
|
||||
return (
|
||||
<div className="image-pack__global" key={pack.id}>
|
||||
<Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive />
|
||||
<div>
|
||||
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
|
||||
<Text variant="b3">{room.name}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
: <div className="image-pack-global__empty"><Text>No global packs</Text></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImagePack;
|
||||
|
||||
export { ImagePackUser, ImagePackGlobal };
|
||||
@@ -0,0 +1,47 @@
|
||||
@use '../../partials/flex';
|
||||
|
||||
.image-pack {
|
||||
&-item {
|
||||
border-top: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
& > *:nth-child(2) {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: var(--sp-normal);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
|
||||
&__global {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: var(--sp-tight);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.image-pack-global {
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
||||
}
|
||||
& .image-pack__global {
|
||||
padding: 0 var(--sp-normal);
|
||||
padding-bottom: var(--sp-normal);
|
||||
&:first-child {
|
||||
padding-top: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackItem.scss';
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
|
||||
function ImagePackItem({
|
||||
url, shortcode, usage, onUsageChange, onDelete, onRename,
|
||||
}) {
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
onSelect={(newUsage) => {
|
||||
onUsageChange(shortcode, newUsage);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-item">
|
||||
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
|
||||
<div className="image-pack-item__content">
|
||||
<Text>{shortcode}</Text>
|
||||
</div>
|
||||
<div className="image-pack-item__usage">
|
||||
<div className="image-pack-item__btn">
|
||||
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
|
||||
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
|
||||
</div>
|
||||
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
||||
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
||||
<Text variant="b2">
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackItem.defaultProps = {
|
||||
onUsageChange: null,
|
||||
onDelete: null,
|
||||
onRename: null,
|
||||
};
|
||||
ImagePackItem.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
shortcode: PropTypes.string.isRequired,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onRename: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagePackItem;
|
||||
@@ -0,0 +1,43 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.image-pack-item {
|
||||
margin: 0 var(--sp-normal);
|
||||
padding: var(--sp-tight) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
& .avatar-container img {
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
}
|
||||
|
||||
&__usage {
|
||||
display: flex;
|
||||
gap: var(--sp-ultra-tight);
|
||||
& button {
|
||||
padding: 6px;
|
||||
}
|
||||
& > button.btn-surface {
|
||||
padding: 6px var(--sp-tight);
|
||||
min-width: 0;
|
||||
@include dir.side(margin, var(--sp-ultra-tight), 0);
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: none;
|
||||
}
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.image-pack-item__btn {
|
||||
display: flex;
|
||||
gap: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackProfile.scss';
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import ImageUpload from '../image-upload/ImageUpload';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
|
||||
function ImagePackProfile({
|
||||
avatarUrl, displayName, attribution, usage,
|
||||
onUsageChange, onAvatarChange, onEditProfile,
|
||||
}) {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { nameInput, attributionInput } = e.target;
|
||||
const name = nameInput.value.trim() || undefined;
|
||||
const att = attributionInput.value.trim() || undefined;
|
||||
|
||||
onEditProfile(name, att);
|
||||
setIsEdit(false);
|
||||
};
|
||||
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
onSelect={(newUsage) => {
|
||||
onUsageChange(newUsage);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-profile">
|
||||
{
|
||||
onAvatarChange
|
||||
? (
|
||||
<ImageUpload
|
||||
bgColor="#555"
|
||||
text={displayName}
|
||||
imageSrc={avatarUrl}
|
||||
size="normal"
|
||||
onUpload={onAvatarChange}
|
||||
onRequestRemove={() => onAvatarChange(undefined)}
|
||||
/>
|
||||
)
|
||||
: <Avatar bgColor="#555" text={displayName} imageSrc={avatarUrl} size="normal" />
|
||||
}
|
||||
<div className="image-pack-profile__content">
|
||||
{
|
||||
isEdit
|
||||
? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="nameInput" label="Name" value={displayName} required />
|
||||
<Input name="attributionInput" label="Attribution" value={attribution} resizable />
|
||||
<div>
|
||||
<Button variant="primary" type="submit">Save</Button>
|
||||
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Text>{displayName}</Text>
|
||||
{onEditProfile && <IconButton size="extra-small" onClick={() => setIsEdit(true)} src={PencilIC} tooltip="Edit" />}
|
||||
</div>
|
||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="image-pack-profile__usage">
|
||||
<Text variant="b3">Pack usage</Text>
|
||||
<Button
|
||||
onClick={onUsageChange ? handleUsageSelect : undefined}
|
||||
iconSrc={onUsageChange ? ChevronBottomIC : null}
|
||||
>
|
||||
<Text>
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackProfile.defaultProps = {
|
||||
avatarUrl: null,
|
||||
attribution: null,
|
||||
onUsageChange: null,
|
||||
onAvatarChange: null,
|
||||
onEditProfile: null,
|
||||
};
|
||||
ImagePackProfile.propTypes = {
|
||||
avatarUrl: PropTypes.string,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
attribution: PropTypes.string,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onAvatarChange: PropTypes.func,
|
||||
onEditProfile: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagePackProfile;
|
||||
@@ -0,0 +1,37 @@
|
||||
@use '../../partials/flex';
|
||||
|
||||
.image-pack-profile {
|
||||
padding: var(--sp-normal);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--sp-tight);
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
|
||||
& > div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-extra-tight);
|
||||
|
||||
& .ic-btn {
|
||||
padding: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
& > form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-extra-tight);
|
||||
& > div:last-child {
|
||||
margin: var(--sp-extra-tight) 0;
|
||||
display: flex;
|
||||
gap: var(--sp-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__usage {
|
||||
& > *:first-child {
|
||||
margin-bottom: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackUpload.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { scaleDownImage } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||
|
||||
function ImagePackUpload({ onUpload }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const inputRef = useRef(null);
|
||||
const shortcodeRef = useRef(null);
|
||||
const [imgFile, setImgFile] = useState(null);
|
||||
const [progress, setProgress] = useState(false);
|
||||
|
||||
const handleSubmit = async (evt) => {
|
||||
evt.preventDefault();
|
||||
if (!imgFile) return;
|
||||
const { shortcodeInput } = evt.target;
|
||||
const shortcode = shortcodeInput.value.trim();
|
||||
if (shortcode === '') return;
|
||||
|
||||
setProgress(true);
|
||||
const image = await scaleDownImage(imgFile, 512, 512);
|
||||
const { content_uri: url } = await mx.uploadContent(image);
|
||||
|
||||
onUpload(shortcode, url);
|
||||
setProgress(false);
|
||||
setImgFile(null);
|
||||
shortcodeRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleFileChange = (evt) => {
|
||||
const img = evt.target.files[0];
|
||||
if (!img) return;
|
||||
setImgFile(img);
|
||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf('.'));
|
||||
shortcodeRef.current.focus();
|
||||
};
|
||||
const handleRemove = () => {
|
||||
setImgFile(null);
|
||||
inputRef.current.value = null;
|
||||
shortcodeRef.current.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="image-pack-upload">
|
||||
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" accept=".png, .gif, .webp" required />
|
||||
{
|
||||
imgFile
|
||||
? (
|
||||
<div className="image-pack-upload__file">
|
||||
<IconButton onClick={handleRemove} src={CirclePlusIC} tooltip="Remove file" />
|
||||
<Text>{imgFile.name}</Text>
|
||||
</div>
|
||||
)
|
||||
: <Button onClick={() => inputRef.current.click()}>Import image</Button>
|
||||
}
|
||||
<Input forwardRef={shortcodeRef} name="shortcodeInput" placeholder="shortcode" required />
|
||||
<Button disabled={progress} variant="primary" type="submit">{progress ? 'Uploading...' : 'Upload'}</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
ImagePackUpload.propTypes = {
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackUpload;
|
||||
@@ -0,0 +1,43 @@
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
|
||||
.image-pack-upload {
|
||||
padding: var(--sp-normal);
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
gap: var(--sp-tight);
|
||||
|
||||
& > .input-container {
|
||||
flex-grow: 1;
|
||||
input {
|
||||
padding: 9px var(--sp-normal);
|
||||
}
|
||||
}
|
||||
&__file {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--bg-surface-low);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
|
||||
& button {
|
||||
--parent-height: 40px;
|
||||
width: var(--parent-height);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& .ic-raw {
|
||||
background-color: var(--bg-caution);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
& .text {
|
||||
@extend .cp-txt__ellipsis;
|
||||
@include dir.side(margin, var(--sp-ultra-tight), var(--sp-normal));
|
||||
max-width: 86px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
||||
|
||||
function ImagePackUsageSelector({ usage, onSelect }) {
|
||||
return (
|
||||
<div>
|
||||
<MenuHeader>Usage</MenuHeader>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'emoticon' ? CheckIC : undefined}
|
||||
variant={usage === 'emoticon' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('emoticon')}
|
||||
>
|
||||
Emoji
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'sticker' ? CheckIC : undefined}
|
||||
variant={usage === 'sticker' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('sticker')}
|
||||
>
|
||||
Sticker
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'both' ? CheckIC : undefined}
|
||||
variant={usage === 'both' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('both')}
|
||||
>
|
||||
Both
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePackUsageSelector.propTypes = {
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImagePackUsageSelector;
|
||||
@@ -7,9 +7,13 @@ import initMatrix from '../../../client/initMatrix';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Spinner from '../../atoms/spinner/Spinner';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
|
||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||
|
||||
function ImageUpload({
|
||||
text, bgColor, imageSrc, onUpload, onRequestRemove,
|
||||
size,
|
||||
}) {
|
||||
const [uploadPromise, setUploadPromise] = useState(null);
|
||||
const uploadImageRef = useRef(null);
|
||||
@@ -18,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;
|
||||
@@ -50,10 +54,14 @@ function ImageUpload({
|
||||
imageSrc={imageSrc}
|
||||
text={text}
|
||||
bgColor={bgColor}
|
||||
size="large"
|
||||
size={size}
|
||||
/>
|
||||
<div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
|
||||
{uploadPromise === null && <Text variant="b3" weight="bold">Upload</Text>}
|
||||
{uploadPromise === null && (
|
||||
size === 'large'
|
||||
? <Text variant="b3" weight="bold">Upload</Text>
|
||||
: <RawIcon src={PlusIC} color="white" />
|
||||
)}
|
||||
{uploadPromise !== null && <Spinner size="small" />}
|
||||
</div>
|
||||
</button>
|
||||
@@ -66,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>
|
||||
);
|
||||
}
|
||||
@@ -75,6 +83,7 @@ ImageUpload.defaultProps = {
|
||||
text: null,
|
||||
bgColor: 'transparent',
|
||||
imageSrc: null,
|
||||
size: 'large',
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
@@ -83,6 +92,7 @@ ImageUpload.propTypes = {
|
||||
imageSrc: PropTypes.string,
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
onRequestRemove: PropTypes.func.isRequired,
|
||||
size: PropTypes.oneOf(['large', 'normal']),
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
@@ -4,42 +4,17 @@ import './Media.scss';
|
||||
|
||||
import encrypt from 'browser-encrypt-attachment';
|
||||
|
||||
import { BlurhashCanvas } from 'react-blurhash';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Spinner from '../../atoms/spinner/Spinner';
|
||||
import ImageLightbox from '../image-lightbox/ImageLightbox';
|
||||
|
||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
||||
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
|
||||
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/a9e28db33058d1893d964ec96cd247ecc3d92fc3/src/utils/blobs.ts#L73
|
||||
const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
function getBlobSafeMimeType(mimetype) {
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
return mimetype;
|
||||
}
|
||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
||||
|
||||
async function getDecryptedBlob(response, type, decryptData) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@@ -61,16 +36,12 @@ async function getUrl(link, type, decryptData) {
|
||||
}
|
||||
}
|
||||
|
||||
function getNativeHeight(width, height) {
|
||||
const MEDIA_MAX_WIDTH = 296;
|
||||
const scale = MEDIA_MAX_WIDTH / width;
|
||||
function getNativeHeight(width, height, maxWidth = 296) {
|
||||
const scale = maxWidth / width;
|
||||
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() {
|
||||
@@ -87,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"
|
||||
@@ -126,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} />
|
||||
@@ -146,9 +115,75 @@ File.propTypes = {
|
||||
file: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
function Image({
|
||||
name, width, height, link, file, type,
|
||||
}) {
|
||||
function Image({ name, width, height, link, file, type, blurhash }) {
|
||||
const [url, setUrl] = useState(null);
|
||||
const [blur, setBlur] = useState(true);
|
||||
const [lightbox, setLightbox] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
async function fetchUrl() {
|
||||
const myUrl = await getUrl(link, type, file);
|
||||
if (unmounted) return;
|
||||
setUrl(myUrl);
|
||||
}
|
||||
fetchUrl();
|
||||
return () => {
|
||||
unmounted = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleLightbox = () => {
|
||||
if (!url) return;
|
||||
setLightbox(!lightbox);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="file-container">
|
||||
<div
|
||||
style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }}
|
||||
className="image-container"
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onClick={toggleLightbox}
|
||||
onKeyDown={toggleLightbox}
|
||||
>
|
||||
{blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{url !== null && (
|
||||
<img
|
||||
style={{ display: blur ? 'none' : 'unset' }}
|
||||
onLoad={() => setBlur(false)}
|
||||
src={url || link}
|
||||
alt={name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{url && (
|
||||
<ImageLightbox url={url} alt={name} isOpen={lightbox} onRequestClose={toggleLightbox} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Image.defaultProps = {
|
||||
file: null,
|
||||
width: null,
|
||||
height: null,
|
||||
type: '',
|
||||
blurhash: '',
|
||||
};
|
||||
Image.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
link: PropTypes.string.isRequired,
|
||||
file: PropTypes.shape({}),
|
||||
type: PropTypes.string,
|
||||
blurhash: PropTypes.string,
|
||||
};
|
||||
|
||||
function Sticker({ name, height, width, link, file, type }) {
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -165,21 +200,21 @@ function Image({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="file-container">
|
||||
<FileHeader name={name} link={url || link} type={type} external />
|
||||
<div style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }} className="image-container">
|
||||
{ url !== null && <img src={url || link} alt={name} />}
|
||||
</div>
|
||||
<div
|
||||
className="sticker-container"
|
||||
style={{ height: width !== null ? getNativeHeight(width, height, 256) : 'unset' }}
|
||||
>
|
||||
{url !== null && <img src={url || link} title={name} alt={name} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Image.defaultProps = {
|
||||
Sticker.defaultProps = {
|
||||
file: null,
|
||||
type: '',
|
||||
width: null,
|
||||
height: null,
|
||||
type: '',
|
||||
};
|
||||
Image.propTypes = {
|
||||
Sticker.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
@@ -188,9 +223,7 @@ Image.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);
|
||||
|
||||
@@ -208,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)} />
|
||||
@@ -232,12 +267,21 @@ Audio.propTypes = {
|
||||
};
|
||||
|
||||
function Video({
|
||||
name, link, thumbnail,
|
||||
width, height, file, type, thumbnailFile, thumbnailType,
|
||||
name,
|
||||
link,
|
||||
thumbnail,
|
||||
thumbnailFile,
|
||||
thumbnailType,
|
||||
width,
|
||||
height,
|
||||
file,
|
||||
type,
|
||||
blurhash,
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [url, setUrl] = useState(null);
|
||||
const [thumbUrl, setThumbUrl] = useState(null);
|
||||
const [blur, setBlur] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
@@ -252,16 +296,16 @@ function Video({
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function loadVideo() {
|
||||
const loadVideo = async () => {
|
||||
const myUrl = await getUrl(link, type, file);
|
||||
setUrl(myUrl);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
function handlePlayVideo() {
|
||||
const handlePlayVideo = () => {
|
||||
setIsLoading(true);
|
||||
loadVideo();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-container">
|
||||
@@ -269,14 +313,27 @@ function Video({
|
||||
<div
|
||||
style={{
|
||||
height: width !== null ? getNativeHeight(width, height) : 'unset',
|
||||
backgroundImage: thumbUrl === null ? 'none' : `url(${thumbUrl}`,
|
||||
}}
|
||||
className="video-container"
|
||||
>
|
||||
{ url === null && isLoading && <Spinner size="small" /> }
|
||||
{ url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
|
||||
{ url !== null && (
|
||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||
{url === null ? (
|
||||
<>
|
||||
{blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{thumbUrl !== null && (
|
||||
<img
|
||||
style={{ display: blur ? 'none' : 'unset' }}
|
||||
src={thumbUrl}
|
||||
onLoad={() => setBlur(false)}
|
||||
alt={name}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <Spinner size="small" />}
|
||||
{!isLoading && (
|
||||
<IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||
<video autoPlay controls poster={thumbUrl}>
|
||||
<source src={url} type={getBlobSafeMimeType(type)} />
|
||||
</video>
|
||||
@@ -290,22 +347,22 @@ Video.defaultProps = {
|
||||
height: null,
|
||||
file: null,
|
||||
thumbnail: null,
|
||||
type: '',
|
||||
thumbnailType: null,
|
||||
thumbnailFile: null,
|
||||
type: '',
|
||||
blurhash: null,
|
||||
};
|
||||
Video.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
link: PropTypes.string.isRequired,
|
||||
thumbnail: PropTypes.string,
|
||||
thumbnailFile: PropTypes.shape({}),
|
||||
thumbnailType: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
file: PropTypes.shape({}),
|
||||
type: PropTypes.string,
|
||||
thumbnailFile: PropTypes.shape({}),
|
||||
thumbnailType: PropTypes.string,
|
||||
blurhash: PropTypes.string,
|
||||
};
|
||||
|
||||
export {
|
||||
File, Image, Audio, Video,
|
||||
};
|
||||
export { File, Image, Sticker, Audio, Video };
|
||||
@@ -27,6 +27,15 @@
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
.sticker-container {
|
||||
display: inline-flex;
|
||||
max-width: 256px;
|
||||
width: 100%;
|
||||
& img {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-container,
|
||||
.video-container,
|
||||
.audio-container {
|
||||
@@ -42,25 +51,40 @@
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
& img {
|
||||
.image-container,
|
||||
.video-container {
|
||||
& img,
|
||||
& canvas {
|
||||
max-width: unset !important;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
border-radius: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
.image-container {
|
||||
max-height: 460px;
|
||||
img {
|
||||
cursor: pointer;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
& .ic-btn-surface {
|
||||
background-color: var(--bg-surface-low);
|
||||
}
|
||||
& .ic-btn-surface,
|
||||
& .donut-spinner {
|
||||
position: absolute;
|
||||
}
|
||||
video {
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.audio-container {
|
||||
audio {
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +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 { getShortcodeToCustomEmoji } from '../../organisms/emoji-board/custom-emoji';
|
||||
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 +33,12 @@ import Tooltip from '../../atoms/tooltip/Tooltip';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
||||
import Time from '../../atoms/time/Time';
|
||||
import ContextMenu, {
|
||||
MenuHeader,
|
||||
MenuItem,
|
||||
MenuBorder,
|
||||
} from '../../atoms/context-menu/ContextMenu';
|
||||
import * as Media from '../media/Media';
|
||||
|
||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
||||
@@ -33,10 +46,14 @@ 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 { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
||||
import { html, plain } from '../../../util/markdown';
|
||||
|
||||
function PlaceholderMessage() {
|
||||
return (
|
||||
@@ -57,9 +74,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" />
|
||||
@@ -67,9 +82,7 @@ const MessageAvatar = React.memo(({
|
||||
</div>
|
||||
));
|
||||
|
||||
const MessageHeader = React.memo(({
|
||||
userId, username, time,
|
||||
}) => (
|
||||
const MessageHeader = React.memo(({ userId, username, timestamp, fullTime }) => (
|
||||
<div className="message__header">
|
||||
<Text
|
||||
style={{ color: colorMXID(userId) }}
|
||||
@@ -82,14 +95,20 @@ const MessageHeader = React.memo(({
|
||||
<span>{twemojify(userId)}</span>
|
||||
</Text>
|
||||
<div className="message__time">
|
||||
<Text variant="b3">{time}</Text>
|
||||
<Text variant="b3">
|
||||
<Time timestamp={timestamp} fullTime={fullTime} />
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
MessageHeader.defaultProps = {
|
||||
fullTime: false,
|
||||
};
|
||||
MessageHeader.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
timestamp: PropTypes.number.isRequired,
|
||||
fullTime: PropTypes.bool,
|
||||
};
|
||||
|
||||
function MessageReply({ name, color, body }) {
|
||||
@@ -97,9 +116,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>
|
||||
);
|
||||
@@ -133,7 +150,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);
|
||||
@@ -162,8 +181,8 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
|
||||
}, []);
|
||||
|
||||
const focusReply = (ev) => {
|
||||
if (!ev.keyCode || ev.keyCode === 32 || ev.keyCode === 13) {
|
||||
if (ev.keyCode) ev.preventDefault();
|
||||
if (!ev.key || ev.key === ' ' || ev.key === 'Enter') {
|
||||
if (ev.key) ev.preventDefault();
|
||||
if (reply?.event === null) return;
|
||||
if (reply?.event.isRedacted()) return;
|
||||
roomTimeline.loadEventTimeline(eventId);
|
||||
@@ -187,20 +206,20 @@ 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>;
|
||||
|
||||
let content = null;
|
||||
if (isCustomHTML) {
|
||||
try {
|
||||
content = twemojify(sanitizeCustomHtml(body), undefined, true, false, true);
|
||||
content = twemojify(
|
||||
sanitizeCustomHtml(initMatrix.matrixClient, body),
|
||||
undefined,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
);
|
||||
} catch {
|
||||
console.error('Malformed custom html: ', body);
|
||||
content = twemojify(body, undefined);
|
||||
@@ -224,10 +243,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;
|
||||
}
|
||||
}
|
||||
@@ -235,22 +258,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 className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
||||
{ msgType === 'm.emote' && (
|
||||
<div dir="auto" className={`text ${emojiOnly ? 'text-h1' : 'text-b1'}`}>
|
||||
{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>
|
||||
);
|
||||
});
|
||||
@@ -277,14 +303,25 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13 && e.shiftKey === false) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onSave(editInputRef.current.value);
|
||||
onCancel();
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
onSave(editInputRef.current.value, body);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value); }}>
|
||||
<form
|
||||
className="message__edit"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSave(editInputRef.current.value, body);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
forwardRef={editInputRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -295,7 +332,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>
|
||||
@@ -322,7 +361,7 @@ function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
|
||||
return rEvent;
|
||||
}
|
||||
|
||||
function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
|
||||
function toggleEmoji(roomId, eventId, emojiKey, shortcode, roomTimeline) {
|
||||
const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
|
||||
if (myAlreadyReactEvent) {
|
||||
const rId = myAlreadyReactEvent.getId();
|
||||
@@ -330,67 +369,77 @@ function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
|
||||
redactEvent(roomId, rId);
|
||||
return;
|
||||
}
|
||||
sendReaction(roomId, eventId, emojiKey);
|
||||
sendReaction(roomId, eventId, emojiKey, shortcode);
|
||||
}
|
||||
|
||||
function pickEmoji(e, roomId, eventId, roomTimeline) {
|
||||
openEmojiBoard(getEventCords(e), (emoji) => {
|
||||
toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
|
||||
toggleEmoji(roomId, eventId, emoji.mxc ?? emoji.unicode, emoji.shortcodes[0], roomTimeline);
|
||||
e.target.click();
|
||||
});
|
||||
}
|
||||
|
||||
function genReactionMsg(userIds, reaction) {
|
||||
function genReactionMsg(userIds, reaction, shortcode) {
|
||||
return (
|
||||
<>
|
||||
{userIds.map((userId, index) => (
|
||||
<React.Fragment key={userId}>
|
||||
{twemojify(getUsername(userId))}
|
||||
{index < userIds.length - 1 && (
|
||||
<span style={{ opacity: '.6' }}>
|
||||
{index === userIds.length - 2 ? ' and ' : ', '}
|
||||
</span>
|
||||
<span style={{ opacity: '.6' }}>{index === userIds.length - 2 ? ' and ' : ', '}</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
|
||||
{twemojify(reaction, { className: 'react-emoji' })}
|
||||
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageReaction({
|
||||
shortcodeToEmoji, reaction, count, users, isActive, onClick,
|
||||
}) {
|
||||
const customEmojiMatch = reaction.match(/^:(\S+):$/);
|
||||
function MessageReaction({ reaction, shortcode, count, users, isActive, onClick }) {
|
||||
let customEmojiUrl = null;
|
||||
if (customEmojiMatch) {
|
||||
const customEmoji = shortcodeToEmoji.get(customEmojiMatch[1]);
|
||||
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(customEmoji?.mxc);
|
||||
if (reaction.match(/^mxc:\/\/\S+$/)) {
|
||||
customEmojiUrl = initMatrix.matrixClient.mxcUrlToHttp(reaction);
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
className="msg__reaction-tooltip"
|
||||
content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction) : '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={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>
|
||||
);
|
||||
}
|
||||
MessageReaction.defaultProps = {
|
||||
shortcode: undefined,
|
||||
};
|
||||
MessageReaction.propTypes = {
|
||||
shortcodeToEmoji: PropTypes.shape({}).isRequired,
|
||||
reaction: PropTypes.node.isRequired,
|
||||
shortcode: PropTypes.string,
|
||||
count: PropTypes.number.isRequired,
|
||||
users: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
@@ -401,11 +450,10 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
const { roomId, room, reactionTimeline } = roomTimeline;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const reactions = {};
|
||||
const shortcodeToEmoji = getShortcodeToCustomEmoji(room);
|
||||
const canSendReaction = room.currentState.maySendEvent('m.reaction', mx.getUserId());
|
||||
|
||||
const eventReactions = reactionTimeline.get(mEvent.getId());
|
||||
const addReaction = (key, count, senderId, isActive) => {
|
||||
const addReaction = (key, shortcode, count, senderId, isActive) => {
|
||||
let reaction = reactions[key];
|
||||
if (reaction === undefined) {
|
||||
reaction = {
|
||||
@@ -414,6 +462,7 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
isActive: false,
|
||||
};
|
||||
}
|
||||
if (shortcode) reaction.shortcode = shortcode;
|
||||
if (count) {
|
||||
reaction.count = count;
|
||||
} else {
|
||||
@@ -429,9 +478,10 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
if (rEvent.getRelation() === null) return;
|
||||
const reaction = rEvent.getRelation();
|
||||
const senderId = rEvent.getSender();
|
||||
const { shortcode } = rEvent.getContent();
|
||||
const isActive = senderId === mx.getUserId();
|
||||
|
||||
addReaction(reaction.key, undefined, senderId, isActive);
|
||||
addReaction(reaction.key, shortcode, undefined, senderId, isActive);
|
||||
});
|
||||
} else {
|
||||
// Use aggregated reactions
|
||||
@@ -439,27 +489,25 @@ function MessageReactionGroup({ roomTimeline, mEvent }) {
|
||||
if (!aggregatedReaction) return null;
|
||||
aggregatedReaction.forEach((reaction) => {
|
||||
if (reaction.type !== 'm.reaction') return;
|
||||
addReaction(reaction.key, reaction.count, undefined, false);
|
||||
addReaction(reaction.key, undefined, reaction.count, undefined, false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="message__reactions text text-b3 noselect">
|
||||
{
|
||||
Object.keys(reactions).map((key) => (
|
||||
<MessageReaction
|
||||
key={key}
|
||||
shortcodeToEmoji={shortcodeToEmoji}
|
||||
reaction={key}
|
||||
count={reactions[key].count}
|
||||
users={reactions[key].users}
|
||||
isActive={reactions[key].isActive}
|
||||
onClick={() => {
|
||||
toggleEmoji(roomId, mEvent.getId(), key, 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) => {
|
||||
@@ -480,11 +528,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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -500,9 +548,11 @@ function handleOpenViewSource(mEvent, roomTimeline) {
|
||||
openViewSource(editedMEvent !== undefined ? editedMEvent : mEvent);
|
||||
}
|
||||
|
||||
const MessageOptions = React.memo(({
|
||||
roomTimeline, mEvent, edit, reply,
|
||||
}) => {
|
||||
function handleTextTranslate(mEvent) {
|
||||
openTextTranslate(mEvent);
|
||||
}
|
||||
|
||||
const MessageOptions = React.memo(({ roomTimeline, mEvent, edit, reply }) => {
|
||||
const { roomId, room } = roomTimeline;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const senderId = mEvent.getSender();
|
||||
@@ -521,19 +571,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={() => (
|
||||
@@ -545,12 +585,12 @@ 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>
|
||||
{(canIRedact || senderId === mx.getUserId()) && (
|
||||
<>
|
||||
<MenuBorder />
|
||||
@@ -562,7 +602,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());
|
||||
@@ -596,7 +636,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';
|
||||
@@ -604,10 +645,18 @@ 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;
|
||||
if (mE.getType() === 'm.sticker') msgType = 'm.image';
|
||||
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
|
||||
if (mE.getType() === 'm.sticker') {
|
||||
msgType = 'm.sticker';
|
||||
} else if (safeMimetype === 'application/octet-stream') {
|
||||
msgType = 'm.file';
|
||||
}
|
||||
|
||||
const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
|
||||
|
||||
switch (msgType) {
|
||||
case 'm.file':
|
||||
@@ -628,6 +677,18 @@ function genMediaContent(mE) {
|
||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||
file={isEncryptedFile ? mContent.file : null}
|
||||
type={mContent.info?.mimetype}
|
||||
blurhash={blurhash}
|
||||
/>
|
||||
);
|
||||
case 'm.sticker':
|
||||
return (
|
||||
<Media.Sticker
|
||||
name={mContent.body}
|
||||
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||
file={isEncryptedFile ? mContent.file : null}
|
||||
type={mContent.info?.mimetype}
|
||||
/>
|
||||
);
|
||||
case 'm.audio':
|
||||
@@ -654,6 +715,7 @@ function genMediaContent(mE) {
|
||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||
file={isEncryptedFile ? mContent.file : null}
|
||||
type={mContent.info?.mimetype}
|
||||
blurhash={blurhash}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -674,13 +736,19 @@ function getEditedBody(editedMEvent) {
|
||||
}
|
||||
|
||||
function Message({
|
||||
mEvent, isBodyOnly, roomTimeline, focus, time,
|
||||
mEvent,
|
||||
isBodyOnly,
|
||||
roomTimeline,
|
||||
focus,
|
||||
fullTime,
|
||||
isEdit,
|
||||
setEdit,
|
||||
cancelEdit,
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
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();
|
||||
@@ -688,62 +756,64 @@ 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(() => {
|
||||
setIsEditing(true);
|
||||
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 userId={senderId} username={username} time={time} />
|
||||
)}
|
||||
{roomTimeline && isReply && (
|
||||
<MessageReplyWrapper
|
||||
roomTimeline={roomTimeline}
|
||||
eventId={mEvent.replyEventId}
|
||||
<MessageHeader
|
||||
userId={senderId}
|
||||
username={username}
|
||||
timestamp={mEvent.getTs()}
|
||||
fullTime={fullTime}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && (
|
||||
{roomTimeline && isReply && (
|
||||
<MessageReplyWrapper roomTimeline={roomTimeline} eventId={mEvent.replyEventId} />
|
||||
)}
|
||||
{!isEdit && (
|
||||
<MessageBody
|
||||
senderName={username}
|
||||
isCustomHTML={isCustomHTML}
|
||||
@@ -752,28 +822,25 @@ function Message({
|
||||
isEdited={isEdited}
|
||||
/>
|
||||
)}
|
||||
{isEditing && (
|
||||
{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);
|
||||
}
|
||||
setIsEditing(false);
|
||||
cancelEdit();
|
||||
}}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
onCancel={cancelEdit}
|
||||
/>
|
||||
)}
|
||||
{haveReactions && (
|
||||
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
|
||||
)}
|
||||
{roomTimeline && !isEditing && (
|
||||
<MessageOptions
|
||||
roomTimeline={roomTimeline}
|
||||
mEvent={mEvent}
|
||||
edit={edit}
|
||||
reply={reply}
|
||||
/>
|
||||
{haveReactions && <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />}
|
||||
{roomTimeline && !isEdit && (
|
||||
<MessageOptions roomTimeline={roomTimeline} mEvent={mEvent} edit={edit} reply={reply} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -783,13 +850,20 @@ Message.defaultProps = {
|
||||
isBodyOnly: false,
|
||||
focus: false,
|
||||
roomTimeline: null,
|
||||
fullTime: false,
|
||||
isEdit: false,
|
||||
setEdit: null,
|
||||
cancelEdit: null,
|
||||
};
|
||||
Message.propTypes = {
|
||||
mEvent: PropTypes.shape({}).isRequired,
|
||||
isBodyOnly: PropTypes.bool,
|
||||
roomTimeline: PropTypes.shape({}),
|
||||
focus: PropTypes.bool,
|
||||
time: PropTypes.string.isRequired,
|
||||
fullTime: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
setEdit: PropTypes.func,
|
||||
cancelEdit: PropTypes.func,
|
||||
};
|
||||
|
||||
export { Message, MessageReply, PlaceholderMessage };
|
||||
@@ -15,7 +15,7 @@
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__avatar-container {
|
||||
padding-top: 6px;
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
@@ -101,7 +101,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.message__header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -115,10 +114,16 @@
|
||||
@extend .cp-txt__ellipsis;
|
||||
color: inherit;
|
||||
}
|
||||
& > span:last-child { display: none; }
|
||||
& > span:last-child {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
& > span:first-child { display: none; }
|
||||
& > span:last-child { display: block; }
|
||||
& > span:first-child {
|
||||
display: none;
|
||||
}
|
||||
& > span:last-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +163,7 @@
|
||||
.message__body {
|
||||
word-break: break-word;
|
||||
|
||||
& > .text > * {
|
||||
& > .text > .message__body-plain {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -168,7 +173,11 @@
|
||||
& > .text > a {
|
||||
white-space: initial !important;
|
||||
}
|
||||
|
||||
|
||||
& > .text > p + p {
|
||||
margin-top: var(--sp-normal);
|
||||
}
|
||||
|
||||
& span[data-mx-pill] {
|
||||
background-color: hsla(0, 0%, 64%, 0.15);
|
||||
padding: 0 2px;
|
||||
@@ -191,7 +200,7 @@
|
||||
& span[data-mx-spoiler] {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(124, 124, 124, 0.5);
|
||||
color:transparent;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -220,6 +229,8 @@
|
||||
padding: var(--sp-extra-tight) 0;
|
||||
&-btns button {
|
||||
margin: var(--sp-tight) 0 0 0;
|
||||
padding: var(--sp-ultra-tight) var(--sp-tight);
|
||||
min-width: 0;
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
}
|
||||
}
|
||||
@@ -250,15 +261,14 @@
|
||||
cursor: pointer;
|
||||
|
||||
& .react-emoji {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 2px;
|
||||
}
|
||||
&-count {
|
||||
margin: 0 var(--sp-ultra-tight);
|
||||
color: var(--tc-surface-normal)
|
||||
color: var(--tc-surface-normal);
|
||||
}
|
||||
&-tooltip .react-emoji {
|
||||
&-tooltip .react-emoji {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 var(--sp-ultra-tight);
|
||||
@@ -271,19 +281,19 @@
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--bg-surface-active)
|
||||
background-color: var(--bg-surface-active);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background-color: var(--bg-caution-active);
|
||||
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: var(--bg-caution-hover);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--bg-caution-active)
|
||||
background-color: var(--bg-caution-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +314,12 @@
|
||||
|
||||
// markdown formating
|
||||
.message__body {
|
||||
& h1, h2, h3, h4, h5, h6 {
|
||||
& h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
margin-bottom: var(--sp-ultra-tight);
|
||||
font-weight: var(--fw-medium);
|
||||
@@ -385,6 +400,7 @@
|
||||
& pre {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
direction: initial;
|
||||
@include scrollbar.scroll;
|
||||
@include scrollbar.scroll__h;
|
||||
@include scrollbar.scroll--auto-hide;
|
||||
@@ -425,7 +441,8 @@
|
||||
@include scrollbar.scroll__h;
|
||||
@include scrollbar.scroll--auto-hide;
|
||||
|
||||
& td, & th {
|
||||
& td,
|
||||
& th {
|
||||
padding: var(--sp-extra-tight);
|
||||
border: 1px solid var(--bg-surface-border);
|
||||
border-width: 0 1px 1px 0;
|
||||
@@ -433,11 +450,11 @@
|
||||
&:last-child {
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
[dir=rtl] & {
|
||||
[dir='rtl'] & {
|
||||
border-width: 0 1px 1px 0;
|
||||
}
|
||||
}
|
||||
[dir=rtl] &:first-child {
|
||||
[dir='rtl'] &:first-child {
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import './TimelineChange.scss';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import Time from '../../atoms/time/Time';
|
||||
|
||||
import JoinArraowIC from '../../../../public/res/ic/outlined/join-arrow.svg';
|
||||
import LeaveArraowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||
@@ -12,7 +13,7 @@ import InviteCancelArraowIC from '../../../../public/res/ic/outlined/invite-canc
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
|
||||
function TimelineChange({
|
||||
variant, content, time, onClick,
|
||||
variant, content, timestamp, onClick,
|
||||
}) {
|
||||
let iconSrc;
|
||||
|
||||
@@ -48,7 +49,9 @@ function TimelineChange({
|
||||
</Text>
|
||||
</div>
|
||||
<div className="timeline-change__time">
|
||||
<Text variant="b3">{time}</Text>
|
||||
<Text variant="b3">
|
||||
<Time timestamp={timestamp} />
|
||||
</Text>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -68,7 +71,7 @@ TimelineChange.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
timestamp: PropTypes.number.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ function RoomAliases({ roomId }) {
|
||||
const loadLocalAliases = async () => {
|
||||
let local = [];
|
||||
try {
|
||||
const result = await mx.unstableGetLocalAliases(roomId);
|
||||
const result = await mx.getLocalAliases(roomId);
|
||||
local = result.aliases.filter((alias) => !aliases.published.includes(alias));
|
||||
} catch {
|
||||
local = [];
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import React, { useReducer, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomEmojis.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { suffixRename } from '../../../util/common';
|
||||
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import ImagePack from '../image-pack/ImagePack';
|
||||
|
||||
function useRoomPacks(room) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
|
||||
const packEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
|
||||
const unUsablePacks = [];
|
||||
const usablePacks = packEvents.filter((mEvent) => {
|
||||
if (typeof mEvent.getContent()?.images !== 'object') {
|
||||
unUsablePacks.push(mEvent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleEvent = (event, state, prevEvent) => {
|
||||
if (event.getRoomId() !== room.roomId) return;
|
||||
if (event.getType() !== 'im.ponies.room_emotes') return;
|
||||
if (!prevEvent?.getContent()?.images || !event.getContent().images) {
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
mx.on('RoomState.events', handleEvent);
|
||||
return () => {
|
||||
mx.removeListener('RoomState.events', handleEvent);
|
||||
};
|
||||
}, [room, mx]);
|
||||
|
||||
const isStateKeyAvailable = (key) => !room.currentState.getStateEvents('im.ponies.room_emotes', key);
|
||||
|
||||
const createPack = async (name) => {
|
||||
const packContent = {
|
||||
pack: { display_name: name },
|
||||
images: {},
|
||||
};
|
||||
let stateKey = '';
|
||||
if (unUsablePacks.length > 0) {
|
||||
const mEvent = unUsablePacks[0];
|
||||
stateKey = mEvent.getStateKey();
|
||||
} else {
|
||||
stateKey = packContent.pack.display_name.replace(/\s/g, '-');
|
||||
if (!isStateKeyAvailable(stateKey)) {
|
||||
stateKey = suffixRename(
|
||||
stateKey,
|
||||
isStateKeyAvailable,
|
||||
);
|
||||
}
|
||||
}
|
||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', packContent, stateKey);
|
||||
};
|
||||
|
||||
const deletePack = async (stateKey) => {
|
||||
await mx.sendStateEvent(room.roomId, 'im.ponies.room_emotes', {}, stateKey);
|
||||
};
|
||||
|
||||
return {
|
||||
usablePacks,
|
||||
createPack,
|
||||
deletePack,
|
||||
};
|
||||
}
|
||||
|
||||
function RoomEmojis({ roomId }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const { usablePacks, createPack, deletePack } = useRoomPacks(room);
|
||||
|
||||
const canChange = room.currentState.maySendStateEvent('im.ponies.room_emote', mx.getUserId());
|
||||
|
||||
const handlePackCreate = (e) => {
|
||||
e.preventDefault();
|
||||
const { nameInput } = e.target;
|
||||
const name = nameInput.value.trim();
|
||||
if (name === '') return;
|
||||
nameInput.value = '';
|
||||
|
||||
createPack(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="room-emojis">
|
||||
{ canChange && (
|
||||
<div className="room-emojis__add-pack">
|
||||
<MenuHeader>Create Pack</MenuHeader>
|
||||
<form onSubmit={handlePackCreate}>
|
||||
<Input name="nameInput" placeholder="Pack Name" required />
|
||||
<Button variant="primary" type="submit">Create pack</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
usablePacks.length > 0
|
||||
? usablePacks.reverse().map((mEvent) => (
|
||||
<ImagePack
|
||||
key={mEvent.getId()}
|
||||
roomId={roomId}
|
||||
stateKey={mEvent.getStateKey()}
|
||||
handlePackDelete={canChange ? deletePack : undefined}
|
||||
/>
|
||||
)) : (
|
||||
<div className="room-emojis__empty">
|
||||
<Text>No emoji or sticker pack.</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RoomEmojis.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RoomEmojis;
|
||||
@@ -0,0 +1,29 @@
|
||||
.room-emojis {
|
||||
.image-pack,
|
||||
.room-emojis__add-pack,
|
||||
.room-emojis__empty {
|
||||
margin: var(--sp-normal) 0;
|
||||
background-color: var(--bg-surface);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
overflow: hidden;
|
||||
|
||||
& > .context-menu__header:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
&__add-pack {
|
||||
& form {
|
||||
margin: var(--sp-normal);
|
||||
display: flex;
|
||||
gap: var(--sp-normal);
|
||||
& .input-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__empty {
|
||||
padding: var(--sp-extra-loose) var(--sp-normal);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -237,12 +237,12 @@ function RoomPermissions({ roomId }) {
|
||||
? permissions[permInfo.parent]?.[permKey]
|
||||
: permissions[permKey];
|
||||
|
||||
if (!permValue) permValue = permInfo.default;
|
||||
if (permValue === undefined) permValue = permInfo.default;
|
||||
|
||||
if (typeof permValue === 'number') {
|
||||
powerLevel = permValue;
|
||||
} else if (permKey === 'notifications') {
|
||||
powerLevel = permValue.room || 50;
|
||||
powerLevel = permValue.room ?? 50;
|
||||
}
|
||||
return (
|
||||
<SettingTile
|
||||
|
||||
@@ -132,7 +132,7 @@ function RoomProfile({ roomId }) {
|
||||
|
||||
const renderEditNameAndTopic = () => (
|
||||
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
|
||||
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" required />}
|
||||
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" />}
|
||||
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />}
|
||||
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>}
|
||||
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
|
||||
|
||||
@@ -2,8 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RoomSearch.scss';
|
||||
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { selectRoom } from '../../../client/action/navigation';
|
||||
@@ -120,14 +118,13 @@ function RoomSearch({ roomId }) {
|
||||
const renderTimeline = (timeline) => (
|
||||
<div className="room-search__result-item" key={timeline[0].getId()}>
|
||||
{ timeline.map((mEvent) => {
|
||||
const time = dateFormat(mEvent.getDate(), 'dd/mm/yyyy - hh:MM TT');
|
||||
const id = mEvent.getId();
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<Message
|
||||
mEvent={mEvent}
|
||||
isBodyOnly={false}
|
||||
time={time}
|
||||
fullTime
|
||||
/>
|
||||
<Button onClick={() => selectRoom(roomId, id)}>View</Button>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openSpaceSettings, openSpaceManage, openInviteUser } from '../../../client/action/navigation';
|
||||
import { markAsRead } from '../../../client/action/notifications';
|
||||
import { leave } from '../../../client/action/room';
|
||||
import {
|
||||
createSpaceShortcut,
|
||||
@@ -17,6 +18,7 @@ import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import CategoryIC from '../../../../public/res/ic/outlined/category.svg';
|
||||
import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
|
||||
@@ -28,11 +30,21 @@ import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
|
||||
function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const { roomList } = initMatrix;
|
||||
const room = mx.getRoom(roomId);
|
||||
const canInvite = room?.canInvite(mx.getUserId());
|
||||
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
|
||||
const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId);
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
const spaceChildren = roomList.getCategorizedSpaces([roomId]);
|
||||
spaceChildren?.forEach((childIds) => {
|
||||
childIds?.forEach((childId) => {
|
||||
markAsRead(childId);
|
||||
});
|
||||
});
|
||||
afterOptionSelect();
|
||||
};
|
||||
const handleInviteClick = () => {
|
||||
openInviteUser(roomId);
|
||||
afterOptionSelect();
|
||||
@@ -71,6 +83,7 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||
return (
|
||||
<div style={{ maxWidth: 'calc(var(--navigation-drawer-width) - var(--sp-normal))' }}>
|
||||
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
||||
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleCategorizeClick}
|
||||
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
)
|
||||
}
|
||||
</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 }) {
|
||||
@@ -141,10 +151,16 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
function getEmojiDataFromTarget(target) {
|
||||
const unicode = target.getAttribute('unicode');
|
||||
const hexcode = target.getAttribute('hexcode');
|
||||
const mxc = target.getAttribute('data-mx-emoticon');
|
||||
let shortcodes = target.getAttribute('shortcodes');
|
||||
if (typeof shortcodes === 'undefined') shortcodes = undefined;
|
||||
else shortcodes = shortcodes.split(',');
|
||||
return { unicode, hexcode, shortcodes };
|
||||
return {
|
||||
unicode,
|
||||
hexcode,
|
||||
shortcodes,
|
||||
mxc,
|
||||
};
|
||||
}
|
||||
|
||||
function selectEmoji(e) {
|
||||
@@ -202,21 +218,22 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
setAvailableEmojis([]);
|
||||
return;
|
||||
}
|
||||
// Retrieve the packs for the new room
|
||||
// Remove packs that aren't marked as emoji packs
|
||||
// Remove packs without emojis
|
||||
const packs = getRelevantPacks(
|
||||
initMatrix.matrixClient.getRoom(selectedRoomId),
|
||||
)
|
||||
.filter((pack) => pack.usage.indexOf('emoticon') !== -1)
|
||||
.filter((pack) => pack.getEmojis().length !== 0);
|
||||
|
||||
// Set an index for each pack so that we know where to jump when the user uses the nav
|
||||
for (let i = 0; i < packs.length; i += 1) {
|
||||
packs[i].packIndex = i;
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(selectedRoomId);
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
if (room) {
|
||||
const packs = getRelevantPacks(room.client, [room, ...parentRooms]).filter(
|
||||
(pack) => pack.getEmojis().length !== 0
|
||||
);
|
||||
|
||||
// Set an index for each pack so that we know where to jump when the user uses the nav
|
||||
for (let i = 0; i < packs.length; i += 1) {
|
||||
packs[i].packIndex = i;
|
||||
}
|
||||
setAvailableEmojis(packs);
|
||||
}
|
||||
|
||||
setAvailableEmojis(packs);
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
@@ -247,6 +264,55 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
|
||||
return (
|
||||
<div id="emoji-board" className="emoji-board">
|
||||
<ScrollView invisible>
|
||||
<div className="emoji-board__nav">
|
||||
{recentEmojis.length > 0 && (
|
||||
<IconButton
|
||||
onClick={() => openGroup(0)}
|
||||
src={RecentClockIC}
|
||||
tooltip="Recent"
|
||||
tooltipPlacement="left"
|
||||
/>
|
||||
)}
|
||||
<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
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</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"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
<div className="emoji-board__content">
|
||||
<div className="emoji-board__content__search">
|
||||
<RawIcon size="small" src={SearchIC} />
|
||||
@@ -256,81 +322,28 @@ 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}
|
||||
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>
|
||||
<ScrollView invisible>
|
||||
<div className="emoji-board__nav">
|
||||
{recentEmojis.length > 0 && (
|
||||
<IconButton
|
||||
onClick={() => openGroup(0)}
|
||||
src={RecentClockIC}
|
||||
tooltip="Recent"
|
||||
tooltipPlacement="right"
|
||||
/>
|
||||
)}
|
||||
<div className="emoji-board__nav-custom">
|
||||
{
|
||||
availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
src={src}
|
||||
key={pack.packIndex}
|
||||
tooltip={pack.displayName}
|
||||
tooltipPlacement="right"
|
||||
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="right"
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||