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:
+1
-3
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": [
|
||||
"matrix.lotusguild.org"
|
||||
],
|
||||
"homeserverList": ["matrix.lotusguild.org"],
|
||||
"allowCustomHomeservers": false,
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
|
||||
+24
-30
@@ -4,15 +4,15 @@ module.exports = {
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'airbnb',
|
||||
'prettier',
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
@@ -20,46 +20,40 @@ module.exports = {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
"globals": {
|
||||
JSX: "readonly"
|
||||
globals: {
|
||||
JSX: 'readonly',
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
'@typescript-eslint'
|
||||
],
|
||||
plugins: ['react', '@typescript-eslint'],
|
||||
rules: {
|
||||
'linebreak-style': 0,
|
||||
'no-underscore-dangle': 0,
|
||||
"no-shadow": "off",
|
||||
'no-shadow': 'off',
|
||||
|
||||
"import/prefer-default-export": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
|
||||
'react/no-unstable-nested-components': [
|
||||
'react/no-unstable-nested-components': ['error', { allowAsProps: true }],
|
||||
'react/jsx-filename-extension': [
|
||||
'error',
|
||||
{ allowAsProps: true },
|
||||
],
|
||||
"react/jsx-filename-extension": [
|
||||
"error",
|
||||
{
|
||||
extensions: [".tsx", ".jsx"],
|
||||
extensions: ['.tsx', '.jsx'],
|
||||
},
|
||||
],
|
||||
|
||||
"react/require-default-props": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
'react/require-default-props': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-shadow": "error"
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
SENTRY_AUTH_TOKEN: ""
|
||||
VITE_APP_VERSION: ${{ github.sha }}
|
||||
|
||||
# ── Quality checks (informational — pre-existing issues exist) ───────
|
||||
- name: TypeScript
|
||||
@@ -41,7 +42,6 @@ jobs:
|
||||
|
||||
- name: Prettier
|
||||
run: npm run check:prettier
|
||||
continue-on-error: true
|
||||
|
||||
# ── Security ─────────────────────────────────────────────────────────
|
||||
- name: Audit (high/critical)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
labels: ["needs-confirmation"]
|
||||
labels: ['needs-confirmation']
|
||||
body:
|
||||
- type: markdown #add faqs in future
|
||||
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.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# Issue Details"
|
||||
value: '# Issue Details'
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue 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).
|
||||
- type: checkboxes #add faqs in future
|
||||
attributes:
|
||||
label: "I acknowledge that:"
|
||||
label: 'I acknowledge that:'
|
||||
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.
|
||||
required: true
|
||||
|
||||
+14
-14
@@ -2,29 +2,29 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# - package-ecosystem: npm
|
||||
# directory: /
|
||||
# schedule:
|
||||
# interval: weekly
|
||||
# day: "tuesday"
|
||||
# time: "01:00"
|
||||
# timezone: "Asia/Kolkata"
|
||||
# open-pull-requests-limit: 15
|
||||
# - package-ecosystem: npm
|
||||
# directory: /
|
||||
# schedule:
|
||||
# interval: weekly
|
||||
# day: "tuesday"
|
||||
# time: "01:00"
|
||||
# timezone: "Asia/Kolkata"
|
||||
# open-pull-requests-limit: 15
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
day: 'tuesday'
|
||||
time: '01:00'
|
||||
timezone: 'Asia/Kolkata'
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
day: 'tuesday'
|
||||
time: '01:00'
|
||||
timezone: 'Asia/Kolkata'
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
node-version-file: '.node-version'
|
||||
package-manager-cache: false
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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:
|
||||
workflow_run:
|
||||
workflows: ["Build pull request"]
|
||||
types: [completed]
|
||||
workflows: ['Build pull request']
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
deploy-pull-request:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||
with:
|
||||
publish-dir: dist
|
||||
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
||||
deploy-message: 'Deploy PR ${{ steps.pr.outputs.id }}'
|
||||
alias: ${{ steps.pr.outputs.id }}
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
@@ -59,5 +59,5 @@ jobs:
|
||||
pr-number: ${{ steps.pr.outputs.id }}
|
||||
comment-tag: ${{ steps.pr.outputs.id }}
|
||||
message: |
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
node-version-file: '.node-version'
|
||||
package-manager-cache: false
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
node-version-file: '.node-version'
|
||||
package-manager-cache: false
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
+11
-11
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
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
|
||||
|
||||
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
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
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
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**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.
|
||||
|
||||
**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. 🎉
|
||||
|
||||
> 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
|
||||
> - Tweet about it (tag @cinnyapp)
|
||||
> - 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
|
||||
|
||||
> ### 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.
|
||||
|
||||
**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:
|
||||
|
||||
|Not ideal|Better|
|
||||
|---|----|
|
||||
|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
|
||||
| Not ideal | Better |
|
||||
| ----------------------------------- | --------------------------------------------- |
|
||||
| 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.
|
||||
|
||||
@@ -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).**
|
||||
|
||||
## Helpful links
|
||||
|
||||
- [BEM methodology](http://getbem.com/introduction/)
|
||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": [
|
||||
"matrix.lotusguild.org"
|
||||
],
|
||||
"homeserverList": ["matrix.lotusguild.org"],
|
||||
"allowCustomHomeservers": false,
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
|
||||
+11
-8
@@ -11,15 +11,15 @@
|
||||
name="description"
|
||||
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="lotus chat, lotus guild, matrix, matrix client"
|
||||
/>
|
||||
<meta name="keywords" content="lotus chat, lotus guild, matrix, matrix client" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Lotus Chat" />
|
||||
<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
|
||||
property="og:description"
|
||||
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="color-scheme" content="dark light" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.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 rel="preconnect" href="https://fonts.googleapis.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 id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||
|
||||
<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",
|
||||
"@giphy/js-fetch-api": "5.8.0",
|
||||
"@giphy/js-types": "5.1.0",
|
||||
"@giphy/react-components": "10.1.2",
|
||||
"@giphy/react-components": "1.6.0",
|
||||
"@sentry/react": "10.53.1",
|
||||
"@tanstack/react-query": "5.24.1",
|
||||
"@tanstack/react-query-devtools": "5.24.1",
|
||||
@@ -162,6 +162,6 @@
|
||||
"vite": "6.4.2",
|
||||
"vite-plugin-pwa": "1.3.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,
|
||||
"homeserverList": [
|
||||
"matrix.lotusguild.org"
|
||||
],
|
||||
"homeserverList": ["matrix.lotusguild.org"],
|
||||
"allowCustomHomeservers": false,
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
|
||||
@@ -56,11 +56,7 @@
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
"social",
|
||||
"communication",
|
||||
"productivity"
|
||||
],
|
||||
"categories": ["social", "communication", "productivity"],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "New Message",
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -9,26 +9,26 @@ const __dirname = path.dirname(__filename);
|
||||
const version = process.argv[2];
|
||||
|
||||
if (!version) {
|
||||
console.error("Version argument missing");
|
||||
console.error('Version argument missing');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const newVersionTag = `v${version}`;
|
||||
|
||||
// Update package.json + package-lock.json safely
|
||||
execSync(`npm version ${version} --no-git-tag-version`, {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
console.log(`Updated package.json and package-lock.json → ${version}`);
|
||||
|
||||
// Update UI version references
|
||||
const files = [
|
||||
"src/app/features/settings/about/About.tsx",
|
||||
"src/app/pages/auth/AuthFooter.tsx",
|
||||
"src/app/pages/client/WelcomePage.tsx",
|
||||
'src/app/features/settings/about/About.tsx',
|
||||
'src/app/pages/auth/AuthFooter.tsx',
|
||||
'src/app/pages/client/WelcomePage.tsx',
|
||||
];
|
||||
|
||||
files.forEach((filePath) => {
|
||||
@@ -39,7 +39,7 @@ files.forEach((filePath) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(absPath, "utf8");
|
||||
const content = fs.readFileSync(absPath, 'utf8');
|
||||
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
|
||||
|
||||
fs.writeFileSync(absPath, updated);
|
||||
|
||||
@@ -137,7 +137,10 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
||||
|
||||
useEffect(() => {
|
||||
const remaining = info.senderTs + info.lifetime - Date.now();
|
||||
if (remaining <= 0) { onIgnore(); return; }
|
||||
if (remaining <= 0) {
|
||||
onIgnore();
|
||||
return;
|
||||
}
|
||||
const id = setTimeout(onIgnore, remaining);
|
||||
return () => clearTimeout(id);
|
||||
}, [info.senderTs, info.lifetime, onIgnore]);
|
||||
@@ -157,7 +160,9 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
||||
<Dialog style={{ maxWidth: toRem(324) }}>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
|
||||
<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>
|
||||
<Box direction="Column" gap="500" alignItems="Center">
|
||||
<Box shrink="No">
|
||||
@@ -294,7 +299,8 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
const refEventId = relation?.event_id;
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@@ -410,10 +416,19 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
||||
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);
|
||||
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)
|
||||
const prevPipModeRef = React.useRef(false);
|
||||
@@ -422,16 +437,35 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
if (!el) return;
|
||||
if (pipMode) {
|
||||
if (!prevPipModeRef.current) {
|
||||
el.style.top = 'auto'; el.style.left = 'auto';
|
||||
el.style.bottom = '72px'; el.style.right = '16px';
|
||||
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.top = 'auto';
|
||||
el.style.left = 'auto';
|
||||
el.style.bottom = '72px';
|
||||
el.style.right = '16px';
|
||||
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.visibility = 'visible';
|
||||
} 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';
|
||||
}
|
||||
prevPipModeRef.current = pipMode;
|
||||
@@ -444,30 +478,66 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
if (!el) return;
|
||||
const l = parseFloat(el.style.left);
|
||||
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(t)) el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
|
||||
if (!isNaN(l))
|
||||
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);
|
||||
return () => window.removeEventListener('resize', onPipWindowResize);
|
||||
}, [pipMode, callEmbedRef]);
|
||||
|
||||
const handlePipMouseDown = (e: React.MouseEvent) => {
|
||||
const el = callEmbedRef.current; if (!el) return;
|
||||
const el = callEmbedRef.current;
|
||||
if (!el) return;
|
||||
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) => {
|
||||
if (!pipDragRef.current || !el) return;
|
||||
const dx = ev.clientX - pipDragRef.current.startX, 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'; }
|
||||
const dx = ev.clientX - pipDragRef.current.startX,
|
||||
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) {
|
||||
el.style.left = `${Math.max(0, 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';
|
||||
el.style.left = `${Math.max(
|
||||
0,
|
||||
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); };
|
||||
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 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);
|
||||
};
|
||||
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) => {
|
||||
@@ -475,50 +545,115 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
if (!el || e.touches.length !== 1) return;
|
||||
const touch = e.touches[0];
|
||||
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) => {
|
||||
if (!pipDragRef.current || !el || ev.touches.length !== 1) return;
|
||||
ev.preventDefault();
|
||||
const t = ev.touches[0];
|
||||
const dx = t.clientX - pipDragRef.current.startX;
|
||||
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) {
|
||||
el.style.left = `${Math.max(0, 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';
|
||||
el.style.left = `${Math.max(
|
||||
0,
|
||||
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 = () => {
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
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('touchend', onTouchEnd);
|
||||
};
|
||||
|
||||
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
|
||||
e.stopPropagation(); e.preventDefault();
|
||||
const el = callEmbedRef.current; if (!el) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const el = callEmbedRef.current;
|
||||
if (!el) return;
|
||||
normaliseToTopLeft(el);
|
||||
const sx = e.clientX, sy = e.clientY, 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 sx = e.clientX,
|
||||
sy = e.clientY,
|
||||
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 dx = ev.clientX-sx, dy = ev.clientY-sy;
|
||||
let w = sw, h = sh, l = sl, t = st;
|
||||
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 dx = ev.clientX - sx,
|
||||
dy = ev.clientY - sy;
|
||||
let w = sw,
|
||||
h = sh,
|
||||
l = sl,
|
||||
t = st;
|
||||
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; };
|
||||
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 onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
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 (
|
||||
@@ -548,25 +683,71 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
aria-label="Return to call"
|
||||
onMouseDown={handlePipMouseDown}
|
||||
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)}
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
{(['se','sw','ne','nw'] as Corner[]).map((corner) => {
|
||||
const s = corner.includes('s'); const e2 = corner.includes('e');
|
||||
const dots = [[2,2],[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,
|
||||
{(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
|
||||
const s = corner.includes('s');
|
||||
const e2 = corner.includes('e');
|
||||
const dots = [
|
||||
[2, 2],
|
||||
[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 (
|
||||
<div key={corner} 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
|
||||
key={corner}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -259,9 +259,16 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
|
||||
<Dialog variant="Surface">
|
||||
<Header style={DialogHeaderStyles} variant="Surface" size="500">
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Device Verification</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Device Verification
|
||||
</Text>
|
||||
</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} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -299,7 +299,9 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Setup Device Verification</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Setup Device Verification
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
@@ -334,7 +336,9 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Reset Device Verification</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Reset Device Verification
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -8,7 +8,6 @@ import { settingsAtom } from '../state/settings';
|
||||
|
||||
const PICKER_WIDTH = 312;
|
||||
|
||||
|
||||
type GifPickerInnerProps = {
|
||||
onSelect: (url: string, width: number, height: number) => void;
|
||||
requestClose: () => void;
|
||||
@@ -34,23 +33,27 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
|
||||
return (
|
||||
<Box direction="Column" style={{ width: `${PICKER_WIDTH}px` }}>
|
||||
{lotusTerminal && (
|
||||
<div style={{
|
||||
padding: '5px 10px 4px',
|
||||
borderBottom: '1px solid rgba(255,107,0,0.2)',
|
||||
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.1em',
|
||||
color: '#FF6B00',
|
||||
userSelect: 'none',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
padding: '5px 10px 4px',
|
||||
borderBottom: '1px solid rgba(255,107,0,0.2)',
|
||||
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.1em',
|
||||
color: '#FF6B00',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
// GIF_SEARCH
|
||||
</div>
|
||||
)}
|
||||
<Box style={{ padding: '8px 8px 4px' }}>
|
||||
<SearchBar style={{ width: '100%', borderRadius: lotusTerminal ? '4px' : '8px' }} />
|
||||
</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
|
||||
key={searchKey}
|
||||
fetchGifs={fetchGifs}
|
||||
@@ -108,7 +111,11 @@ export function GifPicker({ apiKey, onSelect, requestClose }: GifPickerProps) {
|
||||
style={containerStyle}
|
||||
>
|
||||
<SearchContextManager apiKey={apiKey} initialTerm="">
|
||||
<GifPickerInner onSelect={onSelect} requestClose={requestClose} lotusTerminal={!!lotusTerminal} />
|
||||
<GifPickerInner
|
||||
onSelect={onSelect}
|
||||
requestClose={requestClose}
|
||||
lotusTerminal={!!lotusTerminal}
|
||||
/>
|
||||
</SearchContextManager>
|
||||
</Box>
|
||||
</FocusTrap>
|
||||
|
||||
@@ -43,7 +43,9 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Logout</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Logout
|
||||
</Text>
|
||||
</Box>
|
||||
</Header>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
|
||||
@@ -22,7 +22,8 @@ export function RoomSkeleton() {
|
||||
`;
|
||||
|
||||
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%',
|
||||
animation: `shimmer-${id} 1.6s ease-in-out infinite`,
|
||||
borderRadius: '4px',
|
||||
@@ -32,16 +33,18 @@ export function RoomSkeleton() {
|
||||
<>
|
||||
<style>{shimmerKeyframes}</style>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
// CSS vars resolve against both light and dark themes automatically
|
||||
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||
} as React.CSSProperties}
|
||||
style={
|
||||
{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
// CSS vars resolve against both light and dark themes automatically
|
||||
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* Header — matches PageHeader size="600" (56px) */}
|
||||
<div
|
||||
@@ -56,7 +59,15 @@ export function RoomSkeleton() {
|
||||
}}
|
||||
>
|
||||
{/* 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 */}
|
||||
<div style={{ ...shimmer, width: '140px', height: '16px' }} />
|
||||
{/* Spacer */}
|
||||
@@ -70,7 +81,16 @@ export function RoomSkeleton() {
|
||||
<div style={{ flex: 1, overflowY: 'hidden', padding: '16px 0' }}>
|
||||
{MESSAGES.map((msg, i) => (
|
||||
// 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 */}
|
||||
<div style={{ width: '36px', flexShrink: 0 }}>
|
||||
{msg.showAvatar && (
|
||||
|
||||
@@ -23,7 +23,11 @@ export function EditorPreview() {
|
||||
|
||||
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} />
|
||||
</IconButton>
|
||||
<Overlay open={open} backdrop={<OverlayBackdrop />}>
|
||||
@@ -58,7 +62,12 @@ export function EditorPreview() {
|
||||
>
|
||||
<Icon src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} />
|
||||
</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} />
|
||||
</IconButton>
|
||||
<IconButton variant="SurfaceVariant" size="300" radii="300" aria-label="Send">
|
||||
|
||||
@@ -279,44 +279,42 @@ export function Toolbar() {
|
||||
<MarkButton
|
||||
format={MarkType.Bold}
|
||||
icon={Icons.Bold}
|
||||
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`}
|
||||
label="Bold"
|
||||
/>}
|
||||
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} label="Bold" />}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Italic}
|
||||
icon={Icons.Italic}
|
||||
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`}
|
||||
label="Italic"
|
||||
/>}
|
||||
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} label="Italic" />}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Underline}
|
||||
icon={Icons.Underline}
|
||||
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`}
|
||||
label="Underline"
|
||||
/>}
|
||||
tooltip={
|
||||
<BtnTooltip text="Underline" shortCode={`${modKey} + U`} label="Underline" />
|
||||
}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.StrikeThrough}
|
||||
icon={Icons.Strike}
|
||||
tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`}
|
||||
label="Strikethrough"
|
||||
/>}
|
||||
tooltip={
|
||||
<BtnTooltip
|
||||
text="Strike Through"
|
||||
shortCode={`${modKey} + S`}
|
||||
label="Strikethrough"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Code}
|
||||
icon={Icons.Code}
|
||||
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`}
|
||||
label="Inline code"
|
||||
/>}
|
||||
tooltip={
|
||||
<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} label="Inline code" />
|
||||
}
|
||||
/>
|
||||
<MarkButton
|
||||
format={MarkType.Spoiler}
|
||||
icon={Icons.EyeBlind}
|
||||
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`}
|
||||
label="Spoiler"
|
||||
/>}
|
||||
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} label="Spoiler" />}
|
||||
/>
|
||||
</Box>
|
||||
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
||||
@@ -325,30 +323,34 @@ export function Toolbar() {
|
||||
<BlockButton
|
||||
format={BlockType.BlockQuote}
|
||||
icon={Icons.BlockQuote}
|
||||
tooltip={<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`}
|
||||
label="Block quote"
|
||||
/>}
|
||||
tooltip={
|
||||
<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`} label="Block quote" />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.CodeBlock}
|
||||
icon={Icons.BlockCode}
|
||||
tooltip={<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`}
|
||||
label="Code block"
|
||||
/>}
|
||||
tooltip={
|
||||
<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`} label="Code block" />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.OrderedList}
|
||||
icon={Icons.OrderList}
|
||||
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`}
|
||||
label="Ordered list"
|
||||
/>}
|
||||
tooltip={
|
||||
<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} label="Ordered list" />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.UnorderedList}
|
||||
icon={Icons.UnorderList}
|
||||
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`}
|
||||
label="Unordered list"
|
||||
/>}
|
||||
tooltip={
|
||||
<BtnTooltip
|
||||
text="Unordered List"
|
||||
shortCode={`${modKey} + 8`}
|
||||
label="Unordered list"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<HeadingBlockButton />
|
||||
</Box>
|
||||
|
||||
@@ -68,10 +68,30 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||
className={css.Header}
|
||||
variant="Surface"
|
||||
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">
|
||||
<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>
|
||||
<IconButton size="300" onClick={requestClose} aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
@@ -120,9 +140,15 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||
{receiptTs !== undefined && (
|
||||
<Text
|
||||
size="T200"
|
||||
style={lotusTerminal
|
||||
? { color: '#FFB300', textShadow: '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)', fontSize: '0.72rem' }
|
||||
: { opacity: 0.6 }}
|
||||
style={
|
||||
lotusTerminal
|
||||
? {
|
||||
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)}
|
||||
</Text>
|
||||
|
||||
@@ -80,7 +80,9 @@ export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) {
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Join with Address</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Join with Address
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -66,7 +66,9 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
|
||||
size="500"
|
||||
>
|
||||
<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>
|
||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -66,7 +66,9 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Leave Space</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Leave Space
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -48,7 +48,13 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
|
||||
variant={hasError ? 'Critical' : 'SurfaceVariant'}
|
||||
size="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 ? (
|
||||
<Spinner size="100" variant={hasError ? 'Critical' : 'Secondary'} />
|
||||
|
||||
@@ -394,7 +394,9 @@ export function MLocation({ content }: MLocationProps) {
|
||||
const lat = parseFloat(location.latitude);
|
||||
const lon = parseFloat(location.longitude);
|
||||
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 (
|
||||
<Box direction="Column" alignItems="Start" gap="200">
|
||||
|
||||
@@ -29,8 +29,7 @@ export const Reaction = as<
|
||||
{reaction.startsWith('mxc://') ? (
|
||||
<img
|
||||
className={css.ReactionImg}
|
||||
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
|
||||
}
|
||||
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction}
|
||||
alt={reaction}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -8,7 +8,9 @@ export const MessageDeletedContent = as<'div', { children?: never; reason?: stri
|
||||
({ reason, ...props }, ref) => (
|
||||
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
|
||||
<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>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -105,9 +105,9 @@ export function PollContent({
|
||||
const mx = useMatrixClient();
|
||||
const isStable = !!content['m.poll'];
|
||||
|
||||
const poll = (
|
||||
content['m.poll'] ?? content['org.matrix.msc3381.poll.start']
|
||||
) as PollData | undefined;
|
||||
const poll = (content['m.poll'] ?? content['org.matrix.msc3381.poll.start']) as
|
||||
| PollData
|
||||
| undefined;
|
||||
|
||||
const [votes, setVotes] = useState<VoteState>(() => {
|
||||
if (!roomId || !eventId) return { counts: new Map(), myVote: null, total: 0 };
|
||||
@@ -259,7 +259,9 @@ export function PollContent({
|
||||
padding: '7px 12px',
|
||||
borderRadius: '8px',
|
||||
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',
|
||||
lineHeight: 1.4,
|
||||
textAlign: 'left',
|
||||
@@ -281,23 +283,21 @@ export function PollContent({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: `${pct}%`,
|
||||
background: selected
|
||||
? 'rgba(255,255,255,0.10)'
|
||||
: 'rgba(255,255,255,0.05)',
|
||||
background: selected ? 'rgba(255,255,255,0.10)' : 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '8px',
|
||||
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>
|
||||
{selected && (
|
||||
<span style={{ opacity: 0.8, fontSize: '1rem', flexShrink: 0 }}>✓</span>
|
||||
)}
|
||||
{total > 0 && (
|
||||
<span style={{ opacity: 0.6, fontSize: '0.78rem', flexShrink: 0 }}>
|
||||
{pct}%
|
||||
</span>
|
||||
<span style={{ opacity: 0.6, fontSize: '0.78rem', flexShrink: 0 }}>{pct}%</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -79,7 +79,9 @@ export function ReadReceiptAvatars({
|
||||
style={{
|
||||
display: 'flex',
|
||||
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',
|
||||
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
|
||||
borderRadius: '999px',
|
||||
@@ -88,8 +90,7 @@ export function ReadReceiptAvatars({
|
||||
}}
|
||||
>
|
||||
{displayed.map((userId) => {
|
||||
const name =
|
||||
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const avatarMxc = room.getMember(userId)?.getMxcAvatarUrl();
|
||||
const avatarUrl = avatarMxc
|
||||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 32, 32, 'crop') ?? undefined
|
||||
|
||||
@@ -18,7 +18,9 @@ function DummyErrorDialog({
|
||||
<Dialog>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="h2" size="H4">{title}</Text>
|
||||
<Text as="h2" size="H4">
|
||||
{title}
|
||||
</Text>
|
||||
<Text>{message}</Text>
|
||||
</Box>
|
||||
<Button variant="Critical" onClick={onRetry}>
|
||||
|
||||
@@ -37,9 +37,16 @@ function EmailErrorDialog({
|
||||
gap="400"
|
||||
>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="h2" size="H4">{title}</Text>
|
||||
<Text as="h2" size="H4">
|
||||
{title}
|
||||
</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
|
||||
</Text>
|
||||
<Input
|
||||
@@ -141,7 +148,9 @@ export function EmailStageDialog({
|
||||
<Dialog>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<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>
|
||||
|
||||
{errorCode && (
|
||||
|
||||
@@ -43,7 +43,9 @@ export function PasswordStage({
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Account Password</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Account Password
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -35,9 +35,16 @@ function RegistrationTokenErrorDialog({
|
||||
gap="400"
|
||||
>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="h2" size="H4">{title}</Text>
|
||||
<Text as="h2" size="H4">
|
||||
{title}
|
||||
</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
|
||||
</Text>
|
||||
<Input
|
||||
|
||||
@@ -54,7 +54,9 @@ export function SSOStage({
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">SSO Login</Text>
|
||||
<Text as="h2" size="H4">
|
||||
SSO Login
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -18,7 +18,9 @@ function TermsErrorDialog({
|
||||
<Dialog>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="h2" size="H4">{title}</Text>
|
||||
<Text as="h2" size="H4">
|
||||
{title}
|
||||
</Text>
|
||||
<Text>{message}</Text>
|
||||
</Box>
|
||||
<Button variant="Critical" onClick={onRetry}>
|
||||
|
||||
@@ -4,7 +4,13 @@ import { Box, as } from 'folds';
|
||||
import * as css from './UrlPreview.css';
|
||||
|
||||
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) => (
|
||||
|
||||
@@ -68,7 +68,9 @@ function SelfDemoteAlert({ power, onCancel, onChange }: SelfDemoteAlertProps) {
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Self Demotion</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Self Demotion
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
@@ -118,7 +120,9 @@ function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps)
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Shared Power</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Shared Power
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -198,7 +198,9 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Add Existing</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Add Existing
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<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)
|
||||
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)
|
||||
const pttModeRef = useRef(pttMode);
|
||||
@@ -165,8 +167,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
setPttActive(false);
|
||||
}
|
||||
};
|
||||
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pttMode, pttKey, callEmbed]);
|
||||
|
||||
const [hangupState, hangup] = useAsyncCallback(
|
||||
@@ -184,21 +186,31 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
justifyContent="Center"
|
||||
alignItems="Center"
|
||||
>
|
||||
{pttMode && (
|
||||
lotusTerminal ? (
|
||||
<Box style={{
|
||||
position: 'absolute',
|
||||
top: '-2.5rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: pttActive ? 'rgba(0,255,136,0.18)' : 'rgba(255,107,0,0.12)',
|
||||
border: `1px solid ${pttActive ? 'rgba(0,255,136,0.55)' : 'rgba(255,107,0,0.35)'}`,
|
||||
borderRadius: '99px',
|
||||
padding: '0.2rem 0.9rem',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<Text size="T200" style={{ color: pttActive ? '#00FF88' : '#FF6B00', fontWeight: 700, letterSpacing: '0.08em', fontFamily: 'JetBrains Mono, monospace' }}>
|
||||
{pttMode &&
|
||||
(lotusTerminal ? (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-2.5rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: pttActive ? 'rgba(0,255,136,0.18)' : 'rgba(255,107,0,0.12)',
|
||||
border: `1px solid ${pttActive ? 'rgba(0,255,136,0.55)' : 'rgba(255,107,0,0.35)'}`,
|
||||
borderRadius: '99px',
|
||||
padding: '0.2rem 0.9rem',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
size="T200"
|
||||
style={{
|
||||
color: pttActive ? '#00FF88' : '#FF6B00',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
}}
|
||||
>
|
||||
{pttActive ? '● LIVE' : `PTT — Hold ${pttKeyLabel}`}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -221,8 +233,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
{pttActive ? '● Live' : `PTT — Hold ${pttKeyLabel}`}
|
||||
</Text>
|
||||
</Chip>
|
||||
)
|
||||
)}
|
||||
))}
|
||||
{shareConfirm && (
|
||||
<Box
|
||||
style={{
|
||||
@@ -242,17 +253,31 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<Text size="T300" style={{ fontWeight: 600 }}>Share your screen?</Text>
|
||||
<Text size="T200" style={{ opacity: 0.75 }}>Your screen will be visible to all participants in this call.</Text>
|
||||
<Text size="T300" style={{ fontWeight: 600 }}>
|
||||
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">
|
||||
<Button
|
||||
size="300" variant="Success" fill="Solid" radii="300"
|
||||
onClick={() => { callEmbed.control.toggleScreenshare(); setShareConfirm(false); }}
|
||||
size="300"
|
||||
variant="Success"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={() => {
|
||||
callEmbed.control.toggleScreenshare();
|
||||
setShareConfirm(false);
|
||||
}}
|
||||
>
|
||||
<Text size="B300">Share</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300" variant="Secondary" fill="Soft" radii="300" outlined
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={() => setShareConfirm(false)}
|
||||
>
|
||||
<Text size="B300">Cancel</Text>
|
||||
@@ -281,9 +306,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
||||
<ScreenShareButton
|
||||
enabled={screenshare}
|
||||
onToggle={() => screenshare
|
||||
? callEmbed.control.toggleScreenshare()
|
||||
: setShareConfirm(true)
|
||||
onToggle={() =>
|
||||
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -108,7 +108,9 @@ export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||
onClick={() => onToggle()}
|
||||
outlined
|
||||
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}
|
||||
style={disabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
|
||||
>
|
||||
|
||||
@@ -75,7 +75,10 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column" gap="200">
|
||||
{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.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -121,9 +121,16 @@ export function RoomEncryption({ permissions }: RoomEncryptionProps) {
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Enable Encryption</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Enable Encryption
|
||||
</Text>
|
||||
</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} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -103,7 +103,9 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
|
||||
size="500"
|
||||
>
|
||||
<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>
|
||||
<IconButton size="300" onClick={requestClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -58,7 +58,9 @@ function CreateSpaceModal({ state }: CreateSpaceModalProps) {
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">New Space</Text>
|
||||
<Text as="h2" size="H4">
|
||||
New Space
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton size="300" radii="300" onClick={closeDialog} aria-label="Close">
|
||||
|
||||
@@ -22,7 +22,9 @@ export function LobbyHero() {
|
||||
const name = useRoomName(space);
|
||||
const topic = useRoomTopic(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 (
|
||||
<PageHero
|
||||
|
||||
@@ -147,7 +147,8 @@ const DARK: Record<ChatBackground, CSSProperties> = {
|
||||
// True pointy-top hexagonal grid via SVG data URI
|
||||
hexgrid: {
|
||||
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',
|
||||
},
|
||||
|
||||
@@ -305,7 +306,8 @@ const LIGHT: Record<ChatBackground, CSSProperties> = {
|
||||
|
||||
hexgrid: {
|
||||
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',
|
||||
},
|
||||
|
||||
|
||||
@@ -117,7 +117,11 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
{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} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -41,7 +41,12 @@ export function CallChatView() {
|
||||
}
|
||||
>
|
||||
{(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} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -30,7 +30,9 @@ import {
|
||||
} from 'folds';
|
||||
|
||||
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 {
|
||||
CustomEditor,
|
||||
@@ -57,7 +59,9 @@ import {
|
||||
getMentions,
|
||||
} from '../../components/editor';
|
||||
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 {
|
||||
TUploadContent,
|
||||
@@ -143,7 +147,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const alive = useAlive();
|
||||
const alive = useAlive();
|
||||
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
||||
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
||||
const replyUserID = replyDraft?.userId;
|
||||
@@ -467,8 +471,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
async (gifUrl: string, w: number, h: number) => {
|
||||
try {
|
||||
// Only fetch from trusted Giphy CDN domains
|
||||
const allowed = ['media.giphy.com', 'i.giphy.com', 'media0.giphy.com',
|
||||
'media1.giphy.com', 'media2.giphy.com', 'media3.giphy.com', 'media4.giphy.com'];
|
||||
const allowed = [
|
||||
'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);
|
||||
if (!allowed.includes(hostname)) return;
|
||||
|
||||
@@ -695,24 +706,26 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
||||
}
|
||||
content={
|
||||
<React.Suspense fallback={null}><EmojiBoard
|
||||
tab={emojiBoardTab}
|
||||
onTabChange={setEmojiBoardTab}
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
onEmojiSelect={handleEmoticonSelect}
|
||||
onCustomEmojiSelect={handleEmoticonSelect}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
requestClose={() => {
|
||||
setEmojiBoardTab((t) => {
|
||||
if (t) {
|
||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||
return undefined;
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}}
|
||||
/></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<EmojiBoard
|
||||
tab={emojiBoardTab}
|
||||
onTabChange={setEmojiBoardTab}
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
onEmojiSelect={handleEmoticonSelect}
|
||||
onCustomEmojiSelect={handleEmoticonSelect}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
requestClose={() => {
|
||||
setEmojiBoardTab((t) => {
|
||||
if (t) {
|
||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||
return undefined;
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
}
|
||||
>
|
||||
{!hideStickerBtn && (
|
||||
@@ -765,11 +778,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
: undefined
|
||||
}
|
||||
content={
|
||||
<React.Suspense fallback={null}><GifPicker
|
||||
apiKey={gifApiKey}
|
||||
onSelect={handleGifSelect}
|
||||
requestClose={() => setGifOpen(false)}
|
||||
/></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<GifPicker
|
||||
apiKey={gifApiKey}
|
||||
onSelect={handleGifSelect}
|
||||
requestClose={() => setGifOpen(false)}
|
||||
/>
|
||||
</React.Suspense>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
@@ -798,7 +813,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
</UseStateProvider>
|
||||
)}
|
||||
{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}
|
||||
</Text>
|
||||
)}
|
||||
@@ -811,14 +834,28 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
title="Share location"
|
||||
>
|
||||
{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>
|
||||
) : (
|
||||
<Icon src={Icons.Pin} size="100" />
|
||||
)}
|
||||
</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} />
|
||||
</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.
|
||||
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
||||
const _roomId = mEvt.getRoomId();
|
||||
if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity));
|
||||
if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity));
|
||||
}
|
||||
|
||||
if (!document.hasFocus() && !unreadInfo) {
|
||||
@@ -673,7 +673,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
) => {
|
||||
const evtTimeline = getEventTimeline(room, evtId);
|
||||
const absoluteIndex =
|
||||
evtTimeline && getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
|
||||
evtTimeline &&
|
||||
getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
|
||||
|
||||
if (typeof absoluteIndex === 'number') {
|
||||
const scrolled = scrollToItem(absoluteIndex, {
|
||||
@@ -1242,7 +1243,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
mEvent.getType() === 'm.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)
|
||||
return (
|
||||
<Text>
|
||||
@@ -1371,7 +1378,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
{mEvent.isRedacted() ? (
|
||||
<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>
|
||||
);
|
||||
@@ -1424,7 +1435,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
{mEvent.isRedacted() ? (
|
||||
<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>
|
||||
);
|
||||
@@ -1608,12 +1623,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
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 (
|
||||
<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}>
|
||||
<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
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
@@ -1623,14 +1664,45 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
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 = (
|
||||
<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 (
|
||||
<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}>
|
||||
<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
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
@@ -1641,12 +1713,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
const access = mEvent.getContent<{ guest_access?: string }>().guest_access ?? 'unknown';
|
||||
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 (
|
||||
<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}>
|
||||
<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
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
@@ -1657,12 +1755,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
const alias = mEvent.getContent<{ alias?: string }>().alias;
|
||||
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 (
|
||||
<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}>
|
||||
<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
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
@@ -1831,9 +1955,15 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
const [base, len] = timelineSegments[mid];
|
||||
if (item < base) { hi = mid - 1; }
|
||||
else if (item >= base + len) { lo = mid + 1; }
|
||||
else { eventTimeline = timelineSegments[mid][2]; baseIndex = base; break; }
|
||||
if (item < base) {
|
||||
hi = mid - 1;
|
||||
} else if (item >= base + len) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
eventTimeline = timelineSegments[mid][2];
|
||||
baseIndex = base;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!eventTimeline) return null;
|
||||
@@ -1935,131 +2065,131 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
|
||||
return (
|
||||
<ReadPositionsContext.Provider value={readPositions}>
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
|
||||
<TimelineFloat position="Top">
|
||||
<Chip
|
||||
variant="Primary"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.MessageUnread} />}
|
||||
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)
|
||||
}`,
|
||||
}}
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
|
||||
<TimelineFloat position="Top">
|
||||
<Chip
|
||||
variant="Primary"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.MessageUnread} />}
|
||||
onClick={handleJumpToUnread}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
))}
|
||||
<Text size="L400">Jump to Unread</Text>
|
||||
</Chip>
|
||||
|
||||
{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}
|
||||
<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` }}
|
||||
>
|
||||
<Text size="L400">Jump to Latest</Text>
|
||||
</Chip>
|
||||
</TimelineFloat>
|
||||
)}
|
||||
</Box>
|
||||
{!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} />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,8 +56,6 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function RoomView({ eventId }: { eventId?: string }) {
|
||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||
@@ -98,8 +96,9 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
)
|
||||
);
|
||||
|
||||
const chatBgStyle = useMemo(
|
||||
() => getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark),
|
||||
const chatBgStyle = useMemo(
|
||||
() =>
|
||||
getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark),
|
||||
[chatBackground, lotusTerminal, isDark]
|
||||
);
|
||||
|
||||
|
||||
@@ -533,7 +533,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
}
|
||||
>
|
||||
{(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} />
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -596,11 +601,13 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
{!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission &&
|
||||
(direct || (room.getJoinRule() === 'invite' &&
|
||||
getStateEvents(room, StateEvent.SpaceParent).length === 0)) && (
|
||||
<CallButton />
|
||||
)}
|
||||
{!room.isCallRoom() &&
|
||||
livekitSupported &&
|
||||
rtcSupported &&
|
||||
hasCallPermission &&
|
||||
(direct ||
|
||||
(room.getJoinRule() === 'invite' &&
|
||||
getStateEvents(room, StateEvent.SpaceParent).length === 0)) && <CallButton />}
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
@@ -616,7 +623,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
}
|
||||
>
|
||||
{(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} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -117,7 +117,13 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
||||
</>
|
||||
)}
|
||||
</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} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -57,7 +57,12 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
>
|
||||
<Modal
|
||||
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
|
||||
direction="Column"
|
||||
@@ -89,11 +94,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
) : (
|
||||
<Box grow="Yes" style={{ minHeight: 0 }}>
|
||||
<Scroll size="300" hideTrack visibility="Hover">
|
||||
<Box
|
||||
direction="Column"
|
||||
gap="100"
|
||||
style={{ padding: config.space.S200 }}
|
||||
>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{filtered.slice(0, 60).map((room) => (
|
||||
<MenuItem
|
||||
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 shrink="No">
|
||||
{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} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -185,7 +185,12 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Remove Avatar</Text>
|
||||
</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} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -34,7 +34,13 @@ import FocusTrap from 'focus-trap-react';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
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 { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
@@ -313,7 +319,10 @@ function Appearance() {
|
||||
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
||||
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
const [perMessageProfiles, setPerMessageProfiles] = useSetting(settingsAtom, 'perMessageProfiles');
|
||||
const [perMessageProfiles, setPerMessageProfiles] = useSetting(
|
||||
settingsAtom,
|
||||
'perMessageProfiles'
|
||||
);
|
||||
const [lotusTerminal, setLotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
|
||||
return (
|
||||
@@ -359,7 +368,12 @@ function Appearance() {
|
||||
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="200">
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="200"
|
||||
>
|
||||
<SettingTile
|
||||
title="Chat Background"
|
||||
description="Pattern applied behind the message timeline."
|
||||
@@ -373,7 +387,9 @@ function Appearance() {
|
||||
<SettingTile
|
||||
title="Show Profile on Every Message"
|
||||
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 className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
@@ -385,7 +401,10 @@ function Appearance() {
|
||||
{lotusTerminal && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { resetBootSequence(); runLotusBootSequence(true); }}
|
||||
onClick={() => {
|
||||
resetBootSequence();
|
||||
runLotusBootSequence(true);
|
||||
}}
|
||||
title="Replay boot sequence"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
@@ -808,10 +827,12 @@ function Editor() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Calls() {
|
||||
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 [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey');
|
||||
const [listeningForKey, setListeningForKey] = useState(false);
|
||||
@@ -843,7 +864,8 @@ function Calls() {
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
}, [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 (
|
||||
<Box direction="Column" gap="100">
|
||||
@@ -859,10 +881,21 @@ function Calls() {
|
||||
<SettingTile
|
||||
title="Noise Suppression"
|
||||
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 className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="400">
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Push to Talk"
|
||||
description="Mute your microphone by default. Hold the PTT key to speak."
|
||||
@@ -882,9 +915,7 @@ function Calls() {
|
||||
onClick={handleKeyBind}
|
||||
style={{ minWidth: '90px' }}
|
||||
>
|
||||
<Text size="B300">
|
||||
{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}
|
||||
</Text>
|
||||
<Text size="B300">{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -894,7 +925,6 @@ function Calls() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ChatBgGrid() {
|
||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const theme = useTheme();
|
||||
@@ -915,18 +945,16 @@ function ChatBgGrid() {
|
||||
height: toRem(50),
|
||||
borderRadius: toRem(8),
|
||||
cursor: 'pointer',
|
||||
border: chatBackground === opt.value
|
||||
? '2px solid #980000'
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
border:
|
||||
chatBackground === opt.value
|
||||
? '2px solid #980000'
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
...getChatBg(opt.value as ChatBackground, isDark),
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="T200"
|
||||
style={chatBackground === opt.value ? { color: '#980000' } : undefined}
|
||||
>
|
||||
<Text size="T200" style={chatBackground === opt.value ? { color: '#980000' } : undefined}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -120,7 +120,14 @@ function KeywordCross({ pushRule }: PushRulesProps) {
|
||||
|
||||
const removing = removeState.status === AsyncStatus.Loading;
|
||||
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" />}
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
@@ -117,7 +117,11 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps)
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
{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} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -53,10 +53,19 @@ export const createCallEmbed = (
|
||||
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0;
|
||||
|
||||
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 widget = CallEmbed.getWidget(mx, room, intent, themeKind, noiseSuppression, initialAudio, initialVideo);
|
||||
const controlState = pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
||||
const widget = CallEmbed.getWidget(
|
||||
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);
|
||||
|
||||
@@ -77,7 +86,16 @@ export const useCallStart = (dm = false) => {
|
||||
if (!container) {
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useState } from 'react';
|
||||
export function useForceUpdate() {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
return [data, function forceUpdateHook() {
|
||||
setData({});
|
||||
}];
|
||||
return [
|
||||
data,
|
||||
function forceUpdateHook() {
|
||||
setData({});
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -55,11 +55,14 @@ export const usePan = (active: boolean) => {
|
||||
}, [active]);
|
||||
|
||||
// Clean up document listeners if component unmounts during an active drag
|
||||
useEffect(() => () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
useEffect(
|
||||
() => () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
pan,
|
||||
|
||||
@@ -23,9 +23,7 @@ function computePositions(room: Room, myUserId: string): Map<string, string[]> {
|
||||
const map = new Map<string, string[]>();
|
||||
const liveEvents = room.getLiveTimeline().getEvents();
|
||||
// Build O(1) index once instead of O(T) findIndex per member
|
||||
const eventIndex = new Map<string, number>(
|
||||
liveEvents.map((e, i) => [e.getId() ?? '', i])
|
||||
);
|
||||
const eventIndex = new Map<string, number>(liveEvents.map((e, i) => [e.getId() ?? '', i]));
|
||||
for (const member of room.getJoinedMembers()) {
|
||||
if (member.userId === myUserId) continue;
|
||||
const evtId = room.getEventReadUpTo(member.userId);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { lightTheme } from 'folds';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
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 { useSetting } from '../state/hooks/settings';
|
||||
|
||||
@@ -45,7 +51,12 @@ export const LotusTerminalTheme: Theme = {
|
||||
export const LotusTerminalLightTheme: Theme = {
|
||||
id: 'lotus-terminal-light-theme',
|
||||
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[] => {
|
||||
|
||||
+120
-33
@@ -10,10 +10,12 @@ import {
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { ClientConfig } from '../hooks/useClientConfig';
|
||||
const AuthLayout = React.lazy(() => import('./auth').then(m => ({ default: m.AuthLayout })));
|
||||
const Login = React.lazy(() => import('./auth').then(m => ({ default: m.Login })));
|
||||
const Register = React.lazy(() => import('./auth').then(m => ({ default: m.Register })));
|
||||
const ResetPassword = React.lazy(() => import('./auth').then(m => ({ default: m.ResetPassword })));
|
||||
const AuthLayout = React.lazy(() => import('./auth').then((m) => ({ default: m.AuthLayout })));
|
||||
const Login = React.lazy(() => import('./auth').then((m) => ({ default: m.Login })));
|
||||
const Register = React.lazy(() => import('./auth').then((m) => ({ default: m.Register })));
|
||||
const ResetPassword = React.lazy(() =>
|
||||
import('./auth').then((m) => ({ default: m.ResetPassword }))
|
||||
);
|
||||
import {
|
||||
DIRECT_PATH,
|
||||
EXPLORE_PATH,
|
||||
@@ -48,9 +50,8 @@ import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
|
||||
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
|
||||
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
|
||||
|
||||
|
||||
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 { SidebarNav } from './client/SidebarNav';
|
||||
@@ -62,22 +63,38 @@ import { ClientNonUIFeatures } from './client/ClientNonUIFeatures';
|
||||
import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
|
||||
import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification';
|
||||
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';
|
||||
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';
|
||||
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 { Create } from './client/create';
|
||||
const CreateSpaceModalRenderer = React.lazy(() => import('../features/create-space').then(m => ({ default: m.CreateSpaceModalRenderer })));
|
||||
const SearchModalRenderer = React.lazy(() => import('../features/search').then(m => ({ default: m.SearchModalRenderer })));
|
||||
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 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 })));
|
||||
const CreateSpaceModalRenderer = React.lazy(() =>
|
||||
import('../features/create-space').then((m) => ({ default: m.CreateSpaceModalRenderer }))
|
||||
);
|
||||
const SearchModalRenderer = React.lazy(() =>
|
||||
import('../features/search').then((m) => ({ default: m.SearchModalRenderer }))
|
||||
);
|
||||
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 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 { CallStatusRenderer } from './CallStatusRenderer';
|
||||
import { CallEmbedProvider } from '../components/CallEmbedProvider';
|
||||
@@ -112,9 +129,30 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
</React.Suspense>
|
||||
}
|
||||
>
|
||||
<Route path={LOGIN_PATH} 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
|
||||
path={LOGIN_PATH}
|
||||
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
|
||||
@@ -149,12 +187,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
</ClientLayout>
|
||||
<CallStatusRenderer />
|
||||
</CallEmbedProvider>
|
||||
<React.Suspense fallback={null}><SearchModalRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchModalRenderer />
|
||||
</React.Suspense>
|
||||
<UserRoomProfileRenderer />
|
||||
<React.Suspense fallback={null}><CreateRoomModalRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}><CreateSpaceModalRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}><RoomSettingsRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}><SpaceSettingsRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<CreateRoomModalRenderer />
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<CreateSpaceModalRenderer />
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<RoomSettingsRenderer />
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<SpaceSettingsRenderer />
|
||||
</React.Suspense>
|
||||
<ReceiveSelfDeviceVerification />
|
||||
<AutoRestoreBackupOnVerification />
|
||||
</ClientNonUIFeatures>
|
||||
@@ -250,7 +298,14 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
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={_ROOM_PATH}
|
||||
@@ -269,7 +324,9 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={EXPLORE_PATH}>
|
||||
<React.Suspense fallback={null}><Explore /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<Explore />
|
||||
</React.Suspense>
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
@@ -284,8 +341,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_FEATURED_PATH} element={<React.Suspense fallback={null}><FeaturedRooms /></React.Suspense>} />
|
||||
<Route path={_SERVER_PATH} element={<React.Suspense fallback={null}><PublicRooms /></React.Suspense>} />
|
||||
<Route
|
||||
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 path={CREATE_PATH} element={<Create />} />
|
||||
<Route
|
||||
@@ -294,7 +365,9 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={INBOX_PATH}>
|
||||
<React.Suspense fallback={null}><Inbox /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<Inbox />
|
||||
</React.Suspense>
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
@@ -309,8 +382,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_NOTIFICATIONS_PATH} element={<React.Suspense fallback={null}><Notifications /></React.Suspense>} />
|
||||
<Route path={_INVITES_PATH} element={<React.Suspense fallback={null}><Invites /></React.Suspense>} />
|
||||
<Route
|
||||
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 path="/*" element={<p>Page not found</p>} />
|
||||
|
||||
@@ -25,7 +25,9 @@ export function UnAuthRouteThemeManager() {
|
||||
if (lotusTerminal) {
|
||||
const isLight = systemThemeKind === ThemeKind.Light;
|
||||
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);
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
@@ -47,7 +49,9 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||
|
||||
const terminalIsLight = lotusTerminal && activeTheme.kind === ThemeKind.Light;
|
||||
const effectiveTheme = lotusTerminal
|
||||
? (terminalIsLight ? LotusTerminalLightTheme : LotusTerminalTheme)
|
||||
? terminalIsLight
|
||||
? LotusTerminalLightTheme
|
||||
: LotusTerminalTheme
|
||||
: activeTheme;
|
||||
|
||||
// Boot animation only fires when lotusTerminal is toggled on, not on every theme change
|
||||
|
||||
@@ -18,7 +18,13 @@ export function AuthFooter() {
|
||||
>
|
||||
v{pkg.version}
|
||||
</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
|
||||
</Text>
|
||||
<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">
|
||||
<Box grow="Yes" direction="Row" gap="300" alignItems="Center">
|
||||
<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>
|
||||
</Header>
|
||||
<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">
|
||||
Password
|
||||
</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">
|
||||
{loginState.status === AsyncStatus.Error && (
|
||||
<>
|
||||
|
||||
@@ -118,8 +118,8 @@ export const useLoginComplete = (data?: CustomLoginResponse) => {
|
||||
const afterLoginRedirectUrl = getAfterLoginRedirectPath();
|
||||
deleteAfterLoginRedirectPath();
|
||||
const _redir = afterLoginRedirectUrl;
|
||||
const _safePath = (_redir && /^\/(?!\/)/.test(_redir)) ? _redir : getHomePath();
|
||||
navigate(_safePath, { replace: true });
|
||||
const _safePath = _redir && /^\/(?!\/)/.test(_redir) ? _redir : getHomePath();
|
||||
navigate(_safePath, { replace: true });
|
||||
}
|
||||
}, [data, navigate]);
|
||||
};
|
||||
|
||||
@@ -21,14 +21,22 @@ export function ClientLayout({ nav, children }: ClientLayoutProps) {
|
||||
borderRadius: '0 0 4px 0',
|
||||
transition: 'top 0.1s',
|
||||
}}
|
||||
onFocus={(e) => { (e.currentTarget as HTMLElement).style.top = '0'; }}
|
||||
onBlur={(e) => { (e.currentTarget as HTMLElement).style.top = '-40px'; }}
|
||||
onFocus={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.top = '0';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.top = '-40px';
|
||||
}}
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<Box grow="Yes">
|
||||
<Box shrink="No" as="nav" aria-label="Room navigation">{nav}</Box>
|
||||
<Box grow="Yes" as="main" id="main-content">{children}</Box>
|
||||
<Box shrink="No" as="nav" aria-label="Room navigation">
|
||||
{nav}
|
||||
</Box>
|
||||
<Box grow="Yes" as="main" id="main-content">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,8 @@ export function SpecVersions({ baseUrl, children }: { baseUrl: string; children:
|
||||
<Dialog>
|
||||
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
||||
<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>
|
||||
<Button variant="Critical" onClick={retry}>
|
||||
<Text as="span" size="B400">
|
||||
|
||||
@@ -15,7 +15,15 @@ export function WelcomePage() {
|
||||
>
|
||||
<PageHeroSection>
|
||||
<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"
|
||||
subTitle={
|
||||
<span>
|
||||
|
||||
@@ -108,7 +108,13 @@ function DirectHeader() {
|
||||
</Text>
|
||||
</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" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -94,9 +94,16 @@ export function AddServer() {
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Add Server</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Add Server
|
||||
</Text>
|
||||
</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} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -110,7 +117,13 @@ export function AddServer() {
|
||||
<Text priority="400">Add server name to explore public communities.</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
<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 && (
|
||||
<Text style={{ color: color.Critical.Main }} size="T300">
|
||||
Failed to load public rooms. Please try again.
|
||||
|
||||
@@ -56,7 +56,9 @@ export function FeaturedRooms() {
|
||||
<Box direction="Column" gap="700">
|
||||
{spaces && spaces.length > 0 && (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text as="h2" size="H4">Featured Spaces</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Featured Spaces
|
||||
</Text>
|
||||
<RoomCardGrid>
|
||||
{spaces.map((roomIdOrAlias) => (
|
||||
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
||||
@@ -85,7 +87,9 @@ export function FeaturedRooms() {
|
||||
)}
|
||||
{rooms && rooms.length > 0 && (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text as="h3" size="H4">Featured Rooms</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Featured Rooms
|
||||
</Text>
|
||||
<RoomCardGrid>
|
||||
{rooms.map((roomIdOrAlias) => (
|
||||
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
||||
|
||||
@@ -537,7 +537,9 @@ export function PublicRooms() {
|
||||
{isSearch ? (
|
||||
<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">
|
||||
{roomTypeFilters.map((filter) => (
|
||||
|
||||
@@ -122,7 +122,13 @@ function HomeHeader() {
|
||||
</Text>
|
||||
</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" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -429,7 +429,9 @@ function KnownInvites({
|
||||
}: KnownInvitesProps) {
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text as="h3" size="H4">Primary</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Primary
|
||||
</Text>
|
||||
{invites.length > 0 ? (
|
||||
<Box direction="Column" gap="100">
|
||||
{invites.map((invite) => (
|
||||
@@ -488,7 +490,9 @@ function UnknownInvites({
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
|
||||
<Text as="h3" size="H4">Public</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Public
|
||||
</Text>
|
||||
<Box>
|
||||
{invites.length > 0 && (
|
||||
<Chip
|
||||
@@ -585,7 +589,9 @@ function SpamInvites({
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text as="h3" size="H4">Spam</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Spam
|
||||
</Text>
|
||||
{invites.length > 0 ? (
|
||||
<Box direction="Column" gap="100">
|
||||
<SequenceCard
|
||||
|
||||
@@ -522,7 +522,13 @@ function OpenedSpaceFolder({ folder, onClose, children }: OpenedSpaceFolderProps
|
||||
>
|
||||
<SidebarFolderDropTarget ref={aboveTargetRef} position="Top" />
|
||||
<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 />
|
||||
</IconButton>
|
||||
</SidebarAvatar>
|
||||
|
||||
@@ -274,7 +274,13 @@ function SpaceHeader() {
|
||||
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
|
||||
</Box>
|
||||
<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" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@@ -433,7 +439,9 @@ export function Space() {
|
||||
return false;
|
||||
}
|
||||
const showRoomAnyway =
|
||||
roomsWithUnreadSet.has(roomId) || roomId === selectedRoomId || callEmbed?.roomId === roomId;
|
||||
roomsWithUnreadSet.has(roomId) ||
|
||||
roomId === selectedRoomId ||
|
||||
callEmbed?.roomId === roomId;
|
||||
return !showRoomAnyway;
|
||||
},
|
||||
[space.roomId, closedCategories, roomsWithUnreadSet, selectedRoomId, callEmbed]
|
||||
|
||||
@@ -30,7 +30,10 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
|
||||
private get settingsButton(): HTMLElement | undefined {
|
||||
// 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 {
|
||||
@@ -98,7 +101,13 @@ export class CallControl extends EventEmitter implements 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();
|
||||
}
|
||||
|
||||
@@ -177,7 +186,9 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
|
||||
// EC auto-switches to spotlight when screenshare starts — revert to grid
|
||||
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}
|
||||
role="button"
|
||||
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}
|
||||
className={css.Spoiler()}
|
||||
aria-label="Spoiler — click to reveal"
|
||||
|
||||
@@ -6,9 +6,9 @@ export type ListAction<T> =
|
||||
item: T | T[];
|
||||
}
|
||||
| {
|
||||
type: 'REPLACE';
|
||||
item: T;
|
||||
replacement: T;
|
||||
type: 'REPLACE';
|
||||
item: T;
|
||||
replacement: T;
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
@@ -34,7 +34,10 @@ export const createListAtom = <T>() => {
|
||||
return;
|
||||
}
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -21,9 +21,7 @@ export type TUploadItem = {
|
||||
|
||||
export type TUploadListAtom = ReturnType<typeof createListAtom<TUploadItem>>;
|
||||
|
||||
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TUploadListAtom>(
|
||||
createListAtom
|
||||
);
|
||||
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TUploadListAtom>(createListAtom);
|
||||
|
||||
export const roomUploadAtomFamily = createUploadAtomFamily();
|
||||
|
||||
|
||||
@@ -9,7 +9,24 @@ export type DateFormat =
|
||||
| 'YYYY-MM-DD'
|
||||
| '';
|
||||
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 {
|
||||
Modern = 0,
|
||||
Compact = 1,
|
||||
@@ -114,7 +131,11 @@ export const getSettings = (): 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());
|
||||
|
||||
@@ -84,7 +84,15 @@ const transformFontTag: Transformer = (tagName, attribs) => ({
|
||||
tagName,
|
||||
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,
|
||||
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)',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+197
-159
@@ -2,16 +2,16 @@ import { globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
const glitch1 = keyframes({
|
||||
'0%, 90%, 100%': { clipPath: 'inset(0)', transform: 'skewX(0)' },
|
||||
'92%': { clipPath: 'inset(15% 0 72% 0)', transform: 'skewX(-4deg)' },
|
||||
'94%': { clipPath: 'inset(54% 0 22% 0)', transform: 'skewX(3deg)' },
|
||||
'96%': { clipPath: 'inset(30% 0 48% 0)', transform: 'skewX(-2deg)' },
|
||||
'92%': { clipPath: 'inset(15% 0 72% 0)', transform: 'skewX(-4deg)' },
|
||||
'94%': { clipPath: 'inset(54% 0 22% 0)', transform: 'skewX(3deg)' },
|
||||
'96%': { clipPath: 'inset(30% 0 48% 0)', transform: 'skewX(-2deg)' },
|
||||
});
|
||||
|
||||
const glitch2 = keyframes({
|
||||
'0%, 90%, 100%': { clipPath: 'inset(0)', transform: 'skewX(0)' },
|
||||
'92%': { clipPath: 'inset(60% 0 8% 0)', transform: 'skewX(3deg)' },
|
||||
'94%': { clipPath: 'inset(8% 0 68% 0)', transform: 'skewX(-3deg)' },
|
||||
'96%': { clipPath: 'inset(42% 0 38% 0)', transform: 'skewX(1deg)' },
|
||||
'92%': { clipPath: 'inset(60% 0 8% 0)', transform: 'skewX(3deg)' },
|
||||
'94%': { clipPath: 'inset(8% 0 68% 0)', transform: 'skewX(-3deg)' },
|
||||
'96%': { clipPath: 'inset(42% 0 38% 0)', transform: 'skewX(1deg)' },
|
||||
});
|
||||
|
||||
export const lotusTerminalBodyClass = style({
|
||||
@@ -23,67 +23,67 @@ export const lotusTerminalBodyClass = style({
|
||||
backgroundSize: '28px 28px',
|
||||
vars: {
|
||||
// Backgrounds
|
||||
'--lt-bg-primary': '#030508',
|
||||
'--lt-bg-primary': '#030508',
|
||||
'--lt-bg-secondary': '#060c14',
|
||||
'--lt-bg-tertiary': '#0d1520',
|
||||
'--lt-bg-card': '#07101a',
|
||||
'--lt-bg-terminal': '#010304',
|
||||
'--lt-bg-tertiary': '#0d1520',
|
||||
'--lt-bg-card': '#07101a',
|
||||
'--lt-bg-terminal': '#010304',
|
||||
// Accent — Orange
|
||||
'--lt-accent-orange': '#FF6B00',
|
||||
'--lt-accent-orange': '#FF6B00',
|
||||
'--lt-accent-orange-bright': '#FF8C2B',
|
||||
'--lt-accent-orange-dim': 'rgba(255,107,0,0.12)',
|
||||
'--lt-accent-orange-dim': 'rgba(255,107,0,0.12)',
|
||||
'--lt-accent-orange-border': 'rgba(255,107,0,0.35)',
|
||||
// Accent — Amber
|
||||
'--lt-accent-amber': '#FFB300',
|
||||
'--lt-accent-amber': '#FFB300',
|
||||
'--lt-accent-amber-dim': 'rgba(255,179,0,0.10)',
|
||||
// Accent — Cyan
|
||||
'--lt-accent-cyan': '#00D4FF',
|
||||
'--lt-accent-cyan': '#00D4FF',
|
||||
'--lt-accent-cyan-bright': '#33DFFF',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,212,255,0.10)',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,212,255,0.10)',
|
||||
'--lt-accent-cyan-border': 'rgba(0,212,255,0.22)',
|
||||
// Accent — Green
|
||||
'--lt-accent-green': '#00FF88',
|
||||
'--lt-accent-green': '#00FF88',
|
||||
'--lt-accent-green-bright': '#33FFAA',
|
||||
'--lt-accent-green-dim': 'rgba(0,255,136,0.10)',
|
||||
'--lt-accent-green-dim': 'rgba(0,255,136,0.10)',
|
||||
'--lt-accent-green-border': 'rgba(0,255,136,0.22)',
|
||||
// Accent — Red
|
||||
'--lt-accent-red': '#FF2D55',
|
||||
'--lt-accent-red': '#FF2D55',
|
||||
'--lt-accent-red-dim': 'rgba(255,45,85,0.12)',
|
||||
// Accent — Gold
|
||||
'--lt-accent-gold': '#FFD700',
|
||||
'--lt-accent-gold': '#FFD700',
|
||||
'--lt-accent-gold-dim': 'rgba(255,215,0,0.10)',
|
||||
// Accent — Purple
|
||||
'--lt-accent-purple': '#BF5FFF',
|
||||
'--lt-accent-purple': '#BF5FFF',
|
||||
'--lt-accent-purple-dim': 'rgba(191,95,255,0.10)',
|
||||
// Text
|
||||
'--lt-text-primary': '#c4d9ee',
|
||||
'--lt-text-primary': '#c4d9ee',
|
||||
'--lt-text-secondary': '#7fa3bf',
|
||||
'--lt-text-muted': '#3e607a',
|
||||
'--lt-text-dim': '#1e3347',
|
||||
'--lt-text-muted': '#3e607a',
|
||||
'--lt-text-dim': '#1e3347',
|
||||
// Borders
|
||||
'--lt-border-color': 'rgba(0,212,255,0.16)',
|
||||
'--lt-border-color-hi': '#00D4FF',
|
||||
'--lt-border-color': 'rgba(0,212,255,0.16)',
|
||||
'--lt-border-color-hi': '#00D4FF',
|
||||
'--lt-border-color-dim': 'rgba(0,212,255,0.07)',
|
||||
// Glows — text
|
||||
'--lt-glow-orange': '0 0 6px #FF6B00, 0 0 16px rgba(255,107,0,0.55)',
|
||||
'--lt-glow-orange-intense':'0 0 8px #FF6B00, 0 0 22px #FF6B00, 0 0 40px rgba(255,107,0,0.45)',
|
||||
'--lt-glow-cyan': '0 0 6px #00D4FF, 0 0 16px rgba(0,212,255,0.45)',
|
||||
'--lt-glow-cyan-intense': '0 0 8px #00D4FF, 0 0 22px #00D4FF, 0 0 38px rgba(0,212,255,0.35)',
|
||||
'--lt-glow-green': '0 0 6px #00FF88, 0 0 16px rgba(0,255,136,0.45)',
|
||||
'--lt-glow-orange': '0 0 6px #FF6B00, 0 0 16px rgba(255,107,0,0.55)',
|
||||
'--lt-glow-orange-intense': '0 0 8px #FF6B00, 0 0 22px #FF6B00, 0 0 40px rgba(255,107,0,0.45)',
|
||||
'--lt-glow-cyan': '0 0 6px #00D4FF, 0 0 16px rgba(0,212,255,0.45)',
|
||||
'--lt-glow-cyan-intense': '0 0 8px #00D4FF, 0 0 22px #00D4FF, 0 0 38px rgba(0,212,255,0.35)',
|
||||
'--lt-glow-green': '0 0 6px #00FF88, 0 0 16px rgba(0,255,136,0.45)',
|
||||
'--lt-glow-green-intense': '0 0 8px #00FF88, 0 0 22px #00FF88, 0 0 36px rgba(0,255,136,0.35)',
|
||||
'--lt-glow-amber': '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)',
|
||||
'--lt-glow-amber': '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)',
|
||||
'--lt-glow-amber-intense': '0 0 8px #FFB300, 0 0 20px #FFB300, 0 0 34px rgba(255,179,0,0.45)',
|
||||
'--lt-glow-red': '0 0 6px #FF2D55, 0 0 16px rgba(255,45,85,0.45)',
|
||||
'--lt-glow-red': '0 0 6px #FF2D55, 0 0 16px rgba(255,45,85,0.45)',
|
||||
// Glows — box
|
||||
'--lt-box-glow-orange': '0 0 18px rgba(255,107,0,0.22), 0 0 36px rgba(255,107,0,0.08)',
|
||||
'--lt-box-glow-cyan': '0 0 18px rgba(0,212,255,0.18), 0 0 36px rgba(0,212,255,0.06)',
|
||||
'--lt-box-glow-green': '0 0 18px rgba(0,255,136,0.18), 0 0 36px rgba(0,255,136,0.06)',
|
||||
'--lt-box-glow-red': '0 0 18px rgba(255,45,85,0.22), 0 0 36px rgba(255,45,85,0.08)',
|
||||
'--lt-box-glow-amber': '0 0 18px rgba(255,179,0,0.18), 0 0 36px rgba(255,179,0,0.06)',
|
||||
'--lt-box-glow-cyan': '0 0 18px rgba(0,212,255,0.18), 0 0 36px rgba(0,212,255,0.06)',
|
||||
'--lt-box-glow-green': '0 0 18px rgba(0,255,136,0.18), 0 0 36px rgba(0,255,136,0.06)',
|
||||
'--lt-box-glow-red': '0 0 18px rgba(255,45,85,0.22), 0 0 36px rgba(255,45,85,0.08)',
|
||||
'--lt-box-glow-amber': '0 0 18px rgba(255,179,0,0.18), 0 0 36px rgba(255,179,0,0.06)',
|
||||
// Fonts
|
||||
'--lt-font-mono': "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace",
|
||||
'--lt-font-mono': "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace",
|
||||
'--lt-font-display': "'JetBrains Mono', 'Fira Code', 'Courier New', monospace",
|
||||
'--lt-font-crt': "'VT323', 'Courier New', monospace",
|
||||
'--lt-font-crt': "'VT323', 'Courier New', monospace",
|
||||
} as any,
|
||||
});
|
||||
|
||||
@@ -97,7 +97,8 @@ globalStyle(`body.${lotusTerminalBodyClass}::before`, {
|
||||
content: "''",
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'repeating-linear-gradient(0deg, rgba(0,0,0,0.07) 0px, rgba(0,0,0,0.07) 1px, transparent 1px, transparent 3px)',
|
||||
background:
|
||||
'repeating-linear-gradient(0deg, rgba(0,0,0,0.07) 0px, rgba(0,0,0,0.07) 1px, transparent 1px, transparent 3px)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
});
|
||||
@@ -250,8 +251,7 @@ globalStyle(`body.${lotusTerminalBodyClass} hr`, {
|
||||
|
||||
// ── Input / textarea / contenteditable focus — orange glow ─────────────────
|
||||
globalStyle(
|
||||
`body.${lotusTerminalBodyClass} input:focus,` +
|
||||
`body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
`body.${lotusTerminalBodyClass} input:focus,` + `body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
{
|
||||
outline: 'none',
|
||||
borderColor: '#FF6B00',
|
||||
@@ -353,8 +353,6 @@ globalStyle(`body.${lotusTerminalBodyClass}`, {
|
||||
color: '#c4d9ee',
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ── Reaction chips (emoji reactions on messages) ────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} button[data-reaction-key]`, {
|
||||
backgroundColor: 'rgba(0,212,255,0.06)',
|
||||
@@ -391,66 +389,66 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass}`, {
|
||||
backgroundImage: 'radial-gradient(circle, rgba(90,110,150,0.14) 1px, transparent 1px)',
|
||||
color: '#111827',
|
||||
vars: {
|
||||
'--lt-bg-primary': '#edf0f5',
|
||||
'--lt-bg-secondary': '#e2e7ef',
|
||||
'--lt-bg-tertiary': '#d4dae6',
|
||||
'--lt-bg-card': '#ffffff',
|
||||
'--lt-bg-terminal': '#f4f6fa',
|
||||
'--lt-accent-orange': '#c44e00',
|
||||
'--lt-accent-orange-bright':'#d45800',
|
||||
'--lt-accent-orange-dim': 'rgba(196,78,0,0.10)',
|
||||
'--lt-accent-orange-border':'rgba(196,78,0,0.28)',
|
||||
'--lt-accent-amber': '#8a5a00',
|
||||
'--lt-accent-amber-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-cyan': '#0062b8',
|
||||
'--lt-accent-cyan-bright': '#0070cc',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,98,184,0.10)',
|
||||
'--lt-accent-cyan-border': 'rgba(0,98,184,0.22)',
|
||||
'--lt-accent-green': '#006d35',
|
||||
'--lt-bg-primary': '#edf0f5',
|
||||
'--lt-bg-secondary': '#e2e7ef',
|
||||
'--lt-bg-tertiary': '#d4dae6',
|
||||
'--lt-bg-card': '#ffffff',
|
||||
'--lt-bg-terminal': '#f4f6fa',
|
||||
'--lt-accent-orange': '#c44e00',
|
||||
'--lt-accent-orange-bright': '#d45800',
|
||||
'--lt-accent-orange-dim': 'rgba(196,78,0,0.10)',
|
||||
'--lt-accent-orange-border': 'rgba(196,78,0,0.28)',
|
||||
'--lt-accent-amber': '#8a5a00',
|
||||
'--lt-accent-amber-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-cyan': '#0062b8',
|
||||
'--lt-accent-cyan-bright': '#0070cc',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,98,184,0.10)',
|
||||
'--lt-accent-cyan-border': 'rgba(0,98,184,0.22)',
|
||||
'--lt-accent-green': '#006d35',
|
||||
'--lt-accent-green-bright': '#007d3e',
|
||||
'--lt-accent-green-dim': 'rgba(0,109,53,0.10)',
|
||||
'--lt-accent-green-dim': 'rgba(0,109,53,0.10)',
|
||||
'--lt-accent-green-border': 'rgba(0,109,53,0.22)',
|
||||
'--lt-accent-red': '#b5001f',
|
||||
'--lt-accent-red-dim': 'rgba(181,0,31,0.12)',
|
||||
'--lt-accent-gold': '#8a5a00',
|
||||
'--lt-accent-gold-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-purple': '#6b2fb8',
|
||||
'--lt-accent-purple-dim': 'rgba(107,47,184,0.10)',
|
||||
'--lt-text-primary': '#111827',
|
||||
'--lt-text-secondary': '#2d3d56',
|
||||
'--lt-text-muted': '#5a6e8c',
|
||||
'--lt-text-dim': '#8a9ab8',
|
||||
'--lt-border-color': 'rgba(50,80,130,0.18)',
|
||||
'--lt-border-color-hi': '#0062b8',
|
||||
'--lt-border-color-dim': 'rgba(50,80,130,0.09)',
|
||||
'--lt-glow-orange': '0 0 0 1px rgba(196,78,0,0.25), 0 1px 6px rgba(196,78,0,0.18)',
|
||||
'--lt-accent-red': '#b5001f',
|
||||
'--lt-accent-red-dim': 'rgba(181,0,31,0.12)',
|
||||
'--lt-accent-gold': '#8a5a00',
|
||||
'--lt-accent-gold-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-purple': '#6b2fb8',
|
||||
'--lt-accent-purple-dim': 'rgba(107,47,184,0.10)',
|
||||
'--lt-text-primary': '#111827',
|
||||
'--lt-text-secondary': '#2d3d56',
|
||||
'--lt-text-muted': '#5a6e8c',
|
||||
'--lt-text-dim': '#8a9ab8',
|
||||
'--lt-border-color': 'rgba(50,80,130,0.18)',
|
||||
'--lt-border-color-hi': '#0062b8',
|
||||
'--lt-border-color-dim': 'rgba(50,80,130,0.09)',
|
||||
'--lt-glow-orange': '0 0 0 1px rgba(196,78,0,0.25), 0 1px 6px rgba(196,78,0,0.18)',
|
||||
'--lt-glow-orange-intense': '0 0 0 2px rgba(196,78,0,0.35), 0 2px 10px rgba(196,78,0,0.25)',
|
||||
'--lt-glow-cyan': '0 0 0 1px rgba(0,98,184,0.25), 0 1px 6px rgba(0,98,184,0.18)',
|
||||
'--lt-glow-cyan-intense': '0 0 0 2px rgba(0,98,184,0.35), 0 2px 10px rgba(0,98,184,0.25)',
|
||||
'--lt-glow-green': '0 0 0 1px rgba(0,109,53,0.25), 0 1px 6px rgba(0,109,53,0.18)',
|
||||
'--lt-glow-green-intense': '0 0 0 2px rgba(0,109,53,0.35), 0 2px 10px rgba(0,109,53,0.25)',
|
||||
'--lt-glow-amber': '0 0 0 1px rgba(138,90,0,0.25), 0 1px 6px rgba(138,90,0,0.18)',
|
||||
'--lt-glow-amber-intense': '0 0 0 2px rgba(138,90,0,0.35), 0 2px 10px rgba(138,90,0,0.25)',
|
||||
'--lt-glow-red': '0 0 0 1px rgba(181,0,31,0.25), 0 1px 6px rgba(181,0,31,0.18)',
|
||||
'--lt-box-glow-orange': '0 0 0 2px rgba(196,78,0,0.22), 0 2px 8px rgba(196,78,0,0.12)',
|
||||
'--lt-box-glow-cyan': '0 0 0 2px rgba(0,98,184,0.22), 0 2px 8px rgba(0,98,184,0.12)',
|
||||
'--lt-box-glow-green': '0 0 0 2px rgba(0,109,53,0.22), 0 2px 8px rgba(0,109,53,0.12)',
|
||||
'--lt-box-glow-red': '0 0 0 2px rgba(181,0,31,0.22), 0 2px 8px rgba(181,0,31,0.12)',
|
||||
'--lt-box-glow-amber': '0 0 0 2px rgba(138,90,0,0.22), 0 2px 8px rgba(138,90,0,0.12)',
|
||||
'--lt-glow-cyan': '0 0 0 1px rgba(0,98,184,0.25), 0 1px 6px rgba(0,98,184,0.18)',
|
||||
'--lt-glow-cyan-intense': '0 0 0 2px rgba(0,98,184,0.35), 0 2px 10px rgba(0,98,184,0.25)',
|
||||
'--lt-glow-green': '0 0 0 1px rgba(0,109,53,0.25), 0 1px 6px rgba(0,109,53,0.18)',
|
||||
'--lt-glow-green-intense': '0 0 0 2px rgba(0,109,53,0.35), 0 2px 10px rgba(0,109,53,0.25)',
|
||||
'--lt-glow-amber': '0 0 0 1px rgba(138,90,0,0.25), 0 1px 6px rgba(138,90,0,0.18)',
|
||||
'--lt-glow-amber-intense': '0 0 0 2px rgba(138,90,0,0.35), 0 2px 10px rgba(138,90,0,0.25)',
|
||||
'--lt-glow-red': '0 0 0 1px rgba(181,0,31,0.25), 0 1px 6px rgba(181,0,31,0.18)',
|
||||
'--lt-box-glow-orange': '0 0 0 2px rgba(196,78,0,0.22), 0 2px 8px rgba(196,78,0,0.12)',
|
||||
'--lt-box-glow-cyan': '0 0 0 2px rgba(0,98,184,0.22), 0 2px 8px rgba(0,98,184,0.12)',
|
||||
'--lt-box-glow-green': '0 0 0 2px rgba(0,109,53,0.22), 0 2px 8px rgba(0,109,53,0.12)',
|
||||
'--lt-box-glow-red': '0 0 0 2px rgba(181,0,31,0.22), 0 2px 8px rgba(181,0,31,0.12)',
|
||||
'--lt-box-glow-amber': '0 0 0 2px rgba(138,90,0,0.22), 0 2px 8px rgba(138,90,0,0.12)',
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Scanlines + vignette: OFF in light mode (base.css:3676-3678)
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass}::before,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass}::after`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass}::after`,
|
||||
{ display: 'none' }
|
||||
);
|
||||
|
||||
// Caret
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} input,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea`,
|
||||
{ caretColor: '#c44e00' }
|
||||
);
|
||||
|
||||
@@ -462,9 +460,12 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ::-webkit-s
|
||||
background: 'rgba(0,98,184,0.25)',
|
||||
borderRadius: '3px',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ::-webkit-scrollbar-thumb:hover`, {
|
||||
background: 'rgba(0,98,184,0.50)',
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} ::-webkit-scrollbar-thumb:hover`,
|
||||
{
|
||||
background: 'rgba(0,98,184,0.50)',
|
||||
}
|
||||
);
|
||||
|
||||
// Selection
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ::selection`, {
|
||||
@@ -500,12 +501,12 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} pre`, {
|
||||
// Inline semantics
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} strong,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} b`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} b`,
|
||||
{ color: '#c44e00' }
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} em,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} i`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} i`,
|
||||
{ color: '#0062b8' }
|
||||
);
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} mark`, {
|
||||
@@ -514,7 +515,7 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} mark`, {
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} del,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} s`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} s`,
|
||||
{ color: '#b5001f' }
|
||||
);
|
||||
|
||||
@@ -534,15 +535,18 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} hr`, {
|
||||
// Input focus
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} input:focus,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
{
|
||||
borderColor: '#c44e00',
|
||||
boxShadow: '0 0 0 2px rgba(196,78,0,0.22), 0 1px 6px rgba(196,78,0,0.12)',
|
||||
}
|
||||
);
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [contenteditable="true"]:focus`, {
|
||||
boxShadow: '0 0 0 1px rgba(196,78,0,0.40), 0 1px 6px rgba(196,78,0,0.10)',
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [contenteditable="true"]:focus`,
|
||||
{
|
||||
boxShadow: '0 0 0 1px rgba(196,78,0,0.40), 0 1px 6px rgba(196,78,0,0.10)',
|
||||
}
|
||||
);
|
||||
|
||||
// Tables
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} table`, {
|
||||
@@ -610,33 +614,45 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data
|
||||
color: '#0062b8',
|
||||
transition: 'background 0.12s, border-color 0.12s, box-shadow 0.12s',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key]:hover`, {
|
||||
backgroundColor: 'rgba(0,98,184,0.12)',
|
||||
borderColor: 'rgba(0,98,184,0.42)',
|
||||
boxShadow: '0 0 7px rgba(0,98,184,0.16)',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]`, {
|
||||
backgroundColor: 'rgba(196,78,0,0.10)',
|
||||
border: '1px solid rgba(196,78,0,0.35)',
|
||||
color: '#c44e00',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]:hover`, {
|
||||
backgroundColor: 'rgba(196,78,0,0.18)',
|
||||
borderColor: 'rgba(196,78,0,0.55)',
|
||||
boxShadow: '0 0 7px rgba(196,78,0,0.18)',
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key]:hover`,
|
||||
{
|
||||
backgroundColor: 'rgba(0,98,184,0.12)',
|
||||
borderColor: 'rgba(0,98,184,0.42)',
|
||||
boxShadow: '0 0 7px rgba(0,98,184,0.16)',
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]`,
|
||||
{
|
||||
backgroundColor: 'rgba(196,78,0,0.10)',
|
||||
border: '1px solid rgba(196,78,0,0.35)',
|
||||
color: '#c44e00',
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]:hover`,
|
||||
{
|
||||
backgroundColor: 'rgba(196,78,0,0.18)',
|
||||
borderColor: 'rgba(196,78,0,0.55)',
|
||||
boxShadow: '0 0 7px rgba(196,78,0,0.18)',
|
||||
}
|
||||
);
|
||||
|
||||
// ── GIF picker (terminal mode) ───────────────────────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] form`, {
|
||||
background: '#030c14 !important' as any,
|
||||
color: '#e8edf5 !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
border: '1px solid rgba(255,107,0,0.35) !important' as any,
|
||||
borderRadius: '4px !important' as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
});
|
||||
globalStyle(
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] input,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] form`,
|
||||
{
|
||||
background: '#030c14 !important' as any,
|
||||
color: '#e8edf5 !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
border: '1px solid rgba(255,107,0,0.35) !important' as any,
|
||||
borderRadius: '4px !important' as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`, {
|
||||
borderColor: 'rgba(255,107,0,0.70) !important' as any,
|
||||
boxShadow: '0 0 0 2px rgba(255,107,0,0.12) !important' as any,
|
||||
@@ -645,10 +661,13 @@ globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`, {
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input::placeholder`, {
|
||||
color: 'rgba(255,107,0,0.40) !important' as any,
|
||||
});
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`, {
|
||||
display: 'none !important' as any,
|
||||
});
|
||||
globalStyle(
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`,
|
||||
{
|
||||
display: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar`, {
|
||||
width: '4px',
|
||||
});
|
||||
@@ -689,33 +708,50 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-url-p
|
||||
// ── GIF picker light TDS (dark-mode rules already exist via [data-gif-terminal]) ──
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] form`, {
|
||||
background: '#f4f6fa !important' as any,
|
||||
color: '#111827 !important' as any,
|
||||
border: '1px solid rgba(196,78,0,0.28) !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`, {
|
||||
borderColor: 'rgba(196,78,0,0.60) !important' as any,
|
||||
boxShadow: '0 0 0 2px rgba(196,78,0,0.12) !important' as any,
|
||||
outline: 'none !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input::placeholder`, {
|
||||
color: 'rgba(196,78,0,0.45) !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`, {
|
||||
display: 'none !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-track`, {
|
||||
background: '#e2e7ef',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-thumb`, {
|
||||
background: 'rgba(196,78,0,0.35)',
|
||||
borderRadius: '2px',
|
||||
});
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] form`,
|
||||
{
|
||||
background: '#f4f6fa !important' as any,
|
||||
color: '#111827 !important' as any,
|
||||
border: '1px solid rgba(196,78,0,0.28) !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`,
|
||||
{
|
||||
borderColor: 'rgba(196,78,0,0.60) !important' as any,
|
||||
boxShadow: '0 0 0 2px rgba(196,78,0,0.12) !important' as any,
|
||||
outline: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input::placeholder`,
|
||||
{
|
||||
color: 'rgba(196,78,0,0.45) !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`,
|
||||
{
|
||||
display: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-track`,
|
||||
{
|
||||
background: '#e2e7ef',
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-thumb`,
|
||||
{
|
||||
background: 'rgba(196,78,0,0.35)',
|
||||
borderRadius: '2px',
|
||||
}
|
||||
);
|
||||
|
||||
// ── Tooltip TDS ──────────────────────────────────────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} ._6plmi2g > div`, {
|
||||
@@ -749,10 +785,13 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [role="swit
|
||||
background: 'rgba(0,98,184,0.10) !important' as any,
|
||||
borderColor: 'rgba(0,98,184,0.35) !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [role="switch"][aria-checked="true"]`, {
|
||||
background: 'rgba(196,78,0,0.18) !important' as any,
|
||||
borderColor: 'rgba(196,78,0,0.55) !important' as any,
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [role="switch"][aria-checked="true"]`,
|
||||
{
|
||||
background: 'rgba(196,78,0,0.18) !important' as any,
|
||||
borderColor: 'rgba(196,78,0,0.55) !important' as any,
|
||||
}
|
||||
);
|
||||
|
||||
// ── Spinner TDS ───────────────────────────────────────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} ._31czpko`, {
|
||||
@@ -826,4 +865,3 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ._13tt0gb6:
|
||||
background: 'rgba(0,98,184,0.08) !important' as any,
|
||||
color: '#0062b8 !important' as any,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { MsgType } from 'matrix-js-sdk';
|
||||
|
||||
export const MATRIX_BLUR_HASH_PROPERTY_NAME = 'xyz.amorgan.blurhash';
|
||||
export const MATRIX_SPOILER_PROPERTY_NAME = 'page.codeberg.everypizza.msc4193.spoiler';
|
||||
export const MATRIX_SPOILER_REASON_PROPERTY_NAME = 'page.codeberg.everypizza.msc4193.spoiler.reason';
|
||||
export const MATRIX_SPOILER_REASON_PROPERTY_NAME =
|
||||
'page.codeberg.everypizza.msc4193.spoiler.reason';
|
||||
|
||||
export type IImageInfo = {
|
||||
w?: number;
|
||||
|
||||
@@ -10,7 +10,7 @@ function hashCode(str) {
|
||||
for (i = 0; i < str.length; i += 1) {
|
||||
chr = str.charCodeAt(i);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash = (hash << 5) - hash + chr;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
hash |= 0;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ async function deriveKeys(salt, iterations, password) {
|
||||
new TextEncoder().encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits'],
|
||||
['deriveBits']
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.importKey failed: ${e}`, cryptoFailMsg());
|
||||
@@ -53,40 +53,38 @@ async function deriveKeys(salt, iterations, password) {
|
||||
hash: 'SHA-512',
|
||||
},
|
||||
key,
|
||||
512,
|
||||
512
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.deriveBits failed: ${e}`, cryptoFailMsg());
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
console.log(`E2e import/export: deriveKeys took ${(now - start)}ms`);
|
||||
console.log(`E2e import/export: deriveKeys took ${now - start}ms`);
|
||||
|
||||
const aesKey = keybits.slice(0, 32);
|
||||
const hmacKey = keybits.slice(32);
|
||||
|
||||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{ name: 'AES-CTR' },
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
).catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for AES key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
const aesProm = subtleCrypto
|
||||
.importKey('raw', aesKey, { name: 'AES-CTR' }, false, ['encrypt', 'decrypt'])
|
||||
.catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for AES key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
|
||||
const hmacProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
).catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for HMAC key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
const hmacProm = subtleCrypto
|
||||
.importKey(
|
||||
'raw',
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify']
|
||||
)
|
||||
.catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for HMAC key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-return-await
|
||||
return await Promise.all([aesProm, hmacProm]);
|
||||
@@ -177,7 +175,6 @@ function unpackMegolmKeyFile(data) {
|
||||
return decodeBase64(fileStr.slice(dataStart, dataEnd));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ascii-armour a megolm key file
|
||||
*
|
||||
@@ -189,20 +186,20 @@ function unpackMegolmKeyFile(data) {
|
||||
function packMegolmKeyFile(data) {
|
||||
// we split into lines before base64ing, because encodeBase64 doesn't deal
|
||||
// terribly well with large arrays.
|
||||
const LINE_LENGTH = ((72 * 4) / 3);
|
||||
const LINE_LENGTH = (72 * 4) / 3;
|
||||
const nLines = Math.ceil(data.length / LINE_LENGTH);
|
||||
const lines = new Array(nLines + 3);
|
||||
lines[0] = HEADER_LINE;
|
||||
let o = 0;
|
||||
let i;
|
||||
for (i = 1; i <= nLines; i += 1) {
|
||||
lines[i] = encodeBase64(data.subarray(o, o+LINE_LENGTH));
|
||||
lines[i] = encodeBase64(data.subarray(o, o + LINE_LENGTH));
|
||||
o += LINE_LENGTH;
|
||||
}
|
||||
lines[i] = TRAILER_LINE;
|
||||
i += 1;
|
||||
lines[i] = '';
|
||||
return (new TextEncoder().encode(lines.join('\n'))).buffer;
|
||||
return new TextEncoder().encode(lines.join('\n')).buffer;
|
||||
}
|
||||
|
||||
export async function decryptMegolmKeyFile(data, password) {
|
||||
@@ -225,7 +222,7 @@ export async function decryptMegolmKeyFile(data, password) {
|
||||
|
||||
const salt = body.subarray(1, 1 + 16);
|
||||
const iv = body.subarray(17, 17 + 16);
|
||||
const iterations = body[33] << 24 | body[34] << 16 | body[35] << 8 | body[36];
|
||||
const iterations = (body[33] << 24) | (body[34] << 16) | (body[35] << 8) | body[36];
|
||||
const ciphertext = body.subarray(37, 37 + ciphertextLength);
|
||||
const hmac = body.subarray(-32);
|
||||
|
||||
@@ -234,12 +231,7 @@ export async function decryptMegolmKeyFile(data, password) {
|
||||
|
||||
let isValid;
|
||||
try {
|
||||
isValid = await subtleCrypto.verify(
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
hmac,
|
||||
toVerify,
|
||||
);
|
||||
isValid = await subtleCrypto.verify({ name: 'HMAC' }, hmacKey, hmac, toVerify);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.verify failed: ${e}`, cryptoFailMsg());
|
||||
}
|
||||
@@ -256,7 +248,7 @@ export async function decryptMegolmKeyFile(data, password) {
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
ciphertext,
|
||||
ciphertext
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.decrypt failed: ${e}`, cryptoFailMsg());
|
||||
@@ -302,34 +294,33 @@ export async function encryptMegolmKeyFile(data, password, options) {
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
encodedData,
|
||||
encodedData
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.encrypt failed: ' + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
const cipherArray = new Uint8Array(ciphertext);
|
||||
const bodyLength = (1+salt.length+iv.length+4+cipherArray.length+32);
|
||||
const bodyLength = 1 + salt.length + iv.length + 4 + cipherArray.length + 32;
|
||||
const resultBuffer = new Uint8Array(bodyLength);
|
||||
let idx = 0;
|
||||
resultBuffer[idx++] = 1; // version
|
||||
resultBuffer.set(salt, idx); idx += salt.length;
|
||||
resultBuffer.set(iv, idx); idx += iv.length;
|
||||
resultBuffer.set(salt, idx);
|
||||
idx += salt.length;
|
||||
resultBuffer.set(iv, idx);
|
||||
idx += iv.length;
|
||||
resultBuffer[idx++] = kdfRounds >> 24;
|
||||
resultBuffer[idx++] = (kdfRounds >> 16) & 0xff;
|
||||
resultBuffer[idx++] = (kdfRounds >> 8) & 0xff;
|
||||
resultBuffer[idx++] = kdfRounds & 0xff;
|
||||
resultBuffer.set(cipherArray, idx); idx += cipherArray.length;
|
||||
resultBuffer.set(cipherArray, idx);
|
||||
idx += cipherArray.length;
|
||||
|
||||
const toSign = resultBuffer.subarray(0, idx);
|
||||
|
||||
let hmac;
|
||||
try {
|
||||
hmac = await subtleCrypto.sign(
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
toSign,
|
||||
);
|
||||
hmac = await subtleCrypto.sign({ name: 'HMAC' }, hmacKey, toSign);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.sign failed: ' + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user