chore: prettier format all files, brotli, Sentry release tagging, CI gates
Prettier: auto-formatted 103 files to fix baseline. Prettier check in CI is now a hard gate (removed continue-on-error). Brotli: installed libnginx-mod-http-brotli-filter/static. Enabled in nginx with brotli_static on for pre-compressed assets and comp_level 6. Sentry releases: deploy script now exports VITE_APP_VERSION=<git-short-sha> before building so each Sentry release maps to an exact commit. CI also passes github.sha as VITE_APP_VERSION. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-4
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"defaultHomeserver": 0,
|
"defaultHomeserver": 0,
|
||||||
"homeserverList": [
|
"homeserverList": ["matrix.lotusguild.org"],
|
||||||
"matrix.lotusguild.org"
|
|
||||||
],
|
|
||||||
"allowCustomHomeservers": false,
|
"allowCustomHomeservers": false,
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
@@ -14,4 +12,4 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"basename": "/"
|
"basename": "/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-30
@@ -4,15 +4,15 @@ module.exports = {
|
|||||||
es2021: true,
|
es2021: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
'eslint:recommended',
|
||||||
"plugin:react/recommended",
|
'plugin:react/recommended',
|
||||||
"plugin:react-hooks/recommended",
|
'plugin:react-hooks/recommended',
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
"plugin:@typescript-eslint/recommended",
|
'plugin:@typescript-eslint/recommended',
|
||||||
'airbnb',
|
'airbnb',
|
||||||
'prettier',
|
'prettier',
|
||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
@@ -20,46 +20,40 @@ module.exports = {
|
|||||||
ecmaVersion: 'latest',
|
ecmaVersion: 'latest',
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
"globals": {
|
globals: {
|
||||||
JSX: "readonly"
|
JSX: 'readonly',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: ['react', '@typescript-eslint'],
|
||||||
'react',
|
|
||||||
'@typescript-eslint'
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
'linebreak-style': 0,
|
'linebreak-style': 0,
|
||||||
'no-underscore-dangle': 0,
|
'no-underscore-dangle': 0,
|
||||||
"no-shadow": "off",
|
'no-shadow': 'off',
|
||||||
|
|
||||||
"import/prefer-default-export": "off",
|
'import/prefer-default-export': 'off',
|
||||||
"import/extensions": "off",
|
'import/extensions': 'off',
|
||||||
"import/no-unresolved": "off",
|
'import/no-unresolved': 'off',
|
||||||
"import/no-extraneous-dependencies": [
|
'import/no-extraneous-dependencies': [
|
||||||
"error",
|
'error',
|
||||||
{
|
{
|
||||||
devDependencies: true,
|
devDependencies: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
'react/no-unstable-nested-components': [
|
'react/no-unstable-nested-components': ['error', { allowAsProps: true }],
|
||||||
|
'react/jsx-filename-extension': [
|
||||||
'error',
|
'error',
|
||||||
{ allowAsProps: true },
|
|
||||||
],
|
|
||||||
"react/jsx-filename-extension": [
|
|
||||||
"error",
|
|
||||||
{
|
{
|
||||||
extensions: [".tsx", ".jsx"],
|
extensions: ['.tsx', '.jsx'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
"react/require-default-props": "off",
|
'react/require-default-props': 'off',
|
||||||
"react/jsx-props-no-spreading": "off",
|
'react/jsx-props-no-spreading': 'off',
|
||||||
"react-hooks/rules-of-hooks": "error",
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
"react-hooks/exhaustive-deps": "error",
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
"@typescript-eslint/no-shadow": "error"
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
SENTRY_AUTH_TOKEN: ""
|
SENTRY_AUTH_TOKEN: ""
|
||||||
|
VITE_APP_VERSION: ${{ github.sha }}
|
||||||
|
|
||||||
# ── Quality checks (informational — pre-existing issues exist) ───────
|
# ── Quality checks (informational — pre-existing issues exist) ───────
|
||||||
- name: TypeScript
|
- name: TypeScript
|
||||||
@@ -41,7 +42,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
run: npm run check:prettier
|
run: npm run check:prettier
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
# ── Security ─────────────────────────────────────────────────────────
|
# ── Security ─────────────────────────────────────────────────────────
|
||||||
- name: Audit (high/critical)
|
- name: Audit (high/critical)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
labels: ["needs-confirmation"]
|
labels: ['needs-confirmation']
|
||||||
body:
|
body:
|
||||||
- type: markdown #add faqs in future
|
- type: markdown #add faqs in future
|
||||||
attributes:
|
attributes:
|
||||||
@@ -7,7 +7,7 @@ body:
|
|||||||
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
|
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: "# Issue Details"
|
value: '# Issue Details'
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue Description
|
label: Issue Description
|
||||||
@@ -64,7 +64,7 @@ body:
|
|||||||
- Browser:
|
- Browser:
|
||||||
- Cinny Web Version: (app.cinny.in or self hosted)
|
- Cinny Web Version: (app.cinny.in or self hosted)
|
||||||
- Cinny desktop Version: (appimage or deb or flatpak)
|
- Cinny desktop Version: (appimage or deb or flatpak)
|
||||||
- Matrix Homeserver:
|
- Matrix Homeserver:
|
||||||
placeholder: |
|
placeholder: |
|
||||||
- OS: Windows 11
|
- OS: Windows 11
|
||||||
- Browser: Chrome 120.0.6099.109
|
- Browser: Chrome 120.0.6099.109
|
||||||
@@ -80,12 +80,12 @@ body:
|
|||||||
label: Relevant Logs
|
label: Relevant Logs
|
||||||
description: |
|
description: |
|
||||||
If applicable, add browser console logs to help explain your problem.
|
If applicable, add browser console logs to help explain your problem.
|
||||||
|
|
||||||
**To get browser console logs:**
|
**To get browser console logs:**
|
||||||
- Chrome/Edge: Press F12 → Console tab
|
- Chrome/Edge: Press F12 → Console tab
|
||||||
- Firefox: Press F12 → Console tab
|
- Firefox: Press F12 → Console tab
|
||||||
- Safari: Develop → Show Web Inspector → Console
|
- Safari: Develop → Show Web Inspector → Console
|
||||||
|
|
||||||
Please wrap large log outputs in code blocks with triple backticks (```).
|
Please wrap large log outputs in code blocks with triple backticks (```).
|
||||||
placeholder: |
|
placeholder: |
|
||||||
```
|
```
|
||||||
@@ -98,7 +98,7 @@ body:
|
|||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: |
|
description: |
|
||||||
@@ -119,7 +119,7 @@ body:
|
|||||||
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
|
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
|
||||||
- type: checkboxes #add faqs in future
|
- type: checkboxes #add faqs in future
|
||||||
attributes:
|
attributes:
|
||||||
label: "I acknowledge that:"
|
label: 'I acknowledge that:'
|
||||||
options:
|
options:
|
||||||
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
|
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
+14
-14
@@ -2,29 +2,29 @@
|
|||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
# - package-ecosystem: npm
|
# - package-ecosystem: npm
|
||||||
# directory: /
|
# directory: /
|
||||||
# schedule:
|
# schedule:
|
||||||
# interval: weekly
|
# interval: weekly
|
||||||
# day: "tuesday"
|
# day: "tuesday"
|
||||||
# time: "01:00"
|
# time: "01:00"
|
||||||
# timezone: "Asia/Kolkata"
|
# timezone: "Asia/Kolkata"
|
||||||
# open-pull-requests-limit: 15
|
# open-pull-requests-limit: 15
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "tuesday"
|
day: 'tuesday'
|
||||||
time: "01:00"
|
time: '01:00'
|
||||||
timezone: "Asia/Kolkata"
|
timezone: 'Asia/Kolkata'
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
|
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "tuesday"
|
day: 'tuesday'
|
||||||
time: "01:00"
|
time: '01:00'
|
||||||
timezone: "Asia/Kolkata"
|
timezone: 'Asia/Kolkata'
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version-file: '.node-version'
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Deploy PR to Netlify
|
name: Deploy PR to Netlify
|
||||||
run-name: "Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})"
|
run-name: 'Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Build pull request"]
|
workflows: ['Build pull request']
|
||||||
types: [completed]
|
types: [completed]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-pull-request:
|
deploy-pull-request:
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
deploy-message: 'Deploy PR ${{ steps.pr.outputs.id }}'
|
||||||
alias: ${{ steps.pr.outputs.id }}
|
alias: ${{ steps.pr.outputs.id }}
|
||||||
# These don't work because we're in workflow_run
|
# These don't work because we're in workflow_run
|
||||||
enable-pull-request-comment: false
|
enable-pull-request-comment: false
|
||||||
@@ -59,5 +59,5 @@ jobs:
|
|||||||
pr-number: ${{ steps.pr.outputs.id }}
|
pr-number: ${{ steps.pr.outputs.id }}
|
||||||
comment-tag: ${{ steps.pr.outputs.id }}
|
comment-tag: ${{ steps.pr.outputs.id }}
|
||||||
message: |
|
message: |
|
||||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ jobs:
|
|||||||
collapsibleThreshold: 25
|
collapsibleThreshold: 25
|
||||||
failOnDowngrade: false
|
failOnDowngrade: false
|
||||||
path: package-lock.json
|
path: package-lock.json
|
||||||
updateComment: true
|
updateComment: true
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version-file: '.node-version'
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version-file: '.node-version'
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -118,4 +118,4 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
+11
-11
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
|||||||
Examples of behavior that contributes to a positive environment for our
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
overall community
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
advances of any kind
|
advances of any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
- Publishing others' private information, such as a physical or email
|
||||||
address, without their explicit permission
|
address, without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
|||||||
+6
-3
@@ -5,6 +5,7 @@ First off, thanks for taking the time to contribute! ❤️
|
|||||||
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
||||||
|
|
||||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||||
|
>
|
||||||
> - Star the project
|
> - Star the project
|
||||||
> - Tweet about it (tag @cinnyapp)
|
> - Tweet about it (tag @cinnyapp)
|
||||||
> - Refer this project in your project's readme
|
> - Refer this project in your project's readme
|
||||||
@@ -18,6 +19,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
|||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
> ### Legal Notice
|
> ### Legal Notice
|
||||||
|
>
|
||||||
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
|
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
|
||||||
|
|
||||||
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
|
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
|
||||||
@@ -26,9 +28,9 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|Not ideal|Better|
|
| Not ideal | Better |
|
||||||
|---|----|
|
| ----------------------------------- | --------------------------------------------- |
|
||||||
|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
|
| Fixed markAllAsRead in RoomTimeline | Fix read marker when paginating room timeline |
|
||||||
|
|
||||||
It is not always possible to phrase every change in such a manner, but it is desired.
|
It is not always possible to phrase every change in such a manner, but it is desired.
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ Also, we use [ESLint](https://eslint.org/) for clean and stylistically consisten
|
|||||||
**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
|
## Helpful links
|
||||||
|
|
||||||
- [BEM methodology](http://getbem.com/introduction/)
|
- [BEM methodology](http://getbem.com/introduction/)
|
||||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||||
|
|||||||
+1
-3
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"defaultHomeserver": 0,
|
"defaultHomeserver": 0,
|
||||||
"homeserverList": [
|
"homeserverList": ["matrix.lotusguild.org"],
|
||||||
"matrix.lotusguild.org"
|
|
||||||
],
|
|
||||||
"allowCustomHomeservers": false,
|
"allowCustomHomeservers": false,
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
|
|||||||
+11
-8
@@ -11,15 +11,15 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta name="keywords" content="lotus chat, lotus guild, matrix, matrix client" />
|
||||||
name="keywords"
|
|
||||||
content="lotus chat, lotus guild, matrix, matrix client"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content="Lotus Chat" />
|
<meta property="og:title" content="Lotus Chat" />
|
||||||
<meta property="og:url" content="https://chat.lotusguild.org" />
|
<meta property="og:url" content="https://chat.lotusguild.org" />
|
||||||
<meta property="og:image" content="https://chat.lotusguild.org/public/res/android/android-chrome-192x192.png" />
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://chat.lotusguild.org/public/res/android/android-chrome-192x192.png"
|
||||||
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
||||||
@@ -27,9 +27,12 @@
|
|||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|||||||
Generated
+386
-389
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -69,7 +69,7 @@
|
|||||||
"@fontsource/inter": "4.5.14",
|
"@fontsource/inter": "4.5.14",
|
||||||
"@giphy/js-fetch-api": "5.8.0",
|
"@giphy/js-fetch-api": "5.8.0",
|
||||||
"@giphy/js-types": "5.1.0",
|
"@giphy/js-types": "5.1.0",
|
||||||
"@giphy/react-components": "10.1.2",
|
"@giphy/react-components": "1.6.0",
|
||||||
"@sentry/react": "10.53.1",
|
"@sentry/react": "10.53.1",
|
||||||
"@tanstack/react-query": "5.24.1",
|
"@tanstack/react-query": "5.24.1",
|
||||||
"@tanstack/react-query-devtools": "5.24.1",
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
@@ -162,6 +162,6 @@
|
|||||||
"vite": "6.4.2",
|
"vite": "6.4.2",
|
||||||
"vite-plugin-pwa": "1.3.0",
|
"vite-plugin-pwa": "1.3.0",
|
||||||
"vite-plugin-static-copy": "4.1.0",
|
"vite-plugin-static-copy": "4.1.0",
|
||||||
"vite-plugin-top-level-await": "1.6.0"
|
"vite-plugin-top-level-await": "1.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"defaultHomeserver": 0,
|
"defaultHomeserver": 0,
|
||||||
"homeserverList": [
|
"homeserverList": ["matrix.lotusguild.org"],
|
||||||
"matrix.lotusguild.org"
|
|
||||||
],
|
|
||||||
"allowCustomHomeservers": false,
|
"allowCustomHomeservers": false,
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
|
|||||||
@@ -56,11 +56,7 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": [
|
"categories": ["social", "communication", "productivity"],
|
||||||
"social",
|
|
||||||
"communication",
|
|
||||||
"productivity"
|
|
||||||
],
|
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
{
|
{
|
||||||
"name": "New Message",
|
"name": "New Message",
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ const foldsPath = join(__dirname, '../node_modules/folds/dist/index.js');
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let content = readFileSync(foldsPath, 'utf8');
|
let content = readFileSync(foldsPath, 'utf8');
|
||||||
|
|
||||||
// Defensive guard: if src is not a function, render null instead of crashing
|
// Defensive guard: if src is not a function, render null instead of crashing
|
||||||
const original = 'children: src(filled)';
|
const original = 'children: src(filled)';
|
||||||
const patched = 'children: typeof src === "function" ? src(filled) : null';
|
const patched = 'children: typeof src === "function" ? src(filled) : null';
|
||||||
|
|
||||||
if (content.includes(patched)) {
|
if (content.includes(patched)) {
|
||||||
console.log('folds patch already applied.');
|
console.log('folds patch already applied.');
|
||||||
} else if (content.includes(original)) {
|
} else if (content.includes(original)) {
|
||||||
|
|||||||
+12
-12
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs";
|
import fs from 'fs';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { execSync } from "child_process";
|
import { execSync } from 'child_process';
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -9,26 +9,26 @@ const __dirname = path.dirname(__filename);
|
|||||||
const version = process.argv[2];
|
const version = process.argv[2];
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
console.error("Version argument missing");
|
console.error('Version argument missing');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = path.resolve(__dirname, "..");
|
const root = path.resolve(__dirname, '..');
|
||||||
const newVersionTag = `v${version}`;
|
const newVersionTag = `v${version}`;
|
||||||
|
|
||||||
// Update package.json + package-lock.json safely
|
// Update package.json + package-lock.json safely
|
||||||
execSync(`npm version ${version} --no-git-tag-version`, {
|
execSync(`npm version ${version} --no-git-tag-version`, {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
stdio: "inherit",
|
stdio: 'inherit',
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Updated package.json and package-lock.json → ${version}`);
|
console.log(`Updated package.json and package-lock.json → ${version}`);
|
||||||
|
|
||||||
// Update UI version references
|
// Update UI version references
|
||||||
const files = [
|
const files = [
|
||||||
"src/app/features/settings/about/About.tsx",
|
'src/app/features/settings/about/About.tsx',
|
||||||
"src/app/pages/auth/AuthFooter.tsx",
|
'src/app/pages/auth/AuthFooter.tsx',
|
||||||
"src/app/pages/client/WelcomePage.tsx",
|
'src/app/pages/client/WelcomePage.tsx',
|
||||||
];
|
];
|
||||||
|
|
||||||
files.forEach((filePath) => {
|
files.forEach((filePath) => {
|
||||||
@@ -39,10 +39,10 @@ files.forEach((filePath) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(absPath, "utf8");
|
const content = fs.readFileSync(absPath, 'utf8');
|
||||||
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
|
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
|
||||||
|
|
||||||
fs.writeFileSync(absPath, updated);
|
fs.writeFileSync(absPath, updated);
|
||||||
|
|
||||||
console.log(`Updated ${filePath} → ${newVersionTag}`);
|
console.log(`Updated ${filePath} → ${newVersionTag}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -137,7 +137,10 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const remaining = info.senderTs + info.lifetime - Date.now();
|
const remaining = info.senderTs + info.lifetime - Date.now();
|
||||||
if (remaining <= 0) { onIgnore(); return; }
|
if (remaining <= 0) {
|
||||||
|
onIgnore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const id = setTimeout(onIgnore, remaining);
|
const id = setTimeout(onIgnore, remaining);
|
||||||
return () => clearTimeout(id);
|
return () => clearTimeout(id);
|
||||||
}, [info.senderTs, info.lifetime, onIgnore]);
|
}, [info.senderTs, info.lifetime, onIgnore]);
|
||||||
@@ -157,7 +160,9 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
|||||||
<Dialog style={{ maxWidth: toRem(324) }}>
|
<Dialog style={{ maxWidth: toRem(324) }}>
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
|
||||||
<Text size="T200" align="Center">
|
<Text size="T200" align="Center">
|
||||||
{getMemberDisplayName(info.room, info.sender) ?? getMxIdLocalPart(info.sender) ?? info.sender}
|
{getMemberDisplayName(info.room, info.sender) ??
|
||||||
|
getMxIdLocalPart(info.sender) ??
|
||||||
|
info.sender}
|
||||||
</Text>
|
</Text>
|
||||||
<Box direction="Column" gap="500" alignItems="Center">
|
<Box direction="Column" gap="500" alignItems="Center">
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
@@ -294,7 +299,8 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
|||||||
const refEventId = relation?.event_id;
|
const refEventId = relation?.event_id;
|
||||||
|
|
||||||
const mention =
|
const mention =
|
||||||
content['m.mentions']?.room || content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
|
content['m.mentions']?.room ||
|
||||||
|
content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
|
||||||
if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
|
if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -410,10 +416,19 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
const pipDragRef = React.useRef<{
|
const pipDragRef = React.useRef<{
|
||||||
startX: number; startY: number; origLeft: number; origTop: number; dragged: boolean;
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
origLeft: number;
|
||||||
|
origTop: number;
|
||||||
|
dragged: boolean;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const activeDragCleanupRef = React.useRef<(() => void) | null>(null);
|
const activeDragCleanupRef = React.useRef<(() => void) | null>(null);
|
||||||
React.useEffect(() => () => { activeDragCleanupRef.current?.(); }, []);
|
React.useEffect(
|
||||||
|
() => () => {
|
||||||
|
activeDragCleanupRef.current?.();
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// Track previous pipMode to only reset position when first entering pip (not on callVisible changes)
|
// Track previous pipMode to only reset position when first entering pip (not on callVisible changes)
|
||||||
const prevPipModeRef = React.useRef(false);
|
const prevPipModeRef = React.useRef(false);
|
||||||
@@ -422,16 +437,35 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (pipMode) {
|
if (pipMode) {
|
||||||
if (!prevPipModeRef.current) {
|
if (!prevPipModeRef.current) {
|
||||||
el.style.top = 'auto'; el.style.left = 'auto';
|
el.style.top = 'auto';
|
||||||
el.style.bottom = '72px'; el.style.right = '16px';
|
el.style.left = 'auto';
|
||||||
el.style.width = '280px'; el.style.height = '158px';
|
el.style.bottom = '72px';
|
||||||
el.style.borderRadius = '12px'; el.style.overflow = 'hidden';
|
el.style.right = '16px';
|
||||||
el.style.zIndex = '99'; el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
|
el.style.width = '280px';
|
||||||
|
el.style.height = '158px';
|
||||||
|
el.style.borderRadius = '12px';
|
||||||
|
el.style.overflow = 'hidden';
|
||||||
|
el.style.zIndex = '99';
|
||||||
|
el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
|
||||||
el.style.border = '1px solid rgba(255,255,255,0.1)';
|
el.style.border = '1px solid rgba(255,255,255,0.1)';
|
||||||
}
|
}
|
||||||
el.style.visibility = 'visible';
|
el.style.visibility = 'visible';
|
||||||
} else {
|
} else {
|
||||||
['top','left','bottom','right','width','height','borderRadius','overflow','zIndex','boxShadow','border'].forEach(p => { (el.style as any)[p] = ''; });
|
[
|
||||||
|
'top',
|
||||||
|
'left',
|
||||||
|
'bottom',
|
||||||
|
'right',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'borderRadius',
|
||||||
|
'overflow',
|
||||||
|
'zIndex',
|
||||||
|
'boxShadow',
|
||||||
|
'border',
|
||||||
|
].forEach((p) => {
|
||||||
|
(el.style as any)[p] = '';
|
||||||
|
});
|
||||||
el.style.visibility = callVisible ? '' : 'hidden';
|
el.style.visibility = callVisible ? '' : 'hidden';
|
||||||
}
|
}
|
||||||
prevPipModeRef.current = pipMode;
|
prevPipModeRef.current = pipMode;
|
||||||
@@ -444,30 +478,66 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
const l = parseFloat(el.style.left);
|
const l = parseFloat(el.style.left);
|
||||||
const t = parseFloat(el.style.top);
|
const t = parseFloat(el.style.top);
|
||||||
if (!isNaN(l)) el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`;
|
if (!isNaN(l))
|
||||||
if (!isNaN(t)) el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
|
el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`;
|
||||||
|
if (!isNaN(t))
|
||||||
|
el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
|
||||||
};
|
};
|
||||||
window.addEventListener('resize', onPipWindowResize);
|
window.addEventListener('resize', onPipWindowResize);
|
||||||
return () => window.removeEventListener('resize', onPipWindowResize);
|
return () => window.removeEventListener('resize', onPipWindowResize);
|
||||||
}, [pipMode, callEmbedRef]);
|
}, [pipMode, callEmbedRef]);
|
||||||
|
|
||||||
const handlePipMouseDown = (e: React.MouseEvent) => {
|
const handlePipMouseDown = (e: React.MouseEvent) => {
|
||||||
const el = callEmbedRef.current; if (!el) return;
|
const el = callEmbedRef.current;
|
||||||
|
if (!el) return;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
pipDragRef.current = { startX: e.clientX, startY: e.clientY, origLeft: rect.left, origTop: rect.top, dragged: false };
|
pipDragRef.current = {
|
||||||
|
startX: e.clientX,
|
||||||
|
startY: e.clientY,
|
||||||
|
origLeft: rect.left,
|
||||||
|
origTop: rect.top,
|
||||||
|
dragged: false,
|
||||||
|
};
|
||||||
const onMove = (ev: MouseEvent) => {
|
const onMove = (ev: MouseEvent) => {
|
||||||
if (!pipDragRef.current || !el) return;
|
if (!pipDragRef.current || !el) return;
|
||||||
const dx = ev.clientX - pipDragRef.current.startX, dy = ev.clientY - pipDragRef.current.startY;
|
const dx = ev.clientX - pipDragRef.current.startX,
|
||||||
if (!pipDragRef.current.dragged && Math.abs(dx)+Math.abs(dy) > 5) { pipDragRef.current.dragged = true; document.body.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; }
|
dy = ev.clientY - pipDragRef.current.startY;
|
||||||
|
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5) {
|
||||||
|
pipDragRef.current.dragged = true;
|
||||||
|
document.body.style.cursor = 'grabbing';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
}
|
||||||
if (pipDragRef.current.dragged) {
|
if (pipDragRef.current.dragged) {
|
||||||
el.style.left = `${Math.max(0, Math.min(window.innerWidth-el.offsetWidth, pipDragRef.current.origLeft+dx))}px`;
|
el.style.left = `${Math.max(
|
||||||
el.style.top = `${Math.max(0, Math.min(window.innerHeight-el.offsetHeight, pipDragRef.current.origTop+dy))}px`;
|
0,
|
||||||
el.style.right = 'auto'; el.style.bottom = 'auto';
|
Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx)
|
||||||
|
)}px`;
|
||||||
|
el.style.top = `${Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy)
|
||||||
|
)}px`;
|
||||||
|
el.style.right = 'auto';
|
||||||
|
el.style.bottom = 'auto';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; activeDragCleanupRef.current = null; setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0); };
|
const onUp = () => {
|
||||||
activeDragCleanupRef.current = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; };
|
document.removeEventListener('mousemove', onMove);
|
||||||
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
activeDragCleanupRef.current = null;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pipDragRef.current) pipDragRef.current.dragged = false;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
activeDragCleanupRef.current = () => {
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePipTouchStart = (e: React.TouchEvent) => {
|
const handlePipTouchStart = (e: React.TouchEvent) => {
|
||||||
@@ -475,50 +545,115 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
if (!el || e.touches.length !== 1) return;
|
if (!el || e.touches.length !== 1) return;
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
pipDragRef.current = { startX: touch.clientX, startY: touch.clientY, origLeft: rect.left, origTop: rect.top, dragged: false };
|
pipDragRef.current = {
|
||||||
|
startX: touch.clientX,
|
||||||
|
startY: touch.clientY,
|
||||||
|
origLeft: rect.left,
|
||||||
|
origTop: rect.top,
|
||||||
|
dragged: false,
|
||||||
|
};
|
||||||
const onTouchMove = (ev: TouchEvent) => {
|
const onTouchMove = (ev: TouchEvent) => {
|
||||||
if (!pipDragRef.current || !el || ev.touches.length !== 1) return;
|
if (!pipDragRef.current || !el || ev.touches.length !== 1) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const t = ev.touches[0];
|
const t = ev.touches[0];
|
||||||
const dx = t.clientX - pipDragRef.current.startX;
|
const dx = t.clientX - pipDragRef.current.startX;
|
||||||
const dy = t.clientY - pipDragRef.current.startY;
|
const dy = t.clientY - pipDragRef.current.startY;
|
||||||
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5) pipDragRef.current.dragged = true;
|
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5)
|
||||||
|
pipDragRef.current.dragged = true;
|
||||||
if (pipDragRef.current.dragged) {
|
if (pipDragRef.current.dragged) {
|
||||||
el.style.left = `${Math.max(0, Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx))}px`;
|
el.style.left = `${Math.max(
|
||||||
el.style.top = `${Math.max(0, Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy))}px`;
|
0,
|
||||||
el.style.right = 'auto'; el.style.bottom = 'auto';
|
Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx)
|
||||||
|
)}px`;
|
||||||
|
el.style.top = `${Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy)
|
||||||
|
)}px`;
|
||||||
|
el.style.right = 'auto';
|
||||||
|
el.style.bottom = 'auto';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onTouchEnd = () => {
|
const onTouchEnd = () => {
|
||||||
document.removeEventListener('touchmove', onTouchMove);
|
document.removeEventListener('touchmove', onTouchMove);
|
||||||
document.removeEventListener('touchend', onTouchEnd);
|
document.removeEventListener('touchend', onTouchEnd);
|
||||||
activeDragCleanupRef.current = null;
|
activeDragCleanupRef.current = null;
|
||||||
setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0);
|
setTimeout(() => {
|
||||||
|
if (pipDragRef.current) pipDragRef.current.dragged = false;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
activeDragCleanupRef.current = () => {
|
||||||
|
document.removeEventListener('touchmove', onTouchMove);
|
||||||
|
document.removeEventListener('touchend', onTouchEnd);
|
||||||
};
|
};
|
||||||
activeDragCleanupRef.current = () => { document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); };
|
|
||||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||||
document.addEventListener('touchend', onTouchEnd);
|
document.addEventListener('touchend', onTouchEnd);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
|
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
|
||||||
e.stopPropagation(); e.preventDefault();
|
e.stopPropagation();
|
||||||
const el = callEmbedRef.current; if (!el) return;
|
e.preventDefault();
|
||||||
|
const el = callEmbedRef.current;
|
||||||
|
if (!el) return;
|
||||||
normaliseToTopLeft(el);
|
normaliseToTopLeft(el);
|
||||||
const sx = e.clientX, sy = e.clientY, sw = el.offsetWidth, sh = el.offsetHeight;
|
const sx = e.clientX,
|
||||||
const sl = parseFloat(el.style.left), st = parseFloat(el.style.top);
|
sy = e.clientY,
|
||||||
document.body.style.cursor = `${corner}-resize`; document.body.style.userSelect = 'none';
|
sw = el.offsetWidth,
|
||||||
|
sh = el.offsetHeight;
|
||||||
|
const sl = parseFloat(el.style.left),
|
||||||
|
st = parseFloat(el.style.top);
|
||||||
|
document.body.style.cursor = `${corner}-resize`;
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
const onMove = (ev: MouseEvent) => {
|
const onMove = (ev: MouseEvent) => {
|
||||||
const dx = ev.clientX-sx, dy = ev.clientY-sy;
|
const dx = ev.clientX - sx,
|
||||||
let w = sw, h = sh, l = sl, t = st;
|
dy = ev.clientY - sy;
|
||||||
if (corner==='se'){w=sw+dx;h=sh+dy;} if (corner==='sw'){w=sw-dx;h=sh+dy;l=sl+sw-Math.max(PIP_MIN_W,w);}
|
let w = sw,
|
||||||
if (corner==='ne'){w=sw+dx;h=sh-dy;t=st+sh-Math.max(PIP_MIN_H,h);} if (corner==='nw'){w=sw-dx;h=sh-dy;l=sl+sw-Math.max(PIP_MIN_W,w);t=st+sh-Math.max(PIP_MIN_H,h);}
|
h = sh,
|
||||||
w=Math.max(PIP_MIN_W,Math.min(w,window.innerWidth)); h=Math.max(PIP_MIN_H,Math.min(h,window.innerHeight));
|
l = sl,
|
||||||
l=Math.max(0,Math.min(l,window.innerWidth-PIP_MIN_W)); t=Math.max(0,Math.min(t,window.innerHeight-PIP_MIN_H));
|
t = st;
|
||||||
el.style.width=`${w}px`; el.style.height=`${h}px`; el.style.left=`${l}px`; el.style.top=`${t}px`;
|
if (corner === 'se') {
|
||||||
|
w = sw + dx;
|
||||||
|
h = sh + dy;
|
||||||
|
}
|
||||||
|
if (corner === 'sw') {
|
||||||
|
w = sw - dx;
|
||||||
|
h = sh + dy;
|
||||||
|
l = sl + sw - Math.max(PIP_MIN_W, w);
|
||||||
|
}
|
||||||
|
if (corner === 'ne') {
|
||||||
|
w = sw + dx;
|
||||||
|
h = sh - dy;
|
||||||
|
t = st + sh - Math.max(PIP_MIN_H, h);
|
||||||
|
}
|
||||||
|
if (corner === 'nw') {
|
||||||
|
w = sw - dx;
|
||||||
|
h = sh - dy;
|
||||||
|
l = sl + sw - Math.max(PIP_MIN_W, w);
|
||||||
|
t = st + sh - Math.max(PIP_MIN_H, h);
|
||||||
|
}
|
||||||
|
w = Math.max(PIP_MIN_W, Math.min(w, window.innerWidth));
|
||||||
|
h = Math.max(PIP_MIN_H, Math.min(h, window.innerHeight));
|
||||||
|
l = Math.max(0, Math.min(l, window.innerWidth - PIP_MIN_W));
|
||||||
|
t = Math.max(0, Math.min(t, window.innerHeight - PIP_MIN_H));
|
||||||
|
el.style.width = `${w}px`;
|
||||||
|
el.style.height = `${h}px`;
|
||||||
|
el.style.left = `${l}px`;
|
||||||
|
el.style.top = `${t}px`;
|
||||||
};
|
};
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; activeDragCleanupRef.current = null; };
|
const onUp = () => {
|
||||||
activeDragCleanupRef.current = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; };
|
document.removeEventListener('mousemove', onMove);
|
||||||
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
activeDragCleanupRef.current = null;
|
||||||
|
};
|
||||||
|
activeDragCleanupRef.current = () => {
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -548,25 +683,71 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
aria-label="Return to call"
|
aria-label="Return to call"
|
||||||
onMouseDown={handlePipMouseDown}
|
onMouseDown={handlePipMouseDown}
|
||||||
onTouchStart={handlePipTouchStart}
|
onTouchStart={handlePipTouchStart}
|
||||||
onClick={() => { if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId); }}
|
onClick={() => {
|
||||||
|
if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId);
|
||||||
|
}}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
|
onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
|
||||||
style={{ position:'absolute', inset:0, zIndex:1, background:'transparent', cursor:'grab', display:'flex', alignItems:'flex-start', justifyContent:'flex-end', padding:'6px' }}
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
background: 'transparent',
|
||||||
|
cursor: 'grab',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: '6px',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ background:'rgba(0,0,0,0.65)', backdropFilter:'blur(4px)', borderRadius:'6px', padding:'3px 8px', color:'#fff', fontSize:'11px', fontWeight:600, pointerEvents:'none' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'rgba(0,0,0,0.65)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '3px 8px',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 600,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
↗ Return to call
|
↗ Return to call
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(['se','sw','ne','nw'] as Corner[]).map((corner) => {
|
{(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
|
||||||
const s = corner.includes('s'); const e2 = corner.includes('e');
|
const s = corner.includes('s');
|
||||||
const dots = [[2,2],[2,7],[7,2]].map(([a,b]) => ({
|
const e2 = corner.includes('e');
|
||||||
position:'absolute' as const, width:4, height:4, borderRadius:'50%',
|
const dots = [
|
||||||
background:'rgba(255,255,255,0.45)',
|
[2, 2],
|
||||||
[s?'bottom':'top']:a, [e2?'right':'left']:b,
|
[2, 7],
|
||||||
|
[7, 2],
|
||||||
|
].map(([a, b]) => ({
|
||||||
|
position: 'absolute' as const,
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'rgba(255,255,255,0.45)',
|
||||||
|
[s ? 'bottom' : 'top']: a,
|
||||||
|
[e2 ? 'right' : 'left']: b,
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
<div key={corner} onMouseDown={(ev) => handleResizeMouseDown(ev, corner)} onClick={(ev) => ev.stopPropagation()}
|
<div
|
||||||
style={{ position:'absolute', width:'18px', height:'18px', [s?'bottom':'top']:0, [e2?'right':'left']:0, cursor:`${corner}-resize`, zIndex:2 }}>
|
key={corner}
|
||||||
{dots.map((style, i) => <div key={i} style={style} />)}
|
onMouseDown={(ev) => handleResizeMouseDown(ev, corner)}
|
||||||
|
onClick={(ev) => ev.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: '18px',
|
||||||
|
height: '18px',
|
||||||
|
[s ? 'bottom' : 'top']: 0,
|
||||||
|
[e2 ? 'right' : 'left']: 0,
|
||||||
|
cursor: `${corner}-resize`,
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dots.map((style, i) => (
|
||||||
|
<div key={i} style={style} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -259,9 +259,16 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
|
|||||||
<Dialog variant="Surface">
|
<Dialog variant="Surface">
|
||||||
<Header style={DialogHeaderStyles} variant="Surface" size="500">
|
<Header style={DialogHeaderStyles} variant="Surface" size="500">
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Device Verification</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Device Verification
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={handleCancel} aria-label="Cancel verification">
|
<IconButton
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleCancel}
|
||||||
|
aria-label="Cancel verification"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -299,7 +299,9 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Setup Device Verification</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Setup Device Verification
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
@@ -334,7 +336,9 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Reset Device Verification</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Reset Device Verification
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { settingsAtom } from '../state/settings';
|
|||||||
|
|
||||||
const PICKER_WIDTH = 312;
|
const PICKER_WIDTH = 312;
|
||||||
|
|
||||||
|
|
||||||
type GifPickerInnerProps = {
|
type GifPickerInnerProps = {
|
||||||
onSelect: (url: string, width: number, height: number) => void;
|
onSelect: (url: string, width: number, height: number) => void;
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
@@ -34,23 +33,27 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
|
|||||||
return (
|
return (
|
||||||
<Box direction="Column" style={{ width: `${PICKER_WIDTH}px` }}>
|
<Box direction="Column" style={{ width: `${PICKER_WIDTH}px` }}>
|
||||||
{lotusTerminal && (
|
{lotusTerminal && (
|
||||||
<div style={{
|
<div
|
||||||
padding: '5px 10px 4px',
|
style={{
|
||||||
borderBottom: '1px solid rgba(255,107,0,0.2)',
|
padding: '5px 10px 4px',
|
||||||
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
|
borderBottom: '1px solid rgba(255,107,0,0.2)',
|
||||||
fontSize: '10px',
|
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
|
||||||
fontWeight: 700,
|
fontSize: '10px',
|
||||||
letterSpacing: '0.1em',
|
fontWeight: 700,
|
||||||
color: '#FF6B00',
|
letterSpacing: '0.1em',
|
||||||
userSelect: 'none',
|
color: '#FF6B00',
|
||||||
}}>
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
// GIF_SEARCH
|
// GIF_SEARCH
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Box style={{ padding: '8px 8px 4px' }}>
|
<Box style={{ padding: '8px 8px 4px' }}>
|
||||||
<SearchBar style={{ width: '100%', borderRadius: lotusTerminal ? '4px' : '8px' }} />
|
<SearchBar style={{ width: '100%', borderRadius: lotusTerminal ? '4px' : '8px' }} />
|
||||||
</Box>
|
</Box>
|
||||||
<div style={{ overflowY: 'auto', overflowX: 'hidden', maxHeight: '340px', padding: '0 8px 8px' }}>
|
<div
|
||||||
|
style={{ overflowY: 'auto', overflowX: 'hidden', maxHeight: '340px', padding: '0 8px 8px' }}
|
||||||
|
>
|
||||||
<Grid
|
<Grid
|
||||||
key={searchKey}
|
key={searchKey}
|
||||||
fetchGifs={fetchGifs}
|
fetchGifs={fetchGifs}
|
||||||
@@ -108,7 +111,11 @@ export function GifPicker({ apiKey, onSelect, requestClose }: GifPickerProps) {
|
|||||||
style={containerStyle}
|
style={containerStyle}
|
||||||
>
|
>
|
||||||
<SearchContextManager apiKey={apiKey} initialTerm="">
|
<SearchContextManager apiKey={apiKey} initialTerm="">
|
||||||
<GifPickerInner onSelect={onSelect} requestClose={requestClose} lotusTerminal={!!lotusTerminal} />
|
<GifPickerInner
|
||||||
|
onSelect={onSelect}
|
||||||
|
requestClose={requestClose}
|
||||||
|
lotusTerminal={!!lotusTerminal}
|
||||||
|
/>
|
||||||
</SearchContextManager>
|
</SearchContextManager>
|
||||||
</Box>
|
</Box>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Logout</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Logout
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Header>
|
</Header>
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export function RoomSkeleton() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const shimmer = {
|
const shimmer = {
|
||||||
background: 'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
|
background:
|
||||||
|
'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
|
||||||
backgroundSize: '800px 100%',
|
backgroundSize: '800px 100%',
|
||||||
animation: `shimmer-${id} 1.6s ease-in-out infinite`,
|
animation: `shimmer-${id} 1.6s ease-in-out infinite`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
@@ -32,16 +33,18 @@ export function RoomSkeleton() {
|
|||||||
<>
|
<>
|
||||||
<style>{shimmerKeyframes}</style>
|
<style>{shimmerKeyframes}</style>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
display: 'flex',
|
{
|
||||||
flexDirection: 'column',
|
display: 'flex',
|
||||||
flexGrow: 1,
|
flexDirection: 'column',
|
||||||
height: '100%',
|
flexGrow: 1,
|
||||||
overflow: 'hidden',
|
height: '100%',
|
||||||
// CSS vars resolve against both light and dark themes automatically
|
overflow: 'hidden',
|
||||||
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
// CSS vars resolve against both light and dark themes automatically
|
||||||
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||||
} as React.CSSProperties}
|
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Header — matches PageHeader size="600" (56px) */}
|
{/* Header — matches PageHeader size="600" (56px) */}
|
||||||
<div
|
<div
|
||||||
@@ -56,7 +59,15 @@ export function RoomSkeleton() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div style={{ ...shimmer, width: '32px', height: '32px', borderRadius: '50%', flexShrink: 0 }} />
|
<div
|
||||||
|
style={{
|
||||||
|
...shimmer,
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/* Room name */}
|
{/* Room name */}
|
||||||
<div style={{ ...shimmer, width: '140px', height: '16px' }} />
|
<div style={{ ...shimmer, width: '140px', height: '16px' }} />
|
||||||
{/* Spacer */}
|
{/* Spacer */}
|
||||||
@@ -70,7 +81,16 @@ export function RoomSkeleton() {
|
|||||||
<div style={{ flex: 1, overflowY: 'hidden', padding: '16px 0' }}>
|
<div style={{ flex: 1, overflowY: 'hidden', padding: '16px 0' }}>
|
||||||
{MESSAGES.map((msg, i) => (
|
{MESSAGES.map((msg, i) => (
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={i} style={{ display: 'flex', gap: '12px', padding: '4px 16px', alignItems: 'flex-start', marginBottom: msg.showAvatar ? '8px' : '2px' }}>
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '4px 16px',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: msg.showAvatar ? '8px' : '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Avatar — only shown on first message in a group */}
|
{/* Avatar — only shown on first message in a group */}
|
||||||
<div style={{ width: '36px', flexShrink: 0 }}>
|
<div style={{ width: '36px', flexShrink: 0 }}>
|
||||||
{msg.showAvatar && (
|
{msg.showAvatar && (
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ export function EditorPreview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton variant="SurfaceVariant" aria-label="Open editor preview" onClick={() => setOpen(!open)}>
|
<IconButton
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
aria-label="Open editor preview"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
<Icon src={Icons.BlockQuote} />
|
<Icon src={Icons.BlockQuote} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Overlay open={open} backdrop={<OverlayBackdrop />}>
|
<Overlay open={open} backdrop={<OverlayBackdrop />}>
|
||||||
@@ -58,7 +62,12 @@ export function EditorPreview() {
|
|||||||
>
|
>
|
||||||
<Icon src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} />
|
<Icon src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton variant="SurfaceVariant" size="300" radii="300" aria-label="Insert emoji">
|
<IconButton
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
aria-label="Insert emoji"
|
||||||
|
>
|
||||||
<Icon src={Icons.Smile} />
|
<Icon src={Icons.Smile} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton variant="SurfaceVariant" size="300" radii="300" aria-label="Send">
|
<IconButton variant="SurfaceVariant" size="300" radii="300" aria-label="Send">
|
||||||
|
|||||||
@@ -279,44 +279,42 @@ export function Toolbar() {
|
|||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Bold}
|
format={MarkType.Bold}
|
||||||
icon={Icons.Bold}
|
icon={Icons.Bold}
|
||||||
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`}
|
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} label="Bold" />}
|
||||||
label="Bold"
|
|
||||||
/>}
|
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Italic}
|
format={MarkType.Italic}
|
||||||
icon={Icons.Italic}
|
icon={Icons.Italic}
|
||||||
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`}
|
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} label="Italic" />}
|
||||||
label="Italic"
|
|
||||||
/>}
|
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Underline}
|
format={MarkType.Underline}
|
||||||
icon={Icons.Underline}
|
icon={Icons.Underline}
|
||||||
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`}
|
tooltip={
|
||||||
label="Underline"
|
<BtnTooltip text="Underline" shortCode={`${modKey} + U`} label="Underline" />
|
||||||
/>}
|
}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.StrikeThrough}
|
format={MarkType.StrikeThrough}
|
||||||
icon={Icons.Strike}
|
icon={Icons.Strike}
|
||||||
tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`}
|
tooltip={
|
||||||
label="Strikethrough"
|
<BtnTooltip
|
||||||
/>}
|
text="Strike Through"
|
||||||
|
shortCode={`${modKey} + S`}
|
||||||
|
label="Strikethrough"
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Code}
|
format={MarkType.Code}
|
||||||
icon={Icons.Code}
|
icon={Icons.Code}
|
||||||
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`}
|
tooltip={
|
||||||
label="Inline code"
|
<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} label="Inline code" />
|
||||||
/>}
|
}
|
||||||
/>
|
/>
|
||||||
<MarkButton
|
<MarkButton
|
||||||
format={MarkType.Spoiler}
|
format={MarkType.Spoiler}
|
||||||
icon={Icons.EyeBlind}
|
icon={Icons.EyeBlind}
|
||||||
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`}
|
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} label="Spoiler" />}
|
||||||
label="Spoiler"
|
|
||||||
/>}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
||||||
@@ -325,30 +323,34 @@ export function Toolbar() {
|
|||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.BlockQuote}
|
format={BlockType.BlockQuote}
|
||||||
icon={Icons.BlockQuote}
|
icon={Icons.BlockQuote}
|
||||||
tooltip={<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`}
|
tooltip={
|
||||||
label="Block quote"
|
<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`} label="Block quote" />
|
||||||
/>}
|
}
|
||||||
/>
|
/>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.CodeBlock}
|
format={BlockType.CodeBlock}
|
||||||
icon={Icons.BlockCode}
|
icon={Icons.BlockCode}
|
||||||
tooltip={<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`}
|
tooltip={
|
||||||
label="Code block"
|
<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`} label="Code block" />
|
||||||
/>}
|
}
|
||||||
/>
|
/>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.OrderedList}
|
format={BlockType.OrderedList}
|
||||||
icon={Icons.OrderList}
|
icon={Icons.OrderList}
|
||||||
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`}
|
tooltip={
|
||||||
label="Ordered list"
|
<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} label="Ordered list" />
|
||||||
/>}
|
}
|
||||||
/>
|
/>
|
||||||
<BlockButton
|
<BlockButton
|
||||||
format={BlockType.UnorderedList}
|
format={BlockType.UnorderedList}
|
||||||
icon={Icons.UnorderList}
|
icon={Icons.UnorderList}
|
||||||
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`}
|
tooltip={
|
||||||
label="Unordered list"
|
<BtnTooltip
|
||||||
/>}
|
text="Unordered List"
|
||||||
|
shortCode={`${modKey} + 8`}
|
||||||
|
label="Unordered list"
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<HeadingBlockButton />
|
<HeadingBlockButton />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -68,10 +68,30 @@ export const EventReaders = as<'div', EventReadersProps>(
|
|||||||
className={css.Header}
|
className={css.Header}
|
||||||
variant="Surface"
|
variant="Surface"
|
||||||
size="600"
|
size="600"
|
||||||
style={lotusTerminal ? { borderBottom: '1px solid rgba(0,212,255,0.30)', boxShadow: '0 2px 12px rgba(0,212,255,0.08)' } : undefined}
|
style={
|
||||||
|
lotusTerminal
|
||||||
|
? {
|
||||||
|
borderBottom: '1px solid rgba(0,212,255,0.30)',
|
||||||
|
boxShadow: '0 2px 12px rgba(0,212,255,0.08)',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H3" style={lotusTerminal ? { color: '#00D4FF', textShadow: '0 0 6px rgba(0,212,255,0.45)', letterSpacing: '0.05em' } : undefined}>Seen by</Text>
|
<Text
|
||||||
|
size="H3"
|
||||||
|
style={
|
||||||
|
lotusTerminal
|
||||||
|
? {
|
||||||
|
color: '#00D4FF',
|
||||||
|
textShadow: '0 0 6px rgba(0,212,255,0.45)',
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Seen by
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={requestClose} aria-label="Close">
|
<IconButton size="300" onClick={requestClose} aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
@@ -120,9 +140,15 @@ export const EventReaders = as<'div', EventReadersProps>(
|
|||||||
{receiptTs !== undefined && (
|
{receiptTs !== undefined && (
|
||||||
<Text
|
<Text
|
||||||
size="T200"
|
size="T200"
|
||||||
style={lotusTerminal
|
style={
|
||||||
? { color: '#FFB300', textShadow: '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)', fontSize: '0.72rem' }
|
lotusTerminal
|
||||||
: { opacity: 0.6 }}
|
? {
|
||||||
|
color: '#FFB300',
|
||||||
|
textShadow: '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)',
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
}
|
||||||
|
: { opacity: 0.6 }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{formatReadTs(receiptTs, hour24Clock)}
|
{formatReadTs(receiptTs, hour24Clock)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) {
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Join with Address</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Join with Address
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4" id="leave-room-dialog-title">Leave Room</Text>
|
<Text as="h2" size="H4" id="leave-room-dialog-title">
|
||||||
|
Leave Room
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Leave Space</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Leave Space
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -48,7 +48,13 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
|
|||||||
variant={hasError ? 'Critical' : 'SurfaceVariant'}
|
variant={hasError ? 'Critical' : 'SurfaceVariant'}
|
||||||
size="300"
|
size="300"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-label={downloading ? 'Downloading...' : hasError ? 'Download failed, click to retry' : 'Download file'}
|
aria-label={
|
||||||
|
downloading
|
||||||
|
? 'Downloading...'
|
||||||
|
: hasError
|
||||||
|
? 'Download failed, click to retry'
|
||||||
|
: 'Download file'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{downloading ? (
|
{downloading ? (
|
||||||
<Spinner size="100" variant={hasError ? 'Critical' : 'Secondary'} />
|
<Spinner size="100" variant={hasError ? 'Critical' : 'Secondary'} />
|
||||||
|
|||||||
@@ -394,7 +394,9 @@ export function MLocation({ content }: MLocationProps) {
|
|||||||
const lat = parseFloat(location.latitude);
|
const lat = parseFloat(location.latitude);
|
||||||
const lon = parseFloat(location.longitude);
|
const lon = parseFloat(location.longitude);
|
||||||
if (!isFinite(lat) || !isFinite(lon)) return <BrokenContent />;
|
if (!isFinite(lat) || !isFinite(lon)) return <BrokenContent />;
|
||||||
const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${lat - 0.004},${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`;
|
const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${
|
||||||
|
lat - 0.004
|
||||||
|
},${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" alignItems="Start" gap="200">
|
<Box direction="Column" alignItems="Start" gap="200">
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ export const Reaction = as<
|
|||||||
{reaction.startsWith('mxc://') ? (
|
{reaction.startsWith('mxc://') ? (
|
||||||
<img
|
<img
|
||||||
className={css.ReactionImg}
|
className={css.ReactionImg}
|
||||||
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
|
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction}
|
||||||
}
|
|
||||||
alt={reaction}
|
alt={reaction}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export const MessageDeletedContent = as<'div', { children?: never; reason?: stri
|
|||||||
({ reason, ...props }, ref) => (
|
({ reason, ...props }, ref) => (
|
||||||
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
|
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
|
||||||
<Icon size="50" src={Icons.Delete} />
|
<Icon size="50" src={Icons.Delete} />
|
||||||
<i>{reason ? `This message has been deleted — ${reason}` : 'This message has been deleted'}</i>
|
<i>
|
||||||
|
{reason ? `This message has been deleted — ${reason}` : 'This message has been deleted'}
|
||||||
|
</i>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ export function PollContent({
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const isStable = !!content['m.poll'];
|
const isStable = !!content['m.poll'];
|
||||||
|
|
||||||
const poll = (
|
const poll = (content['m.poll'] ?? content['org.matrix.msc3381.poll.start']) as
|
||||||
content['m.poll'] ?? content['org.matrix.msc3381.poll.start']
|
| PollData
|
||||||
) as PollData | undefined;
|
| undefined;
|
||||||
|
|
||||||
const [votes, setVotes] = useState<VoteState>(() => {
|
const [votes, setVotes] = useState<VoteState>(() => {
|
||||||
if (!roomId || !eventId) return { counts: new Map(), myVote: null, total: 0 };
|
if (!roomId || !eventId) return { counts: new Map(), myVote: null, total: 0 };
|
||||||
@@ -259,7 +259,9 @@ export function PollContent({
|
|||||||
padding: '7px 12px',
|
padding: '7px 12px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
background: selected ? 'var(--bg-surface-active)' : 'var(--bg-surface-low)',
|
background: selected ? 'var(--bg-surface-active)' : 'var(--bg-surface-low)',
|
||||||
border: `1px solid ${selected ? 'var(--text-primary)' : 'var(--bg-surface-border)'}`,
|
border: `1px solid ${
|
||||||
|
selected ? 'var(--text-primary)' : 'var(--bg-surface-border)'
|
||||||
|
}`,
|
||||||
fontSize: '0.88rem',
|
fontSize: '0.88rem',
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
@@ -281,23 +283,21 @@ export function PollContent({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
inset: 0,
|
inset: 0,
|
||||||
width: `${pct}%`,
|
width: `${pct}%`,
|
||||||
background: selected
|
background: selected ? 'rgba(255,255,255,0.10)' : 'rgba(255,255,255,0.05)',
|
||||||
? 'rgba(255,255,255,0.10)'
|
|
||||||
: 'rgba(255,255,255,0.05)',
|
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: '8px', position: 'relative' }}>
|
<span
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '8px', position: 'relative' }}
|
||||||
|
>
|
||||||
<span style={{ flexGrow: 1 }}>{text}</span>
|
<span style={{ flexGrow: 1 }}>{text}</span>
|
||||||
{selected && (
|
{selected && (
|
||||||
<span style={{ opacity: 0.8, fontSize: '1rem', flexShrink: 0 }}>✓</span>
|
<span style={{ opacity: 0.8, fontSize: '1rem', flexShrink: 0 }}>✓</span>
|
||||||
)}
|
)}
|
||||||
{total > 0 && (
|
{total > 0 && (
|
||||||
<span style={{ opacity: 0.6, fontSize: '0.78rem', flexShrink: 0 }}>
|
<span style={{ opacity: 0.6, fontSize: '0.78rem', flexShrink: 0 }}>{pct}%</span>
|
||||||
{pct}%
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ export function ReadReceiptAvatars({
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: lotusTerminal ? 'rgba(0,212,255,0.07)' : color.SurfaceVariant.Container,
|
backgroundColor: lotusTerminal
|
||||||
|
? 'rgba(0,212,255,0.07)'
|
||||||
|
: color.SurfaceVariant.Container,
|
||||||
border: lotusTerminal ? '1px solid rgba(0,212,255,0.30)' : '1px solid transparent',
|
border: lotusTerminal ? '1px solid rgba(0,212,255,0.30)' : '1px solid transparent',
|
||||||
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
|
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
|
||||||
borderRadius: '999px',
|
borderRadius: '999px',
|
||||||
@@ -88,8 +90,7 @@ export function ReadReceiptAvatars({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{displayed.map((userId) => {
|
{displayed.map((userId) => {
|
||||||
const name =
|
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
||||||
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
|
||||||
const avatarMxc = room.getMember(userId)?.getMxcAvatarUrl();
|
const avatarMxc = room.getMember(userId)?.getMxcAvatarUrl();
|
||||||
const avatarUrl = avatarMxc
|
const avatarUrl = avatarMxc
|
||||||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 32, 32, 'crop') ?? undefined
|
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 32, 32, 'crop') ?? undefined
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ function DummyErrorDialog({
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text as="h2" size="H4">{title}</Text>
|
<Text as="h2" size="H4">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<Text>{message}</Text>
|
<Text>{message}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Button variant="Critical" onClick={onRetry}>
|
<Button variant="Critical" onClick={onRetry}>
|
||||||
|
|||||||
@@ -37,9 +37,16 @@ function EmailErrorDialog({
|
|||||||
gap="400"
|
gap="400"
|
||||||
>
|
>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text as="h2" size="H4">{title}</Text>
|
<Text as="h2" size="H4">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<Text>{message}</Text>
|
<Text>{message}</Text>
|
||||||
<Text as="label" htmlFor="retryEmailInput" size="L400" style={{ paddingTop: config.space.S400 }}>
|
<Text
|
||||||
|
as="label"
|
||||||
|
htmlFor="retryEmailInput"
|
||||||
|
size="L400"
|
||||||
|
style={{ paddingTop: config.space.S400 }}
|
||||||
|
>
|
||||||
Email
|
Email
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
@@ -141,7 +148,9 @@ export function EmailStageDialog({
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text as="h2" size="H4">Verification Request Sent</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Verification Request Sent
|
||||||
|
</Text>
|
||||||
<Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
|
<Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
|
||||||
|
|
||||||
{errorCode && (
|
{errorCode && (
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ export function PasswordStage({
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Account Password</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Account Password
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -35,9 +35,16 @@ function RegistrationTokenErrorDialog({
|
|||||||
gap="400"
|
gap="400"
|
||||||
>
|
>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text as="h2" size="H4">{title}</Text>
|
<Text as="h2" size="H4">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<Text>{message}</Text>
|
<Text>{message}</Text>
|
||||||
<Text as="label" htmlFor="retryTokenInput" size="L400" style={{ paddingTop: config.space.S400 }}>
|
<Text
|
||||||
|
as="label"
|
||||||
|
htmlFor="retryTokenInput"
|
||||||
|
size="L400"
|
||||||
|
style={{ paddingTop: config.space.S400 }}
|
||||||
|
>
|
||||||
Registration Token
|
Registration Token
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ export function SSOStage({
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">SSO Login</Text>
|
<Text as="h2" size="H4">
|
||||||
|
SSO Login
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ function TermsErrorDialog({
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text as="h2" size="H4">{title}</Text>
|
<Text as="h2" size="H4">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<Text>{message}</Text>
|
<Text>{message}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Button variant="Critical" onClick={onRetry}>
|
<Button variant="Critical" onClick={onRetry}>
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { Box, as } from 'folds';
|
|||||||
import * as css from './UrlPreview.css';
|
import * as css from './UrlPreview.css';
|
||||||
|
|
||||||
export const UrlPreview = as<'div'>(({ className, ...props }, ref) => (
|
export const UrlPreview = as<'div'>(({ className, ...props }, ref) => (
|
||||||
<Box shrink="No" data-url-preview="" className={classNames(css.UrlPreview, className)} {...props} ref={ref} />
|
<Box
|
||||||
|
shrink="No"
|
||||||
|
data-url-preview=""
|
||||||
|
className={classNames(css.UrlPreview, className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const UrlPreviewImg = as<'img'>(({ className, alt, ...props }, ref) => (
|
export const UrlPreviewImg = as<'img'>(({ className, alt, ...props }, ref) => (
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ function SelfDemoteAlert({ power, onCancel, onChange }: SelfDemoteAlertProps) {
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Self Demotion</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Self Demotion
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
@@ -118,7 +120,9 @@ function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps)
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Shared Power</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Shared Power
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -198,7 +198,9 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Add Existing</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Add Existing
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton size="300" radii="300" onClick={requestClose} aria-label="Close">
|
<IconButton size="300" radii="300" onClick={requestClose} aria-label="Close">
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
|
|
||||||
// Track microphone via ref so the PTT effect doesn't need it as a dep (avoids listener churn)
|
// Track microphone via ref so the PTT effect doesn't need it as a dep (avoids listener churn)
|
||||||
const microphoneRef = useRef(microphone);
|
const microphoneRef = useRef(microphone);
|
||||||
useEffect(() => { microphoneRef.current = microphone; }, [microphone]);
|
useEffect(() => {
|
||||||
|
microphoneRef.current = microphone;
|
||||||
|
}, [microphone]);
|
||||||
|
|
||||||
// Handle PTT mode toggle mid-call — save/restore mic state (I-4)
|
// Handle PTT mode toggle mid-call — save/restore mic state (I-4)
|
||||||
const pttModeRef = useRef(pttMode);
|
const pttModeRef = useRef(pttMode);
|
||||||
@@ -165,8 +167,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
setPttActive(false);
|
setPttActive(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
|
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [pttMode, pttKey, callEmbed]);
|
}, [pttMode, pttKey, callEmbed]);
|
||||||
|
|
||||||
const [hangupState, hangup] = useAsyncCallback(
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
@@ -184,21 +186,31 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
justifyContent="Center"
|
justifyContent="Center"
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
>
|
>
|
||||||
{pttMode && (
|
{pttMode &&
|
||||||
lotusTerminal ? (
|
(lotusTerminal ? (
|
||||||
<Box style={{
|
<Box
|
||||||
position: 'absolute',
|
style={{
|
||||||
top: '-2.5rem',
|
position: 'absolute',
|
||||||
left: '50%',
|
top: '-2.5rem',
|
||||||
transform: 'translateX(-50%)',
|
left: '50%',
|
||||||
background: pttActive ? 'rgba(0,255,136,0.18)' : 'rgba(255,107,0,0.12)',
|
transform: 'translateX(-50%)',
|
||||||
border: `1px solid ${pttActive ? 'rgba(0,255,136,0.55)' : 'rgba(255,107,0,0.35)'}`,
|
background: pttActive ? 'rgba(0,255,136,0.18)' : 'rgba(255,107,0,0.12)',
|
||||||
borderRadius: '99px',
|
border: `1px solid ${pttActive ? 'rgba(0,255,136,0.55)' : 'rgba(255,107,0,0.35)'}`,
|
||||||
padding: '0.2rem 0.9rem',
|
borderRadius: '99px',
|
||||||
pointerEvents: 'none',
|
padding: '0.2rem 0.9rem',
|
||||||
whiteSpace: 'nowrap',
|
pointerEvents: 'none',
|
||||||
}}>
|
whiteSpace: 'nowrap',
|
||||||
<Text size="T200" style={{ color: pttActive ? '#00FF88' : '#FF6B00', fontWeight: 700, letterSpacing: '0.08em', fontFamily: 'JetBrains Mono, monospace' }}>
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="T200"
|
||||||
|
style={{
|
||||||
|
color: pttActive ? '#00FF88' : '#FF6B00',
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
fontFamily: 'JetBrains Mono, monospace',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{pttActive ? '● LIVE' : `PTT — Hold ${pttKeyLabel}`}
|
{pttActive ? '● LIVE' : `PTT — Hold ${pttKeyLabel}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -221,8 +233,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
{pttActive ? '● Live' : `PTT — Hold ${pttKeyLabel}`}
|
{pttActive ? '● Live' : `PTT — Hold ${pttKeyLabel}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
{shareConfirm && (
|
{shareConfirm && (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
@@ -242,17 +253,31 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
gap: '0.75rem',
|
gap: '0.75rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="T300" style={{ fontWeight: 600 }}>Share your screen?</Text>
|
<Text size="T300" style={{ fontWeight: 600 }}>
|
||||||
<Text size="T200" style={{ opacity: 0.75 }}>Your screen will be visible to all participants in this call.</Text>
|
Share your screen?
|
||||||
|
</Text>
|
||||||
|
<Text size="T200" style={{ opacity: 0.75 }}>
|
||||||
|
Your screen will be visible to all participants in this call.
|
||||||
|
</Text>
|
||||||
<Box gap="200">
|
<Box gap="200">
|
||||||
<Button
|
<Button
|
||||||
size="300" variant="Success" fill="Solid" radii="300"
|
size="300"
|
||||||
onClick={() => { callEmbed.control.toggleScreenshare(); setShareConfirm(false); }}
|
variant="Success"
|
||||||
|
fill="Solid"
|
||||||
|
radii="300"
|
||||||
|
onClick={() => {
|
||||||
|
callEmbed.control.toggleScreenshare();
|
||||||
|
setShareConfirm(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text size="B300">Share</Text>
|
<Text size="B300">Share</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="300" variant="Secondary" fill="Soft" radii="300" outlined
|
size="300"
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
radii="300"
|
||||||
|
outlined
|
||||||
onClick={() => setShareConfirm(false)}
|
onClick={() => setShareConfirm(false)}
|
||||||
>
|
>
|
||||||
<Text size="B300">Cancel</Text>
|
<Text size="B300">Cancel</Text>
|
||||||
@@ -281,9 +306,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
||||||
<ScreenShareButton
|
<ScreenShareButton
|
||||||
enabled={screenshare}
|
enabled={screenshare}
|
||||||
onToggle={() => screenshare
|
onToggle={() =>
|
||||||
? callEmbed.control.toggleScreenshare()
|
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
|
||||||
: setShareConfirm(true)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
|||||||
onClick={() => onToggle()}
|
onClick={() => onToggle()}
|
||||||
outlined
|
outlined
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-label={disabled ? 'Camera disabled in settings' : enabled ? 'Stop camera' : 'Start camera'}
|
aria-label={
|
||||||
|
disabled ? 'Camera disabled in settings' : enabled ? 'Stop camera' : 'Start camera'
|
||||||
|
}
|
||||||
aria-pressed={enabled}
|
aria-pressed={enabled}
|
||||||
style={disabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
|
style={disabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box grow="Yes" direction="Column" gap="200">
|
<Box grow="Yes" direction="Column" gap="200">
|
||||||
{micDenied && (
|
{micDenied && (
|
||||||
<Text size="T200" style={{ color: 'var(--tc-critical-high, #e53e3e)', textAlign: 'center' }}>
|
<Text
|
||||||
|
size="T200"
|
||||||
|
style={{ color: 'var(--tc-critical-high, #e53e3e)', textAlign: 'center' }}
|
||||||
|
>
|
||||||
Microphone access is blocked. Enable it in your browser settings to join.
|
Microphone access is blocked. Enable it in your browser settings to join.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -121,9 +121,16 @@ export function RoomEncryption({ permissions }: RoomEncryptionProps) {
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Enable Encryption</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Enable Encryption
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={() => setPrompt(false)} radii="300" aria-label="Cancel">
|
<IconButton
|
||||||
|
size="300"
|
||||||
|
onClick={() => setPrompt(false)}
|
||||||
|
radii="300"
|
||||||
|
aria-label="Cancel"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
|
<Text as="h2" size="H4">
|
||||||
|
{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={requestClose} radii="300" aria-label="Close">
|
<IconButton size="300" onClick={requestClose} radii="300" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ function CreateSpaceModal({ state }: CreateSpaceModalProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">New Space</Text>
|
<Text as="h2" size="H4">
|
||||||
|
New Space
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton size="300" radii="300" onClick={closeDialog} aria-label="Close">
|
<IconButton size="300" radii="300" onClick={closeDialog} aria-label="Close">
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export function LobbyHero() {
|
|||||||
const name = useRoomName(space);
|
const name = useRoomName(space);
|
||||||
const topic = useRoomTopic(space);
|
const topic = useRoomTopic(space);
|
||||||
const avatarMxc = useRoomAvatar(space);
|
const avatarMxc = useRoomAvatar(space);
|
||||||
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
|
const avatarUrl = avatarMxc
|
||||||
|
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHero
|
<PageHero
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ const DARK: Record<ChatBackground, CSSProperties> = {
|
|||||||
// True pointy-top hexagonal grid via SVG data URI
|
// True pointy-top hexagonal grid via SVG data URI
|
||||||
hexgrid: {
|
hexgrid: {
|
||||||
backgroundColor: '#060c14',
|
backgroundColor: '#060c14',
|
||||||
backgroundImage: 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%280%2C212%2C255%2C0.13%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
backgroundImage:
|
||||||
|
'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%280%2C212%2C255%2C0.13%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
||||||
backgroundSize: '29px 50px',
|
backgroundSize: '29px 50px',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -305,7 +306,8 @@ const LIGHT: Record<ChatBackground, CSSProperties> = {
|
|||||||
|
|
||||||
hexgrid: {
|
hexgrid: {
|
||||||
backgroundColor: '#f4f8ff',
|
backgroundColor: '#f4f8ff',
|
||||||
backgroundImage: 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%2850%2C100%2C220%2C0.11%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
backgroundImage:
|
||||||
|
'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%2850%2C100%2C220%2C0.11%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
||||||
backgroundSize: '29px 50px',
|
backgroundSize: '29px 50px',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -440,4 +440,4 @@ function RoomNavItem_({
|
|||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const RoomNavItem = React.memo(RoomNavItem_);
|
export const RoomNavItem = React.memo(RoomNavItem_);
|
||||||
|
|||||||
@@ -117,7 +117,11 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
{screenSize === ScreenSize.Mobile && (
|
{screenSize === ScreenSize.Mobile && (
|
||||||
<IconButton onClick={requestClose} variant="Background" aria-label="Close settings">
|
<IconButton
|
||||||
|
onClick={requestClose}
|
||||||
|
variant="Background"
|
||||||
|
aria-label="Close settings"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ export function CallChatView() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton ref={triggerRef} variant="Surface" onClick={handleClose} aria-label="Close call chat">
|
<IconButton
|
||||||
|
ref={triggerRef}
|
||||||
|
variant="Surface"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Close call chat"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ import {
|
|||||||
} from 'folds';
|
} from 'folds';
|
||||||
|
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
const GifPicker = React.lazy(() => import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })));
|
const GifPicker = React.lazy(() =>
|
||||||
|
import('../../components/GifPicker').then((m) => ({ default: m.GifPicker }))
|
||||||
|
);
|
||||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||||
import {
|
import {
|
||||||
CustomEditor,
|
CustomEditor,
|
||||||
@@ -57,7 +59,9 @@ import {
|
|||||||
getMentions,
|
getMentions,
|
||||||
} from '../../components/editor';
|
} from '../../components/editor';
|
||||||
import { EmojiBoardTab } from '../../components/emoji-board/types';
|
import { EmojiBoardTab } from '../../components/emoji-board/types';
|
||||||
const EmojiBoard = React.lazy(() => import('../../components/emoji-board').then((m) => ({ default: m.EmojiBoard })));
|
const EmojiBoard = React.lazy(() =>
|
||||||
|
import('../../components/emoji-board').then((m) => ({ default: m.EmojiBoard }))
|
||||||
|
);
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import {
|
import {
|
||||||
TUploadContent,
|
TUploadContent,
|
||||||
@@ -143,7 +147,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
const powerLevels = usePowerLevelsContext();
|
const powerLevels = usePowerLevelsContext();
|
||||||
const creators = useRoomCreators(room);
|
const creators = useRoomCreators(room);
|
||||||
|
|
||||||
const alive = useAlive();
|
const alive = useAlive();
|
||||||
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
||||||
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
||||||
const replyUserID = replyDraft?.userId;
|
const replyUserID = replyDraft?.userId;
|
||||||
@@ -467,8 +471,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
async (gifUrl: string, w: number, h: number) => {
|
async (gifUrl: string, w: number, h: number) => {
|
||||||
try {
|
try {
|
||||||
// Only fetch from trusted Giphy CDN domains
|
// Only fetch from trusted Giphy CDN domains
|
||||||
const allowed = ['media.giphy.com', 'i.giphy.com', 'media0.giphy.com',
|
const allowed = [
|
||||||
'media1.giphy.com', 'media2.giphy.com', 'media3.giphy.com', 'media4.giphy.com'];
|
'media.giphy.com',
|
||||||
|
'i.giphy.com',
|
||||||
|
'media0.giphy.com',
|
||||||
|
'media1.giphy.com',
|
||||||
|
'media2.giphy.com',
|
||||||
|
'media3.giphy.com',
|
||||||
|
'media4.giphy.com',
|
||||||
|
];
|
||||||
const { hostname } = new URL(gifUrl);
|
const { hostname } = new URL(gifUrl);
|
||||||
if (!allowed.includes(hostname)) return;
|
if (!allowed.includes(hostname)) return;
|
||||||
|
|
||||||
@@ -695,24 +706,26 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
||||||
}
|
}
|
||||||
content={
|
content={
|
||||||
<React.Suspense fallback={null}><EmojiBoard
|
<React.Suspense fallback={null}>
|
||||||
tab={emojiBoardTab}
|
<EmojiBoard
|
||||||
onTabChange={setEmojiBoardTab}
|
tab={emojiBoardTab}
|
||||||
imagePackRooms={imagePackRooms}
|
onTabChange={setEmojiBoardTab}
|
||||||
returnFocusOnDeactivate={false}
|
imagePackRooms={imagePackRooms}
|
||||||
onEmojiSelect={handleEmoticonSelect}
|
returnFocusOnDeactivate={false}
|
||||||
onCustomEmojiSelect={handleEmoticonSelect}
|
onEmojiSelect={handleEmoticonSelect}
|
||||||
onStickerSelect={handleStickerSelect}
|
onCustomEmojiSelect={handleEmoticonSelect}
|
||||||
requestClose={() => {
|
onStickerSelect={handleStickerSelect}
|
||||||
setEmojiBoardTab((t) => {
|
requestClose={() => {
|
||||||
if (t) {
|
setEmojiBoardTab((t) => {
|
||||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
if (t) {
|
||||||
return undefined;
|
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||||
}
|
return undefined;
|
||||||
return t;
|
}
|
||||||
});
|
return t;
|
||||||
}}
|
});
|
||||||
/></React.Suspense>
|
}}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!hideStickerBtn && (
|
{!hideStickerBtn && (
|
||||||
@@ -765,11 +778,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
content={
|
content={
|
||||||
<React.Suspense fallback={null}><GifPicker
|
<React.Suspense fallback={null}>
|
||||||
apiKey={gifApiKey}
|
<GifPicker
|
||||||
onSelect={handleGifSelect}
|
apiKey={gifApiKey}
|
||||||
requestClose={() => setGifOpen(false)}
|
onSelect={handleGifSelect}
|
||||||
/></React.Suspense>
|
requestClose={() => setGifOpen(false)}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -798,7 +813,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
</UseStateProvider>
|
</UseStateProvider>
|
||||||
)}
|
)}
|
||||||
{gifError && (
|
{gifError && (
|
||||||
<Text size="T100" style={{ color: 'var(--tc-danger-normal)', padding: '2px 6px', alignSelf: 'center', whiteSpace: 'nowrap' }}>
|
<Text
|
||||||
|
size="T100"
|
||||||
|
style={{
|
||||||
|
color: 'var(--tc-danger-normal)',
|
||||||
|
padding: '2px 6px',
|
||||||
|
alignSelf: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{gifError}
|
{gifError}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -811,14 +834,28 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
title="Share location"
|
title="Share location"
|
||||||
>
|
>
|
||||||
{locating ? (
|
{locating ? (
|
||||||
<Text size="T200" style={{ fontWeight: 800, fontSize: '10px', letterSpacing: '0.04em', lineHeight: 1 }}>
|
<Text
|
||||||
|
size="T200"
|
||||||
|
style={{
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: '10px',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
...
|
...
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Icon src={Icons.Pin} size="100" />
|
<Icon src={Icons.Pin} size="100" />
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={submit} variant="SurfaceVariant" size="300" radii="300" aria-label="Send message">
|
<IconButton
|
||||||
|
onClick={submit}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
aria-label="Send message"
|
||||||
|
>
|
||||||
<Icon src={Icons.Send} />
|
<Icon src={Icons.Send} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -637,7 +637,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
// and either there are no unread messages or the latest message is from the current user.
|
// and either there are no unread messages or the latest message is from the current user.
|
||||||
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
||||||
const _roomId = mEvt.getRoomId();
|
const _roomId = mEvt.getRoomId();
|
||||||
if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity));
|
if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.hasFocus() && !unreadInfo) {
|
if (!document.hasFocus() && !unreadInfo) {
|
||||||
@@ -673,7 +673,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
) => {
|
) => {
|
||||||
const evtTimeline = getEventTimeline(room, evtId);
|
const evtTimeline = getEventTimeline(room, evtId);
|
||||||
const absoluteIndex =
|
const absoluteIndex =
|
||||||
evtTimeline && getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
|
evtTimeline &&
|
||||||
|
getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
|
||||||
|
|
||||||
if (typeof absoluteIndex === 'number') {
|
if (typeof absoluteIndex === 'number') {
|
||||||
const scrolled = scrollToItem(absoluteIndex, {
|
const scrolled = scrollToItem(absoluteIndex, {
|
||||||
@@ -1242,7 +1243,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
mEvent.getType() === 'm.poll.start' ||
|
mEvent.getType() === 'm.poll.start' ||
|
||||||
mEvent.getType() === 'org.matrix.msc3381.poll.start'
|
mEvent.getType() === 'org.matrix.msc3381.poll.start'
|
||||||
)
|
)
|
||||||
return <PollContent content={mEvent.getContent()} roomId={room.roomId} eventId={mEvent.getId() ?? undefined} />;
|
return (
|
||||||
|
<PollContent
|
||||||
|
content={mEvent.getContent()}
|
||||||
|
roomId={room.roomId}
|
||||||
|
eventId={mEvent.getId() ?? undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
|
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
|
||||||
return (
|
return (
|
||||||
<Text>
|
<Text>
|
||||||
@@ -1371,7 +1378,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
{mEvent.isRedacted() ? (
|
{mEvent.isRedacted() ? (
|
||||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||||
) : (
|
) : (
|
||||||
<PollContent content={mEvent.getContent()} roomId={room.roomId} eventId={mEvent.getId() ?? undefined} />
|
<PollContent
|
||||||
|
content={mEvent.getContent()}
|
||||||
|
roomId={room.roomId}
|
||||||
|
eventId={mEvent.getId() ?? undefined}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Message>
|
</Message>
|
||||||
);
|
);
|
||||||
@@ -1424,7 +1435,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
{mEvent.isRedacted() ? (
|
{mEvent.isRedacted() ? (
|
||||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||||
) : (
|
) : (
|
||||||
<PollContent content={mEvent.getContent()} roomId={room.roomId} eventId={mEvent.getId() ?? undefined} />
|
<PollContent
|
||||||
|
content={mEvent.getContent()}
|
||||||
|
roomId={room.roomId}
|
||||||
|
eventId={mEvent.getId() ?? undefined}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Message>
|
</Message>
|
||||||
);
|
);
|
||||||
@@ -1608,12 +1623,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const senderId = mEvent.getSender() ?? '';
|
const senderId = mEvent.getSender() ?? '';
|
||||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
const timeJSX = (
|
const timeJSX = (
|
||||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
<Time
|
||||||
|
ts={mEvent.getTs()}
|
||||||
|
compact={messageLayout === MessageLayout.Compact}
|
||||||
|
hour24Clock={hour24Clock}
|
||||||
|
dateFormatString={dateFormatString}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
<Event
|
||||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Lock}
|
key={mEvent.getId()}
|
||||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{' enabled end-to-end encryption'}</Text></Box>}
|
data-message-item={item}
|
||||||
|
data-message-id={mEventId}
|
||||||
|
room={room}
|
||||||
|
mEvent={mEvent}
|
||||||
|
highlight={highlighted}
|
||||||
|
messageSpacing={messageSpacing}
|
||||||
|
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||||
|
hideReadReceipts={hideActivity}
|
||||||
|
showDeveloperTools={showDeveloperTools}
|
||||||
|
>
|
||||||
|
<EventContent
|
||||||
|
messageLayout={messageLayout}
|
||||||
|
time={timeJSX}
|
||||||
|
iconSrc={Icons.Lock}
|
||||||
|
content={
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
<b>{senderName}</b>
|
||||||
|
{' enabled end-to-end encryption'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Event>
|
</Event>
|
||||||
);
|
);
|
||||||
@@ -1623,14 +1664,45 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const senderId = mEvent.getSender() ?? '';
|
const senderId = mEvent.getSender() ?? '';
|
||||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
const joinRule = mEvent.getContent<{ join_rule?: string }>().join_rule ?? 'unknown';
|
const joinRule = mEvent.getContent<{ join_rule?: string }>().join_rule ?? 'unknown';
|
||||||
const ruleLabel: Record<string, string> = { public: 'public', invite: 'invite-only', knock: 'knock', restricted: 'restricted' };
|
const ruleLabel: Record<string, string> = {
|
||||||
|
public: 'public',
|
||||||
|
invite: 'invite-only',
|
||||||
|
knock: 'knock',
|
||||||
|
restricted: 'restricted',
|
||||||
|
};
|
||||||
const timeJSX = (
|
const timeJSX = (
|
||||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
<Time
|
||||||
|
ts={mEvent.getTs()}
|
||||||
|
compact={messageLayout === MessageLayout.Compact}
|
||||||
|
hour24Clock={hour24Clock}
|
||||||
|
dateFormatString={dateFormatString}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
<Event
|
||||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Settings}
|
key={mEvent.getId()}
|
||||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{` set room join rule to ${ruleLabel[joinRule] ?? joinRule}`}</Text></Box>}
|
data-message-item={item}
|
||||||
|
data-message-id={mEventId}
|
||||||
|
room={room}
|
||||||
|
mEvent={mEvent}
|
||||||
|
highlight={highlighted}
|
||||||
|
messageSpacing={messageSpacing}
|
||||||
|
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||||
|
hideReadReceipts={hideActivity}
|
||||||
|
showDeveloperTools={showDeveloperTools}
|
||||||
|
>
|
||||||
|
<EventContent
|
||||||
|
messageLayout={messageLayout}
|
||||||
|
time={timeJSX}
|
||||||
|
iconSrc={Icons.Settings}
|
||||||
|
content={
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
<b>{senderName}</b>
|
||||||
|
{` set room join rule to ${ruleLabel[joinRule] ?? joinRule}`}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Event>
|
</Event>
|
||||||
);
|
);
|
||||||
@@ -1641,12 +1713,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
const access = mEvent.getContent<{ guest_access?: string }>().guest_access ?? 'unknown';
|
const access = mEvent.getContent<{ guest_access?: string }>().guest_access ?? 'unknown';
|
||||||
const timeJSX = (
|
const timeJSX = (
|
||||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
<Time
|
||||||
|
ts={mEvent.getTs()}
|
||||||
|
compact={messageLayout === MessageLayout.Compact}
|
||||||
|
hour24Clock={hour24Clock}
|
||||||
|
dateFormatString={dateFormatString}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
<Event
|
||||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Settings}
|
key={mEvent.getId()}
|
||||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{access === 'can_join' ? ' allowed guest access' : ' disabled guest access'}</Text></Box>}
|
data-message-item={item}
|
||||||
|
data-message-id={mEventId}
|
||||||
|
room={room}
|
||||||
|
mEvent={mEvent}
|
||||||
|
highlight={highlighted}
|
||||||
|
messageSpacing={messageSpacing}
|
||||||
|
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||||
|
hideReadReceipts={hideActivity}
|
||||||
|
showDeveloperTools={showDeveloperTools}
|
||||||
|
>
|
||||||
|
<EventContent
|
||||||
|
messageLayout={messageLayout}
|
||||||
|
time={timeJSX}
|
||||||
|
iconSrc={Icons.Settings}
|
||||||
|
content={
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
<b>{senderName}</b>
|
||||||
|
{access === 'can_join' ? ' allowed guest access' : ' disabled guest access'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Event>
|
</Event>
|
||||||
);
|
);
|
||||||
@@ -1657,12 +1755,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
const alias = mEvent.getContent<{ alias?: string }>().alias;
|
const alias = mEvent.getContent<{ alias?: string }>().alias;
|
||||||
const timeJSX = (
|
const timeJSX = (
|
||||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
<Time
|
||||||
|
ts={mEvent.getTs()}
|
||||||
|
compact={messageLayout === MessageLayout.Compact}
|
||||||
|
hour24Clock={hour24Clock}
|
||||||
|
dateFormatString={dateFormatString}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
<Event
|
||||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Hash}
|
key={mEvent.getId()}
|
||||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{alias ? ` set room address to ${alias}` : ' removed room address'}</Text></Box>}
|
data-message-item={item}
|
||||||
|
data-message-id={mEventId}
|
||||||
|
room={room}
|
||||||
|
mEvent={mEvent}
|
||||||
|
highlight={highlighted}
|
||||||
|
messageSpacing={messageSpacing}
|
||||||
|
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||||
|
hideReadReceipts={hideActivity}
|
||||||
|
showDeveloperTools={showDeveloperTools}
|
||||||
|
>
|
||||||
|
<EventContent
|
||||||
|
messageLayout={messageLayout}
|
||||||
|
time={timeJSX}
|
||||||
|
iconSrc={Icons.Hash}
|
||||||
|
content={
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Text size="T300" priority="300">
|
||||||
|
<b>{senderName}</b>
|
||||||
|
{alias ? ` set room address to ${alias}` : ' removed room address'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Event>
|
</Event>
|
||||||
);
|
);
|
||||||
@@ -1831,9 +1955,15 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
while (lo <= hi) {
|
while (lo <= hi) {
|
||||||
const mid = (lo + hi) >>> 1;
|
const mid = (lo + hi) >>> 1;
|
||||||
const [base, len] = timelineSegments[mid];
|
const [base, len] = timelineSegments[mid];
|
||||||
if (item < base) { hi = mid - 1; }
|
if (item < base) {
|
||||||
else if (item >= base + len) { lo = mid + 1; }
|
hi = mid - 1;
|
||||||
else { eventTimeline = timelineSegments[mid][2]; baseIndex = base; break; }
|
} else if (item >= base + len) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
eventTimeline = timelineSegments[mid][2];
|
||||||
|
baseIndex = base;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!eventTimeline) return null;
|
if (!eventTimeline) return null;
|
||||||
@@ -1935,131 +2065,131 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReadPositionsContext.Provider value={readPositions}>
|
<ReadPositionsContext.Provider value={readPositions}>
|
||||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||||
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
|
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
|
||||||
<TimelineFloat position="Top">
|
<TimelineFloat position="Top">
|
||||||
<Chip
|
<Chip
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
radii="Pill"
|
radii="Pill"
|
||||||
outlined
|
outlined
|
||||||
before={<Icon size="50" src={Icons.MessageUnread} />}
|
before={<Icon size="50" src={Icons.MessageUnread} />}
|
||||||
onClick={handleJumpToUnread}
|
onClick={handleJumpToUnread}
|
||||||
>
|
|
||||||
<Text size="L400">Jump to Unread</Text>
|
|
||||||
</Chip>
|
|
||||||
|
|
||||||
<Chip
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
radii="Pill"
|
|
||||||
outlined
|
|
||||||
before={<Icon size="50" src={Icons.CheckTwice} />}
|
|
||||||
onClick={handleMarkAsRead}
|
|
||||||
>
|
|
||||||
<Text size="L400">Mark as Read</Text>
|
|
||||||
</Chip>
|
|
||||||
</TimelineFloat>
|
|
||||||
)}
|
|
||||||
<Scroll ref={scrollRef} visibility="Hover">
|
|
||||||
<Box
|
|
||||||
direction="Column"
|
|
||||||
justifyContent="End"
|
|
||||||
style={{ minHeight: '100%', padding: `${config.space.S600} 0` }}
|
|
||||||
>
|
|
||||||
{!canPaginateBack && rangeAtStart && getItems().length > 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${
|
|
||||||
messageLayout === MessageLayout.Compact ? config.space.S400 : toRem(64)
|
|
||||||
}`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RoomIntro room={room} />
|
<Text size="L400">Jump to Unread</Text>
|
||||||
</div>
|
</Chip>
|
||||||
)}
|
|
||||||
{(canPaginateBack || !rangeAtStart) &&
|
|
||||||
(messageLayout === MessageLayout.Compact ? (
|
|
||||||
<>
|
|
||||||
<MessageBase>
|
|
||||||
<CompactPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase>
|
|
||||||
<CompactPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase>
|
|
||||||
<CompactPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase>
|
|
||||||
<CompactPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase ref={observeBackAnchor}>
|
|
||||||
<CompactPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MessageBase>
|
|
||||||
<DefaultPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase>
|
|
||||||
<DefaultPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase ref={observeBackAnchor}>
|
|
||||||
<DefaultPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{getItems().map(eventRenderer)}
|
<Chip
|
||||||
|
variant="SurfaceVariant"
|
||||||
{(!liveTimelineLinked || !rangeAtEnd) &&
|
radii="Pill"
|
||||||
(messageLayout === MessageLayout.Compact ? (
|
outlined
|
||||||
<>
|
before={<Icon size="50" src={Icons.CheckTwice} />}
|
||||||
<MessageBase ref={observeFrontAnchor}>
|
onClick={handleMarkAsRead}
|
||||||
<CompactPlaceholder key={getItems().length} />
|
>
|
||||||
</MessageBase>
|
<Text size="L400">Mark as Read</Text>
|
||||||
<MessageBase>
|
</Chip>
|
||||||
<CompactPlaceholder key={getItems().length} />
|
</TimelineFloat>
|
||||||
</MessageBase>
|
)}
|
||||||
<MessageBase>
|
<Scroll ref={scrollRef} visibility="Hover">
|
||||||
<CompactPlaceholder key={getItems().length} />
|
<Box
|
||||||
</MessageBase>
|
direction="Column"
|
||||||
<MessageBase>
|
justifyContent="End"
|
||||||
<CompactPlaceholder key={getItems().length} />
|
style={{ minHeight: '100%', padding: `${config.space.S600} 0` }}
|
||||||
</MessageBase>
|
|
||||||
<MessageBase>
|
|
||||||
<CompactPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MessageBase ref={observeFrontAnchor}>
|
|
||||||
<DefaultPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase>
|
|
||||||
<DefaultPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
<MessageBase>
|
|
||||||
<DefaultPlaceholder key={getItems().length} />
|
|
||||||
</MessageBase>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
<span ref={atBottomAnchorRef} />
|
|
||||||
</Box>
|
|
||||||
</Scroll>
|
|
||||||
{!atBottom && (
|
|
||||||
<TimelineFloat position="Bottom">
|
|
||||||
<Chip
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
radii="Pill"
|
|
||||||
outlined
|
|
||||||
before={<Icon size="50" src={Icons.ArrowBottom} />}
|
|
||||||
onClick={handleJumpToLatest}
|
|
||||||
>
|
>
|
||||||
<Text size="L400">Jump to Latest</Text>
|
{!canPaginateBack && rangeAtStart && getItems().length > 0 && (
|
||||||
</Chip>
|
<div
|
||||||
</TimelineFloat>
|
style={{
|
||||||
)}
|
padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${
|
||||||
</Box>
|
messageLayout === MessageLayout.Compact ? config.space.S400 : toRem(64)
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RoomIntro room={room} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(canPaginateBack || !rangeAtStart) &&
|
||||||
|
(messageLayout === MessageLayout.Compact ? (
|
||||||
|
<>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase ref={observeBackAnchor}>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MessageBase>
|
||||||
|
<DefaultPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<DefaultPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase ref={observeBackAnchor}>
|
||||||
|
<DefaultPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{getItems().map(eventRenderer)}
|
||||||
|
|
||||||
|
{(!liveTimelineLinked || !rangeAtEnd) &&
|
||||||
|
(messageLayout === MessageLayout.Compact ? (
|
||||||
|
<>
|
||||||
|
<MessageBase ref={observeFrontAnchor}>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<CompactPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MessageBase ref={observeFrontAnchor}>
|
||||||
|
<DefaultPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<DefaultPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
<MessageBase>
|
||||||
|
<DefaultPlaceholder key={getItems().length} />
|
||||||
|
</MessageBase>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
<span ref={atBottomAnchorRef} />
|
||||||
|
</Box>
|
||||||
|
</Scroll>
|
||||||
|
{!atBottom && (
|
||||||
|
<TimelineFloat position="Bottom">
|
||||||
|
<Chip
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
radii="Pill"
|
||||||
|
outlined
|
||||||
|
before={<Icon size="50" src={Icons.ArrowBottom} />}
|
||||||
|
onClick={handleJumpToLatest}
|
||||||
|
>
|
||||||
|
<Text size="L400">Jump to Latest</Text>
|
||||||
|
</Chip>
|
||||||
|
</TimelineFloat>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</ReadPositionsContext.Provider>
|
</ReadPositionsContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function RoomView({ eventId }: { eventId?: string }) {
|
export function RoomView({ eventId }: { eventId?: string }) {
|
||||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -98,8 +96,9 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const chatBgStyle = useMemo(
|
const chatBgStyle = useMemo(
|
||||||
() => getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark),
|
() =>
|
||||||
|
getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark),
|
||||||
[chatBackground, lotusTerminal, isDark]
|
[chatBackground, lotusTerminal, isDark]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -533,7 +533,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton fill="None" ref={triggerRef} onClick={handleSearchClick} aria-label="Search">
|
<IconButton
|
||||||
|
fill="None"
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={handleSearchClick}
|
||||||
|
aria-label="Search"
|
||||||
|
>
|
||||||
<Icon size="400" src={Icons.Search} />
|
<Icon size="400" src={Icons.Search} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -596,11 +601,13 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission &&
|
{!room.isCallRoom() &&
|
||||||
(direct || (room.getJoinRule() === 'invite' &&
|
livekitSupported &&
|
||||||
getStateEvents(room, StateEvent.SpaceParent).length === 0)) && (
|
rtcSupported &&
|
||||||
<CallButton />
|
hasCallPermission &&
|
||||||
)}
|
(direct ||
|
||||||
|
(room.getJoinRule() === 'invite' &&
|
||||||
|
getStateEvents(room, StateEvent.SpaceParent).length === 0)) && <CallButton />}
|
||||||
{screenSize === ScreenSize.Desktop && (
|
{screenSize === ScreenSize.Desktop && (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Bottom"
|
position="Bottom"
|
||||||
@@ -616,7 +623,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(triggerRef) => (
|
{(triggerRef) => (
|
||||||
<IconButton fill="None" ref={triggerRef} onClick={handleMemberToggle} aria-label="Toggle member list">
|
<IconButton
|
||||||
|
fill="None"
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={handleMemberToggle}
|
||||||
|
aria-label="Toggle member list"
|
||||||
|
>
|
||||||
<Icon size="400" src={Icons.User} />
|
<Icon size="400" src={Icons.User} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -117,7 +117,13 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton title="Drop Typing Status" aria-label="Drop typing status" size="300" radii="Pill" onClick={handleDropAll}>
|
<IconButton
|
||||||
|
title="Drop Typing Status"
|
||||||
|
aria-label="Drop typing status"
|
||||||
|
size="300"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={handleDropAll}
|
||||||
|
>
|
||||||
<Icon size="50" src={Icons.Cross} />
|
<Icon size="50" src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -57,7 +57,12 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
size="400"
|
size="400"
|
||||||
style={{ maxHeight: '440px', borderRadius: config.radii.R500, display: 'flex', flexDirection: 'column' }}
|
style={{
|
||||||
|
maxHeight: '440px',
|
||||||
|
borderRadius: config.radii.R500,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
@@ -89,11 +94,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<Box grow="Yes" style={{ minHeight: 0 }}>
|
<Box grow="Yes" style={{ minHeight: 0 }}>
|
||||||
<Scroll size="300" hideTrack visibility="Hover">
|
<Scroll size="300" hideTrack visibility="Hover">
|
||||||
<Box
|
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||||
direction="Column"
|
|
||||||
gap="100"
|
|
||||||
style={{ padding: config.space.S200 }}
|
|
||||||
>
|
|
||||||
{filtered.slice(0, 60).map((room) => (
|
{filtered.slice(0, 60).map((room) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={room.roomId}
|
key={room.roomId}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -141,7 +141,11 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
{screenSize === ScreenSize.Mobile && (
|
{screenSize === ScreenSize.Mobile && (
|
||||||
<IconButton onClick={requestClose} variant="Background" aria-label="Close settings">
|
<IconButton
|
||||||
|
onClick={requestClose}
|
||||||
|
variant="Background"
|
||||||
|
aria-label="Close settings"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -185,7 +185,12 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
|
|||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Remove Avatar</Text>
|
<Text size="H4">Remove Avatar</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300" aria-label="Cancel">
|
<IconButton
|
||||||
|
size="300"
|
||||||
|
onClick={() => setAlertRemove(false)}
|
||||||
|
radii="300"
|
||||||
|
aria-label="Cancel"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ import FocusTrap from 'focus-trap-react';
|
|||||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
import { useSetting } from '../../../state/hooks/settings';
|
||||||
import { ChatBackground, DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
import {
|
||||||
|
ChatBackground,
|
||||||
|
DateFormat,
|
||||||
|
MessageLayout,
|
||||||
|
MessageSpacing,
|
||||||
|
settingsAtom,
|
||||||
|
} from '../../../state/settings';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { KeySymbol } from '../../../utils/key-symbol';
|
import { KeySymbol } from '../../../utils/key-symbol';
|
||||||
import { isMacOS } from '../../../utils/user-agent';
|
import { isMacOS } from '../../../utils/user-agent';
|
||||||
@@ -313,7 +319,10 @@ function Appearance() {
|
|||||||
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||||
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
||||||
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||||
const [perMessageProfiles, setPerMessageProfiles] = useSetting(settingsAtom, 'perMessageProfiles');
|
const [perMessageProfiles, setPerMessageProfiles] = useSetting(
|
||||||
|
settingsAtom,
|
||||||
|
'perMessageProfiles'
|
||||||
|
);
|
||||||
const [lotusTerminal, setLotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
const [lotusTerminal, setLotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -359,7 +368,12 @@ function Appearance() {
|
|||||||
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
|
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
|
|
||||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="200">
|
<SequenceCard
|
||||||
|
className={SequenceCardStyle}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
direction="Column"
|
||||||
|
gap="200"
|
||||||
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Chat Background"
|
title="Chat Background"
|
||||||
description="Pattern applied behind the message timeline."
|
description="Pattern applied behind the message timeline."
|
||||||
@@ -373,7 +387,9 @@ function Appearance() {
|
|||||||
<SettingTile
|
<SettingTile
|
||||||
title="Show Profile on Every Message"
|
title="Show Profile on Every Message"
|
||||||
description="Display avatar and name on each message instead of grouping consecutive messages."
|
description="Display avatar and name on each message instead of grouping consecutive messages."
|
||||||
after={<Switch variant="Primary" value={perMessageProfiles} onChange={setPerMessageProfiles} />}
|
after={
|
||||||
|
<Switch variant="Primary" value={perMessageProfiles} onChange={setPerMessageProfiles} />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||||
@@ -385,7 +401,10 @@ function Appearance() {
|
|||||||
{lotusTerminal && (
|
{lotusTerminal && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { resetBootSequence(); runLotusBootSequence(true); }}
|
onClick={() => {
|
||||||
|
resetBootSequence();
|
||||||
|
runLotusBootSequence(true);
|
||||||
|
}}
|
||||||
title="Replay boot sequence"
|
title="Replay boot sequence"
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
@@ -808,10 +827,12 @@ function Editor() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function Calls() {
|
function Calls() {
|
||||||
const [cameraOnJoin, setCameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
const [cameraOnJoin, setCameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
||||||
const [callNoiseSuppression, setCallNoiseSuppression] = useSetting(settingsAtom, 'callNoiseSuppression');
|
const [callNoiseSuppression, setCallNoiseSuppression] = useSetting(
|
||||||
|
settingsAtom,
|
||||||
|
'callNoiseSuppression'
|
||||||
|
);
|
||||||
const [pttMode, setPttMode] = useSetting(settingsAtom, 'pttMode');
|
const [pttMode, setPttMode] = useSetting(settingsAtom, 'pttMode');
|
||||||
const [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey');
|
const [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey');
|
||||||
const [listeningForKey, setListeningForKey] = useState(false);
|
const [listeningForKey, setListeningForKey] = useState(false);
|
||||||
@@ -843,7 +864,8 @@ function Calls() {
|
|||||||
window.addEventListener('keydown', onKey, true);
|
window.addEventListener('keydown', onKey, true);
|
||||||
}, [listeningForKey, setPttKey]);
|
}, [listeningForKey, setPttKey]);
|
||||||
|
|
||||||
const keyLabel = (code: string) => code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', '');
|
const keyLabel = (code: string) =>
|
||||||
|
code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
@@ -859,10 +881,21 @@ function Calls() {
|
|||||||
<SettingTile
|
<SettingTile
|
||||||
title="Noise Suppression"
|
title="Noise Suppression"
|
||||||
description="Apply AI noise suppression to filter background noise during calls (powered by Element Call)."
|
description="Apply AI noise suppression to filter background noise during calls (powered by Element Call)."
|
||||||
after={<Switch variant="Primary" value={callNoiseSuppression} onChange={setCallNoiseSuppression} />}
|
after={
|
||||||
|
<Switch
|
||||||
|
variant="Primary"
|
||||||
|
value={callNoiseSuppression}
|
||||||
|
onChange={setCallNoiseSuppression}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="400">
|
<SequenceCard
|
||||||
|
className={SequenceCardStyle}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
direction="Column"
|
||||||
|
gap="400"
|
||||||
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Push to Talk"
|
title="Push to Talk"
|
||||||
description="Mute your microphone by default. Hold the PTT key to speak."
|
description="Mute your microphone by default. Hold the PTT key to speak."
|
||||||
@@ -882,9 +915,7 @@ function Calls() {
|
|||||||
onClick={handleKeyBind}
|
onClick={handleKeyBind}
|
||||||
style={{ minWidth: '90px' }}
|
style={{ minWidth: '90px' }}
|
||||||
>
|
>
|
||||||
<Text size="B300">
|
<Text size="B300">{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}</Text>
|
||||||
{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -894,7 +925,6 @@ function Calls() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ChatBgGrid() {
|
function ChatBgGrid() {
|
||||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -915,18 +945,16 @@ function ChatBgGrid() {
|
|||||||
height: toRem(50),
|
height: toRem(50),
|
||||||
borderRadius: toRem(8),
|
borderRadius: toRem(8),
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
border: chatBackground === opt.value
|
border:
|
||||||
? '2px solid #980000'
|
chatBackground === opt.value
|
||||||
: '2px solid rgba(128,128,128,0.25)',
|
? '2px solid #980000'
|
||||||
|
: '2px solid rgba(128,128,128,0.25)',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
...getChatBg(opt.value as ChatBackground, isDark),
|
...getChatBg(opt.value as ChatBackground, isDark),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text size="T200" style={chatBackground === opt.value ? { color: '#980000' } : undefined}>
|
||||||
size="T200"
|
|
||||||
style={chatBackground === opt.value ? { color: '#980000' } : undefined}
|
|
||||||
>
|
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -120,7 +120,14 @@ function KeywordCross({ pushRule }: PushRulesProps) {
|
|||||||
|
|
||||||
const removing = removeState.status === AsyncStatus.Loading;
|
const removing = removeState.status === AsyncStatus.Loading;
|
||||||
return (
|
return (
|
||||||
<IconButton onClick={remove} size="300" radii="Pill" variant="Secondary" disabled={removing} aria-label="Remove keyword">
|
<IconButton
|
||||||
|
onClick={remove}
|
||||||
|
size="300"
|
||||||
|
radii="Pill"
|
||||||
|
variant="Secondary"
|
||||||
|
disabled={removing}
|
||||||
|
aria-label="Remove keyword"
|
||||||
|
>
|
||||||
{removing ? <Spinner size="100" /> : <Icon src={Icons.Cross} size="100" />}
|
{removing ? <Spinner size="100" /> : <Icon src={Icons.Cross} size="100" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -117,7 +117,11 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps)
|
|||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
{screenSize === ScreenSize.Mobile && (
|
{screenSize === ScreenSize.Mobile && (
|
||||||
<IconButton onClick={requestClose} variant="Background" aria-label="Close settings">
|
<IconButton
|
||||||
|
onClick={requestClose}
|
||||||
|
variant="Background"
|
||||||
|
aria-label="Close settings"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,10 +53,19 @@ export const createCallEmbed = (
|
|||||||
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0;
|
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0;
|
||||||
|
|
||||||
const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
|
const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
|
||||||
const initialAudio = forceAudioOff ? false : (pref?.microphone ?? true);
|
const initialAudio = forceAudioOff ? false : pref?.microphone ?? true;
|
||||||
const initialVideo = pref?.video ?? false;
|
const initialVideo = pref?.video ?? false;
|
||||||
const widget = CallEmbed.getWidget(mx, room, intent, themeKind, noiseSuppression, initialAudio, initialVideo);
|
const widget = CallEmbed.getWidget(
|
||||||
const controlState = pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
mx,
|
||||||
|
room,
|
||||||
|
intent,
|
||||||
|
themeKind,
|
||||||
|
noiseSuppression,
|
||||||
|
initialAudio,
|
||||||
|
initialVideo
|
||||||
|
);
|
||||||
|
const controlState =
|
||||||
|
pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
||||||
|
|
||||||
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
||||||
|
|
||||||
@@ -77,7 +86,16 @@ export const useCallStart = (dm = false) => {
|
|||||||
if (!container) {
|
if (!container) {
|
||||||
throw new Error('Failed to start call, No embed container element found!');
|
throw new Error('Failed to start call, No embed container element found!');
|
||||||
}
|
}
|
||||||
const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref, callNoiseSuppression ?? true, !!pttMode);
|
const callEmbed = createCallEmbed(
|
||||||
|
mx,
|
||||||
|
room,
|
||||||
|
dm,
|
||||||
|
theme.kind,
|
||||||
|
container,
|
||||||
|
pref,
|
||||||
|
callNoiseSuppression ?? true,
|
||||||
|
!!pttMode
|
||||||
|
);
|
||||||
|
|
||||||
setCallEmbed(callEmbed);
|
setCallEmbed(callEmbed);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { useState } from 'react';
|
|||||||
export function useForceUpdate() {
|
export function useForceUpdate() {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
return [data, function forceUpdateHook() {
|
return [
|
||||||
setData({});
|
data,
|
||||||
}];
|
function forceUpdateHook() {
|
||||||
|
setData({});
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,11 +55,14 @@ export const usePan = (active: boolean) => {
|
|||||||
}, [active]);
|
}, [active]);
|
||||||
|
|
||||||
// Clean up document listeners if component unmounts during an active drag
|
// Clean up document listeners if component unmounts during an active drag
|
||||||
useEffect(() => () => {
|
useEffect(
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
() => () => {
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
}, []);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pan,
|
pan,
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ function computePositions(room: Room, myUserId: string): Map<string, string[]> {
|
|||||||
const map = new Map<string, string[]>();
|
const map = new Map<string, string[]>();
|
||||||
const liveEvents = room.getLiveTimeline().getEvents();
|
const liveEvents = room.getLiveTimeline().getEvents();
|
||||||
// Build O(1) index once instead of O(T) findIndex per member
|
// Build O(1) index once instead of O(T) findIndex per member
|
||||||
const eventIndex = new Map<string, number>(
|
const eventIndex = new Map<string, number>(liveEvents.map((e, i) => [e.getId() ?? '', i]));
|
||||||
liveEvents.map((e, i) => [e.getId() ?? '', i])
|
|
||||||
);
|
|
||||||
for (const member of room.getJoinedMembers()) {
|
for (const member of room.getJoinedMembers()) {
|
||||||
if (member.userId === myUserId) continue;
|
if (member.userId === myUserId) continue;
|
||||||
const evtId = room.getEventReadUpTo(member.userId);
|
const evtId = room.getEventReadUpTo(member.userId);
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { lightTheme } from 'folds';
|
import { lightTheme } from 'folds';
|
||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||||
import { butterTheme, darkTheme, lotusTerminalLightTheme, lotusTerminalTheme, silverTheme } from '../../colors.css';
|
import {
|
||||||
|
butterTheme,
|
||||||
|
darkTheme,
|
||||||
|
lotusTerminalLightTheme,
|
||||||
|
lotusTerminalTheme,
|
||||||
|
silverTheme,
|
||||||
|
} from '../../colors.css';
|
||||||
import { settingsAtom } from '../state/settings';
|
import { settingsAtom } from '../state/settings';
|
||||||
import { useSetting } from '../state/hooks/settings';
|
import { useSetting } from '../state/hooks/settings';
|
||||||
|
|
||||||
@@ -45,7 +51,12 @@ export const LotusTerminalTheme: Theme = {
|
|||||||
export const LotusTerminalLightTheme: Theme = {
|
export const LotusTerminalLightTheme: Theme = {
|
||||||
id: 'lotus-terminal-light-theme',
|
id: 'lotus-terminal-light-theme',
|
||||||
kind: ThemeKind.Light,
|
kind: ThemeKind.Light,
|
||||||
classNames: ['lotus-terminal-light-theme', lotusTerminalLightTheme, onLightFontWeight, 'prism-light'],
|
classNames: [
|
||||||
|
'lotus-terminal-light-theme',
|
||||||
|
lotusTerminalLightTheme,
|
||||||
|
onLightFontWeight,
|
||||||
|
'prism-light',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useThemes = (): Theme[] => {
|
export const useThemes = (): Theme[] => {
|
||||||
|
|||||||
+120
-33
@@ -10,10 +10,12 @@ import {
|
|||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { ClientConfig } from '../hooks/useClientConfig';
|
import { ClientConfig } from '../hooks/useClientConfig';
|
||||||
const AuthLayout = React.lazy(() => import('./auth').then(m => ({ default: m.AuthLayout })));
|
const AuthLayout = React.lazy(() => import('./auth').then((m) => ({ default: m.AuthLayout })));
|
||||||
const Login = React.lazy(() => import('./auth').then(m => ({ default: m.Login })));
|
const Login = React.lazy(() => import('./auth').then((m) => ({ default: m.Login })));
|
||||||
const Register = React.lazy(() => import('./auth').then(m => ({ default: m.Register })));
|
const Register = React.lazy(() => import('./auth').then((m) => ({ default: m.Register })));
|
||||||
const ResetPassword = React.lazy(() => import('./auth').then(m => ({ default: m.ResetPassword })));
|
const ResetPassword = React.lazy(() =>
|
||||||
|
import('./auth').then((m) => ({ default: m.ResetPassword }))
|
||||||
|
);
|
||||||
import {
|
import {
|
||||||
DIRECT_PATH,
|
DIRECT_PATH,
|
||||||
EXPLORE_PATH,
|
EXPLORE_PATH,
|
||||||
@@ -48,9 +50,8 @@ import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
|
|||||||
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
|
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
|
||||||
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
|
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
|
||||||
|
|
||||||
|
|
||||||
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
|
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
|
||||||
const Room = React.lazy(() => import('../features/room').then(m => ({ default: m.Room })));
|
const Room = React.lazy(() => import('../features/room').then((m) => ({ default: m.Room })));
|
||||||
|
|
||||||
import { WelcomePage } from './client/WelcomePage';
|
import { WelcomePage } from './client/WelcomePage';
|
||||||
import { SidebarNav } from './client/SidebarNav';
|
import { SidebarNav } from './client/SidebarNav';
|
||||||
@@ -62,22 +63,38 @@ import { ClientNonUIFeatures } from './client/ClientNonUIFeatures';
|
|||||||
import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
|
import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
|
||||||
import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification';
|
import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification';
|
||||||
import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
|
import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
|
||||||
const RoomSettingsRenderer = React.lazy(() => import('../features/room-settings').then(m => ({ default: m.RoomSettingsRenderer })));
|
const RoomSettingsRenderer = React.lazy(() =>
|
||||||
|
import('../features/room-settings').then((m) => ({ default: m.RoomSettingsRenderer }))
|
||||||
|
);
|
||||||
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
||||||
const SpaceSettingsRenderer = React.lazy(() => import('../features/space-settings').then(m => ({ default: m.SpaceSettingsRenderer })));
|
const SpaceSettingsRenderer = React.lazy(() =>
|
||||||
|
import('../features/space-settings').then((m) => ({ default: m.SpaceSettingsRenderer }))
|
||||||
|
);
|
||||||
import { UserRoomProfileRenderer } from '../components/UserRoomProfileRenderer';
|
import { UserRoomProfileRenderer } from '../components/UserRoomProfileRenderer';
|
||||||
const CreateRoomModalRenderer = React.lazy(() => import('../features/create-room').then(m => ({ default: m.CreateRoomModalRenderer })));
|
const CreateRoomModalRenderer = React.lazy(() =>
|
||||||
|
import('../features/create-room').then((m) => ({ default: m.CreateRoomModalRenderer }))
|
||||||
|
);
|
||||||
import { HomeCreateRoom } from './client/home/CreateRoom';
|
import { HomeCreateRoom } from './client/home/CreateRoom';
|
||||||
import { Create } from './client/create';
|
import { Create } from './client/create';
|
||||||
const CreateSpaceModalRenderer = React.lazy(() => import('../features/create-space').then(m => ({ default: m.CreateSpaceModalRenderer })));
|
const CreateSpaceModalRenderer = React.lazy(() =>
|
||||||
const SearchModalRenderer = React.lazy(() => import('../features/search').then(m => ({ default: m.SearchModalRenderer })));
|
import('../features/create-space').then((m) => ({ default: m.CreateSpaceModalRenderer }))
|
||||||
const Explore = React.lazy(() => import('./client/explore').then(m => ({ default: m.Explore })));
|
);
|
||||||
const FeaturedRooms = React.lazy(() => import('./client/explore').then(m => ({ default: m.FeaturedRooms })));
|
const SearchModalRenderer = React.lazy(() =>
|
||||||
const PublicRooms = React.lazy(() => import('./client/explore').then(m => ({ default: m.PublicRooms })));
|
import('../features/search').then((m) => ({ default: m.SearchModalRenderer }))
|
||||||
const Notifications = React.lazy(() => import('./client/inbox').then(m => ({ default: m.Notifications })));
|
);
|
||||||
const Inbox = React.lazy(() => import('./client/inbox').then(m => ({ default: m.Inbox })));
|
const Explore = React.lazy(() => import('./client/explore').then((m) => ({ default: m.Explore })));
|
||||||
const Invites = React.lazy(() => import('./client/inbox').then(m => ({ default: m.Invites })));
|
const FeaturedRooms = React.lazy(() =>
|
||||||
const Lobby = React.lazy(() => import('../features/lobby').then(m => ({ default: m.Lobby })));
|
import('./client/explore').then((m) => ({ default: m.FeaturedRooms }))
|
||||||
|
);
|
||||||
|
const PublicRooms = React.lazy(() =>
|
||||||
|
import('./client/explore').then((m) => ({ default: m.PublicRooms }))
|
||||||
|
);
|
||||||
|
const Notifications = React.lazy(() =>
|
||||||
|
import('./client/inbox').then((m) => ({ default: m.Notifications }))
|
||||||
|
);
|
||||||
|
const Inbox = React.lazy(() => import('./client/inbox').then((m) => ({ default: m.Inbox })));
|
||||||
|
const Invites = React.lazy(() => import('./client/inbox').then((m) => ({ default: m.Invites })));
|
||||||
|
const Lobby = React.lazy(() => import('../features/lobby').then((m) => ({ default: m.Lobby })));
|
||||||
import { getFallbackSession } from '../state/sessions';
|
import { getFallbackSession } from '../state/sessions';
|
||||||
import { CallStatusRenderer } from './CallStatusRenderer';
|
import { CallStatusRenderer } from './CallStatusRenderer';
|
||||||
import { CallEmbedProvider } from '../components/CallEmbedProvider';
|
import { CallEmbedProvider } from '../components/CallEmbedProvider';
|
||||||
@@ -112,9 +129,30 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route path={LOGIN_PATH} element={<React.Suspense fallback={null}><Login /></React.Suspense>} />
|
<Route
|
||||||
<Route path={REGISTER_PATH} element={<React.Suspense fallback={null}><Register /></React.Suspense>} />
|
path={LOGIN_PATH}
|
||||||
<Route path={RESET_PASSWORD_PATH} element={<React.Suspense fallback={null}><ResetPassword /></React.Suspense>} />
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<Login />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={REGISTER_PATH}
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<Register />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={RESET_PASSWORD_PATH}
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<ResetPassword />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
@@ -149,12 +187,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
</ClientLayout>
|
</ClientLayout>
|
||||||
<CallStatusRenderer />
|
<CallStatusRenderer />
|
||||||
</CallEmbedProvider>
|
</CallEmbedProvider>
|
||||||
<React.Suspense fallback={null}><SearchModalRenderer /></React.Suspense>
|
<React.Suspense fallback={null}>
|
||||||
|
<SearchModalRenderer />
|
||||||
|
</React.Suspense>
|
||||||
<UserRoomProfileRenderer />
|
<UserRoomProfileRenderer />
|
||||||
<React.Suspense fallback={null}><CreateRoomModalRenderer /></React.Suspense>
|
<React.Suspense fallback={null}>
|
||||||
<React.Suspense fallback={null}><CreateSpaceModalRenderer /></React.Suspense>
|
<CreateRoomModalRenderer />
|
||||||
<React.Suspense fallback={null}><RoomSettingsRenderer /></React.Suspense>
|
</React.Suspense>
|
||||||
<React.Suspense fallback={null}><SpaceSettingsRenderer /></React.Suspense>
|
<React.Suspense fallback={null}>
|
||||||
|
<CreateSpaceModalRenderer />
|
||||||
|
</React.Suspense>
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<RoomSettingsRenderer />
|
||||||
|
</React.Suspense>
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<SpaceSettingsRenderer />
|
||||||
|
</React.Suspense>
|
||||||
<ReceiveSelfDeviceVerification />
|
<ReceiveSelfDeviceVerification />
|
||||||
<AutoRestoreBackupOnVerification />
|
<AutoRestoreBackupOnVerification />
|
||||||
</ClientNonUIFeatures>
|
</ClientNonUIFeatures>
|
||||||
@@ -250,7 +298,14 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
element={<WelcomePage />}
|
element={<WelcomePage />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Route path={_LOBBY_PATH} element={<React.Suspense fallback={null}><Lobby /></React.Suspense>} />
|
<Route
|
||||||
|
path={_LOBBY_PATH}
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<Lobby />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path={_SEARCH_PATH} element={<SpaceSearch />} />
|
<Route path={_SEARCH_PATH} element={<SpaceSearch />} />
|
||||||
<Route
|
<Route
|
||||||
path={_ROOM_PATH}
|
path={_ROOM_PATH}
|
||||||
@@ -269,7 +324,9 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
<PageRoot
|
<PageRoot
|
||||||
nav={
|
nav={
|
||||||
<MobileFriendlyPageNav path={EXPLORE_PATH}>
|
<MobileFriendlyPageNav path={EXPLORE_PATH}>
|
||||||
<React.Suspense fallback={null}><Explore /></React.Suspense>
|
<React.Suspense fallback={null}>
|
||||||
|
<Explore />
|
||||||
|
</React.Suspense>
|
||||||
</MobileFriendlyPageNav>
|
</MobileFriendlyPageNav>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -284,8 +341,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
element={<WelcomePage />}
|
element={<WelcomePage />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Route path={_FEATURED_PATH} element={<React.Suspense fallback={null}><FeaturedRooms /></React.Suspense>} />
|
<Route
|
||||||
<Route path={_SERVER_PATH} element={<React.Suspense fallback={null}><PublicRooms /></React.Suspense>} />
|
path={_FEATURED_PATH}
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<FeaturedRooms />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={_SERVER_PATH}
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<PublicRooms />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={CREATE_PATH} element={<Create />} />
|
<Route path={CREATE_PATH} element={<Create />} />
|
||||||
<Route
|
<Route
|
||||||
@@ -294,7 +365,9 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
<PageRoot
|
<PageRoot
|
||||||
nav={
|
nav={
|
||||||
<MobileFriendlyPageNav path={INBOX_PATH}>
|
<MobileFriendlyPageNav path={INBOX_PATH}>
|
||||||
<React.Suspense fallback={null}><Inbox /></React.Suspense>
|
<React.Suspense fallback={null}>
|
||||||
|
<Inbox />
|
||||||
|
</React.Suspense>
|
||||||
</MobileFriendlyPageNav>
|
</MobileFriendlyPageNav>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -309,8 +382,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
element={<WelcomePage />}
|
element={<WelcomePage />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Route path={_NOTIFICATIONS_PATH} element={<React.Suspense fallback={null}><Notifications /></React.Suspense>} />
|
<Route
|
||||||
<Route path={_INVITES_PATH} element={<React.Suspense fallback={null}><Invites /></React.Suspense>} />
|
path={_NOTIFICATIONS_PATH}
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<Notifications />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={_INVITES_PATH}
|
||||||
|
element={
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<Invites />
|
||||||
|
</React.Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/*" element={<p>Page not found</p>} />
|
<Route path="/*" element={<p>Page not found</p>} />
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function UnAuthRouteThemeManager() {
|
|||||||
if (lotusTerminal) {
|
if (lotusTerminal) {
|
||||||
const isLight = systemThemeKind === ThemeKind.Light;
|
const isLight = systemThemeKind === ThemeKind.Light;
|
||||||
document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark');
|
document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark');
|
||||||
document.body.classList.add(...(isLight ? LotusTerminalLightTheme : LotusTerminalTheme).classNames);
|
document.body.classList.add(
|
||||||
|
...(isLight ? LotusTerminalLightTheme : LotusTerminalTheme).classNames
|
||||||
|
);
|
||||||
document.body.classList.add(lotusTerminalBodyClass);
|
document.body.classList.add(lotusTerminalBodyClass);
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.removeAttribute('data-theme');
|
document.documentElement.removeAttribute('data-theme');
|
||||||
@@ -47,7 +49,9 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const terminalIsLight = lotusTerminal && activeTheme.kind === ThemeKind.Light;
|
const terminalIsLight = lotusTerminal && activeTheme.kind === ThemeKind.Light;
|
||||||
const effectiveTheme = lotusTerminal
|
const effectiveTheme = lotusTerminal
|
||||||
? (terminalIsLight ? LotusTerminalLightTheme : LotusTerminalTheme)
|
? terminalIsLight
|
||||||
|
? LotusTerminalLightTheme
|
||||||
|
: LotusTerminalTheme
|
||||||
: activeTheme;
|
: activeTheme;
|
||||||
|
|
||||||
// Boot animation only fires when lotusTerminal is toggled on, not on every theme change
|
// Boot animation only fires when lotusTerminal is toggled on, not on every theme change
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ export function AuthFooter() {
|
|||||||
>
|
>
|
||||||
v{pkg.version}
|
v{pkg.version}
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="a" size="T300" href="https://matrix.lotusguild.org" target="_blank" rel="noreferrer">
|
<Text
|
||||||
|
as="a"
|
||||||
|
size="T300"
|
||||||
|
href="https://matrix.lotusguild.org"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
Community
|
Community
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="a" size="T300" href="https://matrix.org" target="_blank" rel="noreferrer">
|
<Text as="a" size="T300" href="https://matrix.org" target="_blank" rel="noreferrer">
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ export function AuthLayout() {
|
|||||||
<Header className={css.AuthHeader} size="600" variant="Surface">
|
<Header className={css.AuthHeader} size="600" variant="Surface">
|
||||||
<Box grow="Yes" direction="Row" gap="300" alignItems="Center">
|
<Box grow="Yes" direction="Row" gap="300" alignItems="Center">
|
||||||
<img className={css.AuthLogo} src={LotusLogo} alt="Lotus Chat Logo" />
|
<img className={css.AuthLogo} src={LotusLogo} alt="Lotus Chat Logo" />
|
||||||
<Text as="h1" size="H3">Lotus Chat</Text>
|
<Text as="h1" size="H3">
|
||||||
|
Lotus Chat
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Header>
|
</Header>
|
||||||
<Box className={css.AuthCardContent} direction="Column">
|
<Box className={css.AuthCardContent} direction="Column">
|
||||||
|
|||||||
@@ -230,7 +230,15 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
|
|||||||
<Text as="label" htmlFor="passwordInput" size="L400" priority="300">
|
<Text as="label" htmlFor="passwordInput" size="L400" priority="300">
|
||||||
Password
|
Password
|
||||||
</Text>
|
</Text>
|
||||||
<PasswordInput id="passwordInput" name="passwordInput" aria-label="Password" variant="Background" size="500" outlined required />
|
<PasswordInput
|
||||||
|
id="passwordInput"
|
||||||
|
name="passwordInput"
|
||||||
|
aria-label="Password"
|
||||||
|
variant="Background"
|
||||||
|
size="500"
|
||||||
|
outlined
|
||||||
|
required
|
||||||
|
/>
|
||||||
<Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
|
<Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
|
||||||
{loginState.status === AsyncStatus.Error && (
|
{loginState.status === AsyncStatus.Error && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ export const useLoginComplete = (data?: CustomLoginResponse) => {
|
|||||||
const afterLoginRedirectUrl = getAfterLoginRedirectPath();
|
const afterLoginRedirectUrl = getAfterLoginRedirectPath();
|
||||||
deleteAfterLoginRedirectPath();
|
deleteAfterLoginRedirectPath();
|
||||||
const _redir = afterLoginRedirectUrl;
|
const _redir = afterLoginRedirectUrl;
|
||||||
const _safePath = (_redir && /^\/(?!\/)/.test(_redir)) ? _redir : getHomePath();
|
const _safePath = _redir && /^\/(?!\/)/.test(_redir) ? _redir : getHomePath();
|
||||||
navigate(_safePath, { replace: true });
|
navigate(_safePath, { replace: true });
|
||||||
}
|
}
|
||||||
}, [data, navigate]);
|
}, [data, navigate]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,14 +21,22 @@ export function ClientLayout({ nav, children }: ClientLayoutProps) {
|
|||||||
borderRadius: '0 0 4px 0',
|
borderRadius: '0 0 4px 0',
|
||||||
transition: 'top 0.1s',
|
transition: 'top 0.1s',
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => { (e.currentTarget as HTMLElement).style.top = '0'; }}
|
onFocus={(e) => {
|
||||||
onBlur={(e) => { (e.currentTarget as HTMLElement).style.top = '-40px'; }}
|
(e.currentTarget as HTMLElement).style.top = '0';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
(e.currentTarget as HTMLElement).style.top = '-40px';
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Skip to main content
|
Skip to main content
|
||||||
</a>
|
</a>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Box shrink="No" as="nav" aria-label="Room navigation">{nav}</Box>
|
<Box shrink="No" as="nav" aria-label="Room navigation">
|
||||||
<Box grow="Yes" as="main" id="main-content">{children}</Box>
|
{nav}
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" as="main" id="main-content">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export function SpecVersions({ baseUrl, children }: { baseUrl: string; children:
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
||||||
<Text>
|
<Text>
|
||||||
Unable to connect to the homeserver. The homeserver or your internet connection may be down.
|
Unable to connect to the homeserver. The homeserver or your internet connection
|
||||||
|
may be down.
|
||||||
</Text>
|
</Text>
|
||||||
<Button variant="Critical" onClick={retry}>
|
<Button variant="Critical" onClick={retry}>
|
||||||
<Text as="span" size="B400">
|
<Text as="span" size="B400">
|
||||||
|
|||||||
@@ -15,7 +15,15 @@ export function WelcomePage() {
|
|||||||
>
|
>
|
||||||
<PageHeroSection>
|
<PageHeroSection>
|
||||||
<PageHero
|
<PageHero
|
||||||
icon={<img width="70" height="70" src={LotusLogo} alt="Lotus Chat" style={{ objectFit: "contain" }} />}
|
icon={
|
||||||
|
<img
|
||||||
|
width="70"
|
||||||
|
height="70"
|
||||||
|
src={LotusLogo}
|
||||||
|
alt="Lotus Chat"
|
||||||
|
style={{ objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
title="Welcome to Lotus Chat"
|
title="Welcome to Lotus Chat"
|
||||||
subTitle={
|
subTitle={
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -108,7 +108,13 @@ function DirectHeader() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<IconButton aria-expanded={!!menuAnchor} aria-haspopup="menu" variant="Background" onClick={handleOpenMenu} aria-label="Direct messages options">
|
<IconButton
|
||||||
|
aria-expanded={!!menuAnchor}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
variant="Background"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
aria-label="Direct messages options"
|
||||||
|
>
|
||||||
<Icon src={Icons.VerticalDots} size="200" />
|
<Icon src={Icons.VerticalDots} size="200" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -94,9 +94,16 @@ export function AddServer() {
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text as="h2" size="H4">Add Server</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Add Server
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={() => setDialog(false)} radii="300" aria-label="Close">
|
<IconButton
|
||||||
|
size="300"
|
||||||
|
onClick={() => setDialog(false)}
|
||||||
|
radii="300"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -110,7 +117,13 @@ export function AddServer() {
|
|||||||
<Text priority="400">Add server name to explore public communities.</Text>
|
<Text priority="400">Add server name to explore public communities.</Text>
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">Server Name</Text>
|
<Text size="L400">Server Name</Text>
|
||||||
<Input ref={serverInputRef} name="serverInput" aria-label="Server name" variant="Background" required />
|
<Input
|
||||||
|
ref={serverInputRef}
|
||||||
|
name="serverInput"
|
||||||
|
aria-label="Server name"
|
||||||
|
variant="Background"
|
||||||
|
required
|
||||||
|
/>
|
||||||
{exploreState.status === AsyncStatus.Error && (
|
{exploreState.status === AsyncStatus.Error && (
|
||||||
<Text style={{ color: color.Critical.Main }} size="T300">
|
<Text style={{ color: color.Critical.Main }} size="T300">
|
||||||
Failed to load public rooms. Please try again.
|
Failed to load public rooms. Please try again.
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ export function FeaturedRooms() {
|
|||||||
<Box direction="Column" gap="700">
|
<Box direction="Column" gap="700">
|
||||||
{spaces && spaces.length > 0 && (
|
{spaces && spaces.length > 0 && (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text as="h2" size="H4">Featured Spaces</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Featured Spaces
|
||||||
|
</Text>
|
||||||
<RoomCardGrid>
|
<RoomCardGrid>
|
||||||
{spaces.map((roomIdOrAlias) => (
|
{spaces.map((roomIdOrAlias) => (
|
||||||
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
||||||
@@ -85,7 +87,9 @@ export function FeaturedRooms() {
|
|||||||
)}
|
)}
|
||||||
{rooms && rooms.length > 0 && (
|
{rooms && rooms.length > 0 && (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text as="h3" size="H4">Featured Rooms</Text>
|
<Text as="h3" size="H4">
|
||||||
|
Featured Rooms
|
||||||
|
</Text>
|
||||||
<RoomCardGrid>
|
<RoomCardGrid>
|
||||||
{rooms.map((roomIdOrAlias) => (
|
{rooms.map((roomIdOrAlias) => (
|
||||||
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
||||||
|
|||||||
@@ -537,7 +537,9 @@ export function PublicRooms() {
|
|||||||
{isSearch ? (
|
{isSearch ? (
|
||||||
<Text as="h3" size="H4">{`Results for "${serverSearchParams.term}"`}</Text>
|
<Text as="h3" size="H4">{`Results for "${serverSearchParams.term}"`}</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text as="h3" size="H4">Popular Communities</Text>
|
<Text as="h3" size="H4">
|
||||||
|
Popular Communities
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Box gap="200">
|
<Box gap="200">
|
||||||
{roomTypeFilters.map((filter) => (
|
{roomTypeFilters.map((filter) => (
|
||||||
|
|||||||
@@ -122,7 +122,13 @@ function HomeHeader() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<IconButton aria-expanded={!!menuAnchor} aria-haspopup="menu" variant="Background" onClick={handleOpenMenu} aria-label="Home options">
|
<IconButton
|
||||||
|
aria-expanded={!!menuAnchor}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
variant="Background"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
aria-label="Home options"
|
||||||
|
>
|
||||||
<Icon src={Icons.VerticalDots} size="200" />
|
<Icon src={Icons.VerticalDots} size="200" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -429,7 +429,9 @@ function KnownInvites({
|
|||||||
}: KnownInvitesProps) {
|
}: KnownInvitesProps) {
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Text as="h3" size="H4">Primary</Text>
|
<Text as="h3" size="H4">
|
||||||
|
Primary
|
||||||
|
</Text>
|
||||||
{invites.length > 0 ? (
|
{invites.length > 0 ? (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
{invites.map((invite) => (
|
{invites.map((invite) => (
|
||||||
@@ -488,7 +490,9 @@ function UnknownInvites({
|
|||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
|
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
|
||||||
<Text as="h3" size="H4">Public</Text>
|
<Text as="h3" size="H4">
|
||||||
|
Public
|
||||||
|
</Text>
|
||||||
<Box>
|
<Box>
|
||||||
{invites.length > 0 && (
|
{invites.length > 0 && (
|
||||||
<Chip
|
<Chip
|
||||||
@@ -585,7 +589,9 @@ function SpamInvites({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Text as="h3" size="H4">Spam</Text>
|
<Text as="h3" size="H4">
|
||||||
|
Spam
|
||||||
|
</Text>
|
||||||
{invites.length > 0 ? (
|
{invites.length > 0 ? (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
|
|||||||
@@ -522,7 +522,13 @@ function OpenedSpaceFolder({ folder, onClose, children }: OpenedSpaceFolderProps
|
|||||||
>
|
>
|
||||||
<SidebarFolderDropTarget ref={aboveTargetRef} position="Top" />
|
<SidebarFolderDropTarget ref={aboveTargetRef} position="Top" />
|
||||||
<SidebarAvatar size="300">
|
<SidebarAvatar size="300">
|
||||||
<IconButton data-id={folder.id} size="300" variant="Background" onClick={onClose} aria-label="Close folder">
|
<IconButton
|
||||||
|
data-id={folder.id}
|
||||||
|
size="300"
|
||||||
|
variant="Background"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close folder"
|
||||||
|
>
|
||||||
<Icon size="400" src={Icons.ChevronTop} filled />
|
<Icon size="400" src={Icons.ChevronTop} filled />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</SidebarAvatar>
|
</SidebarAvatar>
|
||||||
|
|||||||
@@ -274,7 +274,13 @@ function SpaceHeader() {
|
|||||||
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
|
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton aria-expanded={!!menuAnchor} aria-haspopup="menu" variant="Background" onClick={handleOpenMenu} aria-label="Space options">
|
<IconButton
|
||||||
|
aria-expanded={!!menuAnchor}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
variant="Background"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
aria-label="Space options"
|
||||||
|
>
|
||||||
<Icon src={Icons.VerticalDots} size="200" />
|
<Icon src={Icons.VerticalDots} size="200" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -433,7 +439,9 @@ export function Space() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const showRoomAnyway =
|
const showRoomAnyway =
|
||||||
roomsWithUnreadSet.has(roomId) || roomId === selectedRoomId || callEmbed?.roomId === roomId;
|
roomsWithUnreadSet.has(roomId) ||
|
||||||
|
roomId === selectedRoomId ||
|
||||||
|
callEmbed?.roomId === roomId;
|
||||||
return !showRoomAnyway;
|
return !showRoomAnyway;
|
||||||
},
|
},
|
||||||
[space.roomId, closedCategories, roomsWithUnreadSet, selectedRoomId, callEmbed]
|
[space.roomId, closedCategories, roomsWithUnreadSet, selectedRoomId, callEmbed]
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
|
|
||||||
private get settingsButton(): HTMLElement | undefined {
|
private get settingsButton(): HTMLElement | undefined {
|
||||||
// EC 0.19.3: settings button has data-testid="settings-bottom-center"
|
// EC 0.19.3: settings button has data-testid="settings-bottom-center"
|
||||||
return (this.document?.querySelector('[data-testid="settings-bottom-center"]') as HTMLElement) ?? undefined;
|
return (
|
||||||
|
(this.document?.querySelector('[data-testid="settings-bottom-center"]') as HTMLElement) ??
|
||||||
|
undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get reactionsButton(): HTMLElement | undefined {
|
private get reactionsButton(): HTMLElement | undefined {
|
||||||
@@ -98,7 +101,13 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async forceState(desired: CallControlState) {
|
public async forceState(desired: CallControlState) {
|
||||||
this.state = new CallControlState(desired.microphone, desired.video, desired.sound, this.screenshare, this.spotlight);
|
this.state = new CallControlState(
|
||||||
|
desired.microphone,
|
||||||
|
desired.video,
|
||||||
|
desired.sound,
|
||||||
|
this.screenshare,
|
||||||
|
this.spotlight
|
||||||
|
);
|
||||||
await this.applyState();
|
await this.applyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +186,9 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
|
|
||||||
// EC auto-switches to spotlight when screenshare starts — revert to grid
|
// EC auto-switches to spotlight when screenshare starts — revert to grid
|
||||||
if (!prevScreenshare && screenshare) {
|
if (!prevScreenshare && screenshare) {
|
||||||
setTimeout(() => { if (this.spotlight) this.gridButton?.click(); }, 600);
|
setTimeout(() => {
|
||||||
|
if (this.spotlight) this.gridButton?.click();
|
||||||
|
}, 600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -462,7 +462,9 @@ export const getReactCustomHtmlParser = (
|
|||||||
{...props}
|
{...props}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') params.handleSpoilerClick?.(e as any); }}
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') params.handleSpoilerClick?.(e as any);
|
||||||
|
}}
|
||||||
onClick={params.handleSpoilerClick}
|
onClick={params.handleSpoilerClick}
|
||||||
className={css.Spoiler()}
|
className={css.Spoiler()}
|
||||||
aria-label="Spoiler — click to reveal"
|
aria-label="Spoiler — click to reveal"
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ export type ListAction<T> =
|
|||||||
item: T | T[];
|
item: T | T[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'REPLACE';
|
type: 'REPLACE';
|
||||||
item: T;
|
item: T;
|
||||||
replacement: T;
|
replacement: T;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'DELETE';
|
type: 'DELETE';
|
||||||
@@ -34,9 +34,12 @@ export const createListAtom = <T>() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action.type === 'REPLACE') {
|
if (action.type === 'REPLACE') {
|
||||||
set(baseListAtom, items.map((item) => item === action.item ? action.replacement : item));
|
set(
|
||||||
|
baseListAtom,
|
||||||
|
items.map((item) => (item === action.item ? action.replacement : item))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export type TListAtom<T> = ReturnType<typeof createListAtom<T>>;
|
export type TListAtom<T> = ReturnType<typeof createListAtom<T>>;
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ export type TUploadItem = {
|
|||||||
|
|
||||||
export type TUploadListAtom = ReturnType<typeof createListAtom<TUploadItem>>;
|
export type TUploadListAtom = ReturnType<typeof createListAtom<TUploadItem>>;
|
||||||
|
|
||||||
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TUploadListAtom>(
|
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TUploadListAtom>(createListAtom);
|
||||||
createListAtom
|
|
||||||
);
|
|
||||||
|
|
||||||
export const roomUploadAtomFamily = createUploadAtomFamily();
|
export const roomUploadAtomFamily = createUploadAtomFamily();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,24 @@ export type DateFormat =
|
|||||||
| 'YYYY-MM-DD'
|
| 'YYYY-MM-DD'
|
||||||
| '';
|
| '';
|
||||||
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
|
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
|
||||||
export type ChatBackground = 'none' | 'blueprint' | 'carbon' | 'stars' | 'topographic' | 'herringbone' | 'crosshatch' | 'chevron' | 'polka' | 'triangles' | 'plaid' | 'tactical' | 'circuit' | 'hexgrid' | 'waves' | 'neon' | 'aurora';
|
export type ChatBackground =
|
||||||
|
| 'none'
|
||||||
|
| 'blueprint'
|
||||||
|
| 'carbon'
|
||||||
|
| 'stars'
|
||||||
|
| 'topographic'
|
||||||
|
| 'herringbone'
|
||||||
|
| 'crosshatch'
|
||||||
|
| 'chevron'
|
||||||
|
| 'polka'
|
||||||
|
| 'triangles'
|
||||||
|
| 'plaid'
|
||||||
|
| 'tactical'
|
||||||
|
| 'circuit'
|
||||||
|
| 'hexgrid'
|
||||||
|
| 'waves'
|
||||||
|
| 'neon'
|
||||||
|
| 'aurora';
|
||||||
export enum MessageLayout {
|
export enum MessageLayout {
|
||||||
Modern = 0,
|
Modern = 0,
|
||||||
Compact = 1,
|
Compact = 1,
|
||||||
@@ -114,7 +131,11 @@ export const getSettings = (): Settings => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setSettings = (settings: Settings) => {
|
export const setSettings = (settings: Settings) => {
|
||||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } catch { /* quota */ }
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||||
|
} catch {
|
||||||
|
/* quota */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseSettings = atom<Settings>(getSettings());
|
const baseSettings = atom<Settings>(getSettings());
|
||||||
|
|||||||
@@ -84,7 +84,15 @@ const transformFontTag: Transformer = (tagName, attribs) => ({
|
|||||||
tagName,
|
tagName,
|
||||||
attribs: {
|
attribs: {
|
||||||
...attribs,
|
...attribs,
|
||||||
style: `${attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color']) ? `background-color: ${attribs['data-mx-bg-color']};` : ''} ${attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color']) ? `color: ${attribs['data-mx-color']}` : ''}`.trim(),
|
style: `${
|
||||||
|
attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color'])
|
||||||
|
? `background-color: ${attribs['data-mx-bg-color']};`
|
||||||
|
: ''
|
||||||
|
} ${
|
||||||
|
attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color'])
|
||||||
|
? `color: ${attribs['data-mx-color']}`
|
||||||
|
: ''
|
||||||
|
}`.trim(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,7 +100,15 @@ const transformSpanTag: Transformer = (tagName, attribs) => ({
|
|||||||
tagName,
|
tagName,
|
||||||
attribs: {
|
attribs: {
|
||||||
...attribs,
|
...attribs,
|
||||||
style: `${attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color']) ? `background-color: ${attribs['data-mx-bg-color']};` : ''} ${attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color']) ? `color: ${attribs['data-mx-color']}` : ''}`.trim(),
|
style: `${
|
||||||
|
attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color'])
|
||||||
|
? `background-color: ${attribs['data-mx-bg-color']};`
|
||||||
|
: ''
|
||||||
|
} ${
|
||||||
|
attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color'])
|
||||||
|
? `color: ${attribs['data-mx-color']}`
|
||||||
|
: ''
|
||||||
|
}`.trim(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -421,4 +421,3 @@ export const lotusTerminalLightTheme = createTheme(color, {
|
|||||||
Overlay: 'rgba(237, 240, 245, 0.97)',
|
Overlay: 'rgba(237, 240, 245, 0.97)',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user