Compare commits

..

5 Commits

Author SHA1 Message Date
Ajay Bura 1e1c487642 fix success and critical color hover+active color 2026-03-12 13:29:14 +05:30
Ajay Bura 43e3a9c290 add bluish tone to text color 2026-03-12 12:50:25 +05:30
Ajay Bura 66a18bdc10 make call room name transparent 2026-03-12 12:38:32 +05:30
Ajay Bura 649c30bcc7 change gradient direction toward top 2026-03-12 12:33:54 +05:30
Ajay Bura e039299e83 Gradient Theme - Moonlight 2026-03-12 12:16:03 +05:30
103 changed files with 635 additions and 2260 deletions
-17
View File
@@ -1,17 +0,0 @@
{
"defaultHomeserver": 0,
"homeserverList": [
"matrix.lotusguild.org"
],
"allowCustomHomeservers": false,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [],
"rooms": [],
"servers": []
},
"hashRouter": {
"enabled": false,
"basename": "/"
}
}
-17
View File
@@ -1,17 +0,0 @@
{
"defaultHomeserver": 0,
"homeserverList": [
"matrix.lotusguild.org"
],
"allowCustomHomeservers": false,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [],
"rooms": [],
"servers": []
},
"hashRouter": {
"enabled": false,
"basename": "/"
}
}
+98 -49
View File
@@ -1,62 +1,111 @@
# Lotus Chat # Cinny
<p>
<a href="https://github.com/ajbura/cinny/releases">
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
<a href="https://hub.docker.com/r/ajbura/cinny">
<img alt="DockerHub downloads" src="https://img.shields.io/docker/pulls/ajbura/cinny?logo=docker&style=social"></a>
<a href="https://fosstodon.org/@cinnyapp">
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
<a href="https://twitter.com/intent/follow?screen_name=cinnyapp">
<img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/cinnyapp?logo=twitter&style=social"></a>
<a href="https://cinny.in/#sponsor">
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
</p>
A Matrix client for [Lotus Guild](https://lotusguild.org) — forked from [Cinny](https://github.com/cinnyapp/cinny) v4.11.1. A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
- [Contributing](./CONTRIBUTING.md)
Deployed at [chat.lotusguild.org](https://chat.lotusguild.org). <img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
--- ## Getting started
The web app is available at [app.cinny.in](https://app.cinny.in/) and gets updated on each new release. The `dev` branch is continuously deployed at [dev.cinny.in](https://dev.cinny.in) but keep in mind that it could have things broken.
## Changes from upstream Cinny You can also download our desktop app from the [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
### Branding & Identity ## Self-hosting
To host Cinny on your own, simply download the tarball from [GitHub releases](https://github.com/cinnyapp/cinny/releases/latest), and serve the files from `dist/` using your preferred webserver. Alternatively, you can just pull the docker image from [DockerHub](https://hub.docker.com/r/ajbura/cinny) or [GitHub Container Registry](https://github.com/cinnyapp/cinny/pkgs/container/cinny).
- Package renamed to `lotus-chat`, description updated to "Lotus Chat — Matrix client for Lotus Guild" * The default homeservers and explore pages are defined in [`config.json`](config.json).
- App title changed from "Cinny" to "Lotus Chat" throughout
- Favicon, PWA icons, and all icon sizes (57×57 → 180×180 Apple touch icons) replaced with Lotus.png variants
- Logo in About dialog and Auth page replaced with official Lotus.png
- Auth footer rewritten: shows dynamic version from `package.json`, links to lotusguild.org, chat.lotusguild.org, and matrix.lotusguild.org
- Welcome page tagline changed from "Yet another matrix client" to "A Matrix client for Lotus Guild"
- Encryption key export filename changed from `cinny-keys.txt` to `lotus-keys.txt`
- `manifest.json` updated with Lotus name, description, and branding colors
### LotusGuild Terminal Design System (TDS) v1.2 * You need to set up redirects to serve the assests. Example configurations; [netlify](netlify.toml), [nginx](contrib/nginx/cinny.domain.tld.conf), [caddy](contrib/caddy/caddyfile).
* If you have trouble configuring redirects you can [enable hash routing](config.json#L35) — the url in the browser will have a `/#/` between the domain and open channel (ie. `app.cinny.in/#/home/` instead of `app.cinny.in/home/`) but you won't have to configure your webserver.
A full custom theme engine layered on top of Cinny's vanilla-extract theming: * To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts).
* For example, if you want to deploy on `https://cinny.in/app`, then set `base: '/app'`.
**Dark mode** (`LotusTerminalTheme`): <details><summary><b>PGP Public Key to verify tarball</b></summary>
- CRT terminal aesthetic: scanline overlay, vignette, phosphor glow
- Palette: bg `#030508`, orange `#FF6B00`, cyan `#00D4FF`, green `#00FF88`, text `#c4d9ee`
- Monospace font stack, terminal-style scrollbars
- Custom hex-grid and circuit-board CSS background patterns
- Matrix-style boot messages on the welcome page
- CSS variables: `--lt-*` family covering colors, glow effects, borders, animations
**Light mode** (`LotusTerminalLightTheme`): ```
- Full light palette: bg `#edf0f5`, orange `#c44e00`, cyan `#0062b8`, green `#006d35`, text `#111827` -----BEGIN PGP PUBLIC KEY BLOCK-----
- No CRT effects (scanlines, vignette disabled)
- Light-mode scrollbars, adjusted code block colors, semantic color overrides
- Scoped to `html[data-theme="light"] body.lotusTerminalBodyClass`
- `ThemeManager.tsx` sets `data-theme` attribute based on active theme kind
**Chat Backgrounds** (18+ custom patterns, all TDS-aware): mQGNBGJw/g0BDAC8qQeLqDMzYzfPyOmRlHVEoguVTo+eo1aVdQH2X7OELdjjBlyj
- Blueprint grid, carbon fiber, starfield, topographic contours, herringbone, crosshatch 6d6c1adv/uF2g83NNMoQY7GEeHjRnXE4m8kYSaarb840pxrYUagDc0dAbJOGaCBY
- Chevron, polka dots, triangles, plaid FKTo7U1Kvg0vdiaRuus0pvc1NVdXSxRNQbFXBSwduD+zn66TI3HfcEHNN62FG1cE
- All patterns use CSS custom properties — adapt to both TDS dark and light themes K1jWDwLAU0P3kKmj8+CAc3h9ZklPu0k/+t5bf/LJkvdBJAUzGZpehbPL5f3u3BZ0
- Settings toggle for showing per-message sender profiles leZLIrR8uV7PiV5jKFahxlKR5KQHld8qQm+qVhYbUzpuMBGmh419I6UvTzxuRcvU
Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
AdQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQSRri2MHidaaZv+
vvuUMwx6UK/M8wUCZqEDwAUJFvwIswAKCRCUMwx6UK/M877qC/4lxXOQIoWnLLkK
YiRCTkGsH6NdxgeYr6wpXT4xuQ45ZxCytwHpOGQmO/5up5961TxWW8D1frRIJHjj
AZGoRCL3EKEuY8nt3D99fpf3DvZrs1uoVAhiyn737hRlZAg+QsJheeGCmdSJ0hX5
Yud8SE+9zxLS1+CEjMrsUd/RGre/phme+wNXfaHfREAC9ewolgVChPIbMxG2f+vs
K8Xv52BFng7ta9fgsl1XuOjpuaSbQv6g+4ONk/lxKF0SmnhEGM3dmIYPONxW47Yf
atnIjRra/YhPTNwrNBGMmG4IFKaOsMbjW/eakjWTWOVKKJNBMoDdRcYYWIMCpLy8
AQUrMtQEsHSnqCwrw818S5A6rrhcfVGk36RGm0nOy6LS5g5jmqaYsvbCcBGY9B2c
SUAVNm17oo7TtEajk8hcSXoZod1t++pyjcVKEmSn3nFK7v5m3V+cPhNTxZMK459P
3x1Ucqj/kTqrxKw6s2Uknuk0ajmw0ljV+BQwgL6maguo9BKgCNW5AY0EYnD+DQEM
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
QPqfGDpowBwRkkOsGz/XVcesJ1Pzg4bKivTS9kZjZSyT9RRSY8As0sVUN57AwYul
s1+eh00n/tVpi2Jj9pCm7S0csSXvXj8v2OTdK1jt4YjpzR0/rwh4+/xlOjDjZEqH
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
NwARAQABiQG8BBgBCAAmAhsMFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmahA9IF
CRb8CMUACgkQlDMMelCvzPPQgQv/d5/z+fxgKqgfhQX+V49X4WgTVxZ/CzztDoJ1
XAq1dzTNEy8AFguXIo6eVXPSpMxec7ZreN3+UPQBnCf3eR5YxWNYOYKmk0G4E8D2
KGUJept7TSA42/8N2ov6tToXFg4CgzKZj0fYLwgutly7K8eiWmSU6ptaO8aEQBHB
gTGIOO3h6vJMGVycmoeRnHjv4wV84YWSVFSoJ7cY0he4Z9UznJBbE/KHZjrkXsPo
N+Gg5lDuOP5xjKzM5SogV9lhxBAhMWAg3URUF15yruZBiA8uV1FOK8sal/9C1G7V
M6ygA6uOZqXlZtcdA94RoSsW2pZ9eLVPsxz2B3Zko7tu11MpNP/wYmfGTI3KxZBj
n/eodvwjJSgHpGOFSmbNzvPJo3to5nNlp7wH1KxIMc6Uuu9hgfDfwkFZgV2bnFIa
Q6gyF548Ub48z7Dz83+WwLgbX19ve4oZx+dqSdczP6ILHRQomtrzrkkP2LU52oI5
mxFo+ioe/ABCufSmyqFye0psX3Sp
=WtqZ
-----END PGP PUBLIC KEY BLOCK-----
```
</details>
### Voice / Video Call Improvements ## Local development
> [!TIP]
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Krypton LTS (v24.13.1).
- **Element Call 0.19.3**: Upgraded from 0.16.3. Dist copied to `public/element-call/` by vite at build time. Execute the following commands to start a development server:
- **Camera default OFF**: Camera no longer persists across sessions via localStorage. Always starts disabled. Optional `cameraOnJoin` setting for explicit opt-in. ```sh
- **Deafen button**: Tooltip corrected to "Deafen" / "Undeafen" (was "Turn Off Sound" / "Turn On Sound") npm ci # Installs all dependencies
- **Screenshare confirmation**: A confirm dialog appears before screenshare is broadcast to call participants npm start # Serve a development version
- **Auto-revert spotlight on screenshare**: When someone starts screensharing, EC normally forces all participants into spotlight view. Patched in `CallControl.ts` `onControlMutation()` — detects the screenshare button going `primary` and clicks `gridButton` after 600ms to revert to grid layout. Participants choose to watch screenshare manually. ```
- **Push to Talk (PTT)**:
- Configurable keybind (default: Space) via Settings > General > Calls To build the app:
- Mic activates on keydown, deactivates on keyup; mic muted on tab blur/focus to prevent stuck-on mic ```sh
- Visual indicator: plain folds `Chip` by default; when LotusGuild TDS is active: orange `PTT — Hold SPACE` / green `● LIVE` in JetBrains Mono npm run build # Compiles the app into the dist/ directory
- Listens on both main window and EC iframe `contentWindow` for reliable key capture ```
- Implemented via `CallControl.setMicrophone()` public method on the widget bridge
- **Noise suppression toggle**: Settings > General > Calls — passes `noiseSuppression` URL parameter to the embedded Element Call widget ### Running with Docker
- **DM calls**: Phone button in DM room header starts a 1-on-1 call via `useCallStart(true)`. `Room.tsx` switches to CallView layout when the DM has an active call embed. Standard voice rooms were already supported; this extends call support to all direct messages. This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
- **Picture-in-picture (PiP)**: When navigating away from a call room while in an active call, the call embed shrinks to a 280x158px floating window in the bottom-right corner. The PiP window is **draggable** — drag it anywhere on screen to move it out of the way. Clicking (without dragging) navigates back to the call room. Drag vs click distinguished by a 5px movement threshold; touch drag supported. Imperative style overrides on `callEmbedRef.current` via `useEffect` — a wrapper div cannot be used because `useCallEmbedPlacementSync` writes `top/left/width/height` directly onto that element. use this locally, you can build the container like so:
```
docker build -t cinny:latest .
```
You can then run the container you've built with a command similar to this:
```
docker run -p 8080:80 cinny:latest
```
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
+22 -8
View File
@@ -1,15 +1,29 @@
{ {
"defaultHomeserver": 0, "defaultHomeserver": 1,
"homeserverList": [ "homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"],
"matrix.lotusguild.org" "allowCustomHomeservers": true,
],
"allowCustomHomeservers": false,
"featuredCommunities": { "featuredCommunities": {
"openAsDefault": false, "openAsDefault": false,
"spaces": [], "spaces": [
"rooms": [], "#cinny-space:matrix.org",
"servers": [] "#community:matrix.org",
"#space:unredacted.org",
"#science-space:matrix.org",
"#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org"
],
"rooms": [
"#cinny:matrix.org",
"#freesoftware:matrix.org",
"#pcapdroid:matrix.org",
"#gentoo:matrix.org",
"#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org"
],
"servers": ["matrix.org", "mozilla.org", "unredacted.org"]
}, },
"hashRouter": { "hashRouter": {
"enabled": false, "enabled": false,
"basename": "/" "basename": "/"
+11 -14
View File
@@ -4,36 +4,33 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>Lotus Chat</title> <title>Cinny</title>
<meta name="name" content="Lotus Chat" /> <meta name="name" content="Cinny" />
<meta name="author" content="Lotus Guild" /> <meta name="author" content="Ajay Bura" />
<meta <meta
name="description" name="description"
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community." content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
/> />
<meta <meta
name="keywords" name="keywords"
content="lotus chat, lotus guild, matrix, matrix client" content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
/> />
<meta property="og:title" content="Lotus Chat" /> <meta property="og:title" content="Cinny" />
<meta property="og:url" content="https://chat.lotusguild.org" /> <meta property="og:url" content="https://cinny.in" />
<meta property="og:image" content="https://chat.lotusguild.org/public/res/android/android-chrome-192x192.png" /> <meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
<meta <meta
property="og:description" property="og:description"
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community." content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
/> />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<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 id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="application-name" content="Lotus Chat" /> <meta name="application-name" content="Cinny" />
<meta name="apple-mobile-web-app-title" content="Lotus Chat" /> <meta name="apple-mobile-web-app-title" content="Cinny" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
+43 -22
View File
@@ -1,12 +1,12 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.11.1", "version": "4.11.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cinny", "name": "cinny",
"version": "4.11.1", "version": "4.11.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6", "@atlaskit/pragmatic-drag-and-drop": "1.1.6",
@@ -59,10 +59,10 @@
"react-range": "1.8.14", "react-range": "1.8.14",
"react-router-dom": "6.30.3", "react-router-dom": "6.30.3",
"sanitize-html": "2.12.1", "sanitize-html": "2.12.1",
"slate": "0.123.0", "slate": "0.112.0",
"slate-dom": "0.123.0", "slate-dom": "0.112.2",
"slate-history": "0.113.1", "slate-history": "0.110.3",
"slate-react": "0.123.0", "slate-react": "0.112.1",
"ua-parser-js": "1.0.35" "ua-parser-js": "1.0.35"
}, },
"devDependencies": { "devDependencies": {
@@ -10291,15 +10291,20 @@
} }
}, },
"node_modules/slate": { "node_modules/slate": {
"version": "0.123.0", "version": "0.112.0",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.123.0.tgz", "resolved": "https://registry.npmjs.org/slate/-/slate-0.112.0.tgz",
"integrity": "sha512-Oon3HR/QzJQBjuOUJT1jGGlp8Ff7t3Bkr/rJ2lDqxNT4H+cBnXpEVQ/si6hn1ZCHhD2xY/2N91PQoH/rD7kxTg==", "integrity": "sha512-PRnfFgDA3tSop4OH47zu4M1R4Uuhm/AmASu29Qp7sGghVFb713kPBKEnSf1op7Lx/nCHkRlCa3ThfHtCBy+5Yw==",
"license": "MIT" "license": "MIT",
"dependencies": {
"immer": "^10.0.3",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
}
}, },
"node_modules/slate-dom": { "node_modules/slate-dom": {
"version": "0.123.0", "version": "0.112.2",
"resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.123.0.tgz", "resolved": "https://registry.npmjs.org/slate-dom/-/slate-dom-0.112.2.tgz",
"integrity": "sha512-OUinp4tvSrAlt64JL9y20Xin08jgnnj1gJmIuPdGvU5MELKXRNZh17a7EKKNOS6OZPAE8Dk9NI1MAIS/Qz0YBw==", "integrity": "sha512-cozITMlpcBxrov854reM6+TooiHiqpfM/nZPrnjpN1wSiDsAQmYbWUyftC+jlwcpFj80vywfDHzlG6hXIc5h6A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
@@ -10311,13 +10316,13 @@
"tiny-invariant": "1.3.1" "tiny-invariant": "1.3.1"
}, },
"peerDependencies": { "peerDependencies": {
"slate": ">=0.121.0" "slate": ">=0.99.0"
} }
}, },
"node_modules/slate-history": { "node_modules/slate-history": {
"version": "0.113.1", "version": "0.110.3",
"resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.113.1.tgz", "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.110.3.tgz",
"integrity": "sha512-J9NSJ+UG2GxoW0lw5mloaKcN0JI0x2IA5M5FxyGiInpn+QEutxT1WK7S/JneZCMFJBoHs1uu7S7e6pxQjubHmQ==", "integrity": "sha512-sgdff4Usdflmw5ZUbhDkxFwCBQ2qlDKMMkF93w66KdV48vHOgN2BmLrf+2H8SdX8PYIpP/cTB0w8qWC2GwhDVA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-plain-object": "^5.0.0" "is-plain-object": "^5.0.0"
@@ -10327,14 +10332,15 @@
} }
}, },
"node_modules/slate-react": { "node_modules/slate-react": {
"version": "0.123.0", "version": "0.112.1",
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.123.0.tgz", "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.112.1.tgz",
"integrity": "sha512-nQwXL1FEacrY9ZFmatRhoBnsySNUX2x6qB77V3oNHd7wWxBJWuzz4GMrBXcVoRE8Gac7Angf8xaNGzb6zcPlHg==", "integrity": "sha512-V9b+waxPweXqAkSQmKQ1afG4Me6nVQACPpxQtHPIX02N7MXa5f5WilYv+bKt7vKKw+IZC2F0Gjzhv5BekVgP/A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"direction": "^1.0.4", "direction": "^1.0.4",
"is-hotkey": "^0.2.0", "is-hotkey": "^0.2.0",
"is-plain-object": "^5.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"scroll-into-view-if-needed": "^3.1.0", "scroll-into-view-if-needed": "^3.1.0",
"tiny-invariant": "1.3.1" "tiny-invariant": "1.3.1"
@@ -10342,8 +10348,18 @@
"peerDependencies": { "peerDependencies": {
"react": ">=18.2.0", "react": ">=18.2.0",
"react-dom": ">=18.2.0", "react-dom": ">=18.2.0",
"slate": ">=0.121.0", "slate": ">=0.99.0",
"slate-dom": ">=0.119.1" "slate-dom": ">=0.110.2"
}
},
"node_modules/slate/node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
} }
}, },
"node_modules/smob": { "node_modules/smob": {
@@ -10713,6 +10729,11 @@
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.10", "version": "0.2.10",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz",
+7 -7
View File
@@ -1,7 +1,7 @@
{ {
"name": "lotus-chat", "name": "cinny",
"version": "4.11.1", "version": "4.11.0",
"description": "Lotus Chat \u2014 Matrix client for Lotus Guild", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"engines": { "engines": {
@@ -71,10 +71,10 @@
"react-range": "1.8.14", "react-range": "1.8.14",
"react-router-dom": "6.30.3", "react-router-dom": "6.30.3",
"sanitize-html": "2.12.1", "sanitize-html": "2.12.1",
"slate": "0.123.0", "slate": "0.112.0",
"slate-dom": "0.123.0", "slate-dom": "0.112.2",
"slate-history": "0.113.1", "slate-history": "0.110.3",
"slate-react": "0.123.0", "slate-react": "0.112.1",
"ua-parser-js": "1.0.35" "ua-parser-js": "1.0.35"
}, },
"devDependencies": { "devDependencies": {
-17
View File
@@ -1,17 +0,0 @@
{
"defaultHomeserver": 0,
"homeserverList": [
"matrix.lotusguild.org"
],
"allowCustomHomeservers": false,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [],
"rooms": [],
"servers": []
},
"hashRouter": {
"enabled": false,
"basename": "/"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 32 KiB

+5 -24
View File
@@ -1,14 +1,14 @@
{ {
"name": "Lotus Chat", "name": "Cinny",
"short_name": "Lotus Chat", "short_name": "Cinny",
"description": "Lotus Chat \u2014 the Lotus Guild Matrix client", "description": "Yet another matrix client",
"dir": "auto", "dir": "auto",
"lang": "en-US", "lang": "en-US",
"display": "standalone", "display": "standalone",
"orientation": "portrait", "orientation": "portrait",
"start_url": "./", "start_url": "./",
"background_color": "#0a0a0a", "background_color": "#fff",
"theme_color": "#980000", "theme_color": "#fff",
"icons": [ "icons": [
{ {
"src": "./public/android/android-chrome-36x36.png", "src": "./public/android/android-chrome-36x36.png",
@@ -55,24 +55,5 @@
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
],
"categories": [
"social",
"communication",
"productivity"
],
"shortcuts": [
{
"name": "New Message",
"short_name": "DM",
"description": "Open a new direct message",
"url": "/",
"icons": [
{
"src": "res/android/android-chrome-96x96.png",
"sizes": "96x96"
}
]
}
] ]
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

+11 -12
View File
@@ -1,14 +1,13 @@
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g fill="#980000" fill-opacity="0.88"> <g clip-path="url(#clip0_2707_1961)">
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/> <path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/> <path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/> <path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#45B83B"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
</g> </g>
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/> <defs>
<circle cx="14.5" cy="14.5" r="3" fill="#45B83B"/> <clipPath id="clip0_2707_1961">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 841 B

After

Width:  |  Height:  |  Size: 1.5 KiB

+11 -12
View File
@@ -1,14 +1,13 @@
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g fill="#980000" fill-opacity="0.88"> <g clip-path="url(#clip0_2707_2015)">
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/> <path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/> <path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/> <path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#989898"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
</g> </g>
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/> <defs>
<circle cx="14.5" cy="14.5" r="3" fill="#989898"/> <clipPath id="clip0_2707_2015">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 841 B

After

Width:  |  Height:  |  Size: 1.5 KiB

+17 -11
View File
@@ -1,13 +1,19 @@
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In -->
<g fill="#980000" fill-opacity="0.88"> <svg version="1.1"
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/> xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/> x="0px" y="0px" width="18px" height="18px" viewBox="0 0 18 18" enable-background="new 0 0 18 18" xml:space="preserve">
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/> <defs>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/> </defs>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/> <g>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/> <g>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/> <circle fill="#FFFFFF" cx="9" cy="9" r="8.5"/>
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/> </g>
<g>
<path d="M9,0C4,0,0,4,0,9c0,5,4,9,9,9c5,0,9-4,9-9C18,4,14,0,9,0z M1.2,10.8l3.5-2.3c0-0.1,0-0.2,0-0.3c0-1.8,1.3-3.2,3.1-3.4
c0.1,0,0.2,0,0.4,0c1.2,0,2.3,0.6,2.9,1.6c0.3-0.1,0.6-0.1,0.9-0.1c0.4,0,0.8,0,1.2,0.1c0.7,0.2,1.4,0.5,2,0.9
C14.6,7.1,14,7,13.3,7c-1.2,0-2.2,0.4-2.9,1.4c-0.7,0.9-1.1,2-1.1,3.2c0,1.5-0.4,2.9-1.3,4.2c-0.3,0.4-0.5,0.7-0.8,1
C4.2,16.1,1.9,13.8,1.2,10.8z"/>
<circle cx="9.5" cy="6.4" r="0.5"/>
</g>
</g> </g>
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 788 B

After

Width:  |  Height:  |  Size: 871 B

+4 -312
View File
@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useEffect, useRef } from 'react'; import React, { ReactNode, useCallback, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { config } from 'folds'; import { config } from 'folds';
import { import {
@@ -13,10 +13,6 @@ import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call'; import { CallEmbed } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom'; import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
import { useRoomNavigate } from '../hooks/useRoomNavigate';
const PIP_MIN_W = 200;
const PIP_MIN_H = 112; // keeps roughly 16:9 at minimum
function CallUtils({ embed }: { embed: CallEmbed }) { function CallUtils({ embed }: { embed: CallEmbed }) {
const setCallEmbed = useSetAtom(callEmbedAtom); const setCallEmbed = useSetAtom(callEmbedAtom);
@@ -33,19 +29,6 @@ function CallUtils({ embed }: { embed: CallEmbed }) {
return null; return null;
} }
type Corner = 'se' | 'sw' | 'ne' | 'nw';
/** Normalise the element to top/left positioning so resize math is uniform. */
function normaliseToTopLeft(el: HTMLDivElement) {
const rect = el.getBoundingClientRect();
el.style.top = `${rect.top}px`;
el.style.left = `${rect.left}px`;
el.style.right = 'auto';
el.style.bottom = 'auto';
el.style.width = `${rect.width}px`;
el.style.height = `${rect.height}px`;
}
type CallEmbedProviderProps = { type CallEmbedProviderProps = {
children?: ReactNode; children?: ReactNode;
}; };
@@ -57,246 +40,10 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
const selectedRoom = useSelectedRoom(); const selectedRoom = useSelectedRoom();
const chat = useAtomValue(callChatAtom); const chat = useAtomValue(callChatAtom);
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const { navigateRoom } = useRoomNavigate();
const chatOnlyView = chat && screenSize !== ScreenSize.Desktop; const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
const inCallRoom = callEmbed && selectedRoom === callEmbed.roomId;
const callActive = callEmbed && joined;
const callVisible = inCallRoom && callActive && !chatOnlyView;
const pipMode = callActive && !inCallRoom;
const pipDragRef = useRef<{ const callVisible = callEmbed && selectedRoom === callEmbed.roomId && joined && !chatOnlyView;
startX: number;
startY: number;
origLeft: number;
origTop: number;
dragged: boolean;
} | null>(null);
// useCallEmbedPlacementSync writes top/left/width/height directly on this element.
// Override those in PiP mode; clear them when returning so it can take back control.
useEffect(() => {
const el = callEmbedRef.current;
if (!el) return;
if (pipMode) {
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 {
el.style.top = '';
el.style.left = '';
el.style.bottom = '';
el.style.right = '';
el.style.width = '';
el.style.height = '';
el.style.borderRadius = '';
el.style.overflow = '';
el.style.zIndex = '';
el.style.boxShadow = '';
el.style.border = '';
el.style.visibility = callVisible ? '' : 'hidden';
}
}, [pipMode, callVisible]);
// ── Drag to move ────────────────────────────────────────────────────────────
const handlePipMouseDown = (e: React.MouseEvent) => {
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,
};
const onMouseMove = (ev: MouseEvent) => {
if (!pipDragRef.current || !el) return;
const dx = ev.clientX - pipDragRef.current.startX;
const 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) {
const newLeft = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx));
const newTop = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy));
el.style.left = `${newLeft}px`;
el.style.top = `${newTop}px`;
el.style.right = 'auto';
el.style.bottom = 'auto';
}
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
setTimeout(() => {
if (pipDragRef.current) pipDragRef.current.dragged = false;
}, 0);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const handlePipTouchStart = (e: React.TouchEvent) => {
const el = callEmbedRef.current;
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,
};
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) {
const newLeft = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx));
const newTop = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy));
el.style.left = `${newLeft}px`;
el.style.top = `${newTop}px`;
el.style.right = 'auto';
el.style.bottom = 'auto';
}
};
const onTouchEnd = () => {
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
setTimeout(() => {
if (pipDragRef.current) pipDragRef.current.dragged = false;
}, 0);
};
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
};
const handlePipClick = (roomId: string) => {
if (pipDragRef.current?.dragged) return;
navigateRoom(roomId);
};
// ── Resize from corner handles ───────────────────────────────────────────────
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
e.stopPropagation();
e.preventDefault();
const el = callEmbedRef.current;
if (!el) return;
normaliseToTopLeft(el);
const startX = e.clientX;
const startY = e.clientY;
const startW = el.offsetWidth;
const startH = el.offsetHeight;
const startL = parseFloat(el.style.left);
const startT = parseFloat(el.style.top);
document.body.style.cursor = `${corner}-resize`;
document.body.style.userSelect = 'none';
const onMouseMove = (ev: MouseEvent) => {
const dx = ev.clientX - startX;
const dy = ev.clientY - startY;
let w = startW;
let h = startH;
let l = startL;
let t = startT;
if (corner === 'se') { w = startW + dx; h = startH + dy; }
if (corner === 'sw') { w = startW - dx; h = startH + dy; l = startL + startW - Math.max(PIP_MIN_W, w); }
if (corner === 'ne') { w = startW + dx; h = startH - dy; t = startT + startH - Math.max(PIP_MIN_H, h); }
if (corner === 'nw') { w = startW - dx; h = startH - dy; l = startL + startW - Math.max(PIP_MIN_W, w); t = startT + startH - 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 onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
// Corner handle style helper
const cornerHandle = (corner: Corner): React.CSSProperties => {
const s = corner.includes('s');
const e = corner.includes('e');
return {
position: 'absolute',
width: '18px',
height: '18px',
[s ? 'bottom' : 'top']: 0,
[e ? 'right' : 'left']: 0,
cursor: `${corner}-resize`,
zIndex: 2,
};
};
// Grip dot pattern rendered inside each handle
const gripDots = (corner: Corner) => {
const s = corner.includes('s');
const e = corner.includes('e');
// 3 dots arranged in an L toward the active corner
const dots: React.CSSProperties[] = [];
const r = 2; // dot radius px
const gap = 5;
const base = 3;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i + j < 2) continue; // only the 3 corner-most dots
dots.push({
position: 'absolute',
width: r * 2,
height: r * 2,
borderRadius: '50%',
background: 'rgba(255,255,255,0.45)',
[s ? 'bottom' : 'top']: base + i * gap,
[e ? 'right' : 'left']: base + j * gap,
});
}
}
return dots;
};
return ( return (
<CallEmbedContextProvider value={callEmbed}> <CallEmbedContextProvider value={callEmbed}>
@@ -304,7 +51,6 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
<CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider> <CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider>
<div <div
data-call-embed-container data-call-embed-container
ref={callEmbedRef}
style={{ style={{
visibility: callVisible ? undefined : 'hidden', visibility: callVisible ? undefined : 'hidden',
position: 'fixed', position: 'fixed',
@@ -313,62 +59,8 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
width: '100%', width: '100%',
height: '50%', height: '50%',
}} }}
> ref={callEmbedRef}
{pipMode && callEmbed && ( />
<>
{/* Drag-to-move overlay (zIndex 1, sits over the iframe) */}
<div
role="button"
tabIndex={0}
aria-label="Return to call"
onMouseDown={handlePipMouseDown}
onTouchStart={handlePipTouchStart}
onClick={() => handlePipClick(callEmbed.roomId)}
onKeyDown={(e) => e.key === 'Enter' && handlePipClick(callEmbed.roomId)}
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',
}}
>
Return to call
</div>
</div>
{/* Corner resize handles (zIndex 2, above the drag overlay) */}
{(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => (
<div
key={corner}
style={cornerHandle(corner)}
onMouseDown={(e) => handleResizeMouseDown(e, corner)}
onClick={(e) => e.stopPropagation()}
>
{gripDots(corner).map((style, i) => (
<div key={i} style={style} />
))}
</div>
))}
</>
)}
</div>
</CallEmbedContextProvider> </CallEmbedContextProvider>
); );
} }
-1
View File
@@ -49,7 +49,6 @@ const NavItemBase = style({
display: 'flex', display: 'flex',
justifyContent: 'start', justifyContent: 'start',
cursor: 'pointer', cursor: 'pointer',
backgroundColor: Container,
color: OnContainer, color: OnContainer,
outline: 'none', outline: 'none',
minHeight: toRem(36), minHeight: toRem(36),
+3 -3
View File
@@ -14,7 +14,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
return ( return (
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}> <Box grow="Yes">
{nav} {nav}
{screenSize !== ScreenSize.Mobile && ( {screenSize !== ScreenSize.Mobile && (
<Line variant="Background" size="300" direction="Vertical" /> <Line variant="Background" size="300" direction="Vertical" />
@@ -79,11 +79,11 @@ export function PageNavContent({
); );
} }
export const Page = as<'div'>(({ className, ...props }, ref) => ( export const Page = as<'div', css.PageVariants>(({ className, transparent, ...props }, ref) => (
<Box <Box
grow="Yes" grow="Yes"
direction="Column" direction="Column"
className={classNames(ContainerColor({ variant: 'Surface' }), className)} className={classNames(css.Page({ transparent }), className)}
{...props} {...props}
ref={ref} ref={ref}
/> />
+14
View File
@@ -1,6 +1,7 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds'; import { DefaultReset, color, config, toRem } from 'folds';
import { ContainerColor } from '../../styles/ContainerColor.css';
export const PageNav = recipe({ export const PageNav = recipe({
variants: { variants: {
@@ -59,6 +60,19 @@ export const PageNavContent = style({
paddingBottom: config.space.S700, paddingBottom: config.space.S700,
}); });
export const Page = recipe({
base: [ContainerColor({ variant: 'Surface' })],
variants: {
transparent: {
true: {
background: 'transparent',
},
},
},
});
export type PageVariants = RecipeVariants<typeof Page>;
export const PageHeader = recipe({ export const PageHeader = recipe({
base: { base: {
paddingLeft: config.space.S400, paddingLeft: config.space.S400,
@@ -1,13 +1,11 @@
import { createVar, style } from '@vanilla-extract/css'; import { createVar, style } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { color, config, DefaultReset, Disabled, FocusOutline, toRem } from 'folds'; import { color, config, DefaultReset, Disabled, FocusOutline, toRem } from 'folds';
import { ContainerColor } from '../../styles/ContainerColor.css';
export const Sidebar = style([ export const Sidebar = style([
DefaultReset, DefaultReset,
{ {
width: toRem(66), width: toRem(66),
backgroundColor: color.Background.Container,
borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`, borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
display: 'flex', display: 'flex',
@@ -187,7 +185,6 @@ export type SidebarAvatarVariants = RecipeVariants<typeof SidebarAvatar>;
export const SidebarFolder = recipe({ export const SidebarFolder = recipe({
base: [ base: [
ContainerColor({ variant: 'Background' }),
{ {
padding: config.space.S100, padding: config.space.S100,
width: toRem(42), width: toRem(42),
@@ -3,7 +3,6 @@ import { color, config } from 'folds';
export const SplashScreen = style({ export const SplashScreen = style({
minHeight: '100%', minHeight: '100%',
backgroundColor: color.Background.Container,
color: color.Background.OnContainer, color: color.Background.OnContainer,
}); });
@@ -21,7 +21,7 @@ export function SplashScreen({ children }: SplashScreenProps) {
justifyContent="Center" justifyContent="Center"
> >
<Text size="H2" align="Center"> <Text size="H2" align="Center">
Lotus Chat Cinny
</Text> </Text>
</Box> </Box>
</Box> </Box>
@@ -157,6 +157,7 @@ export function CallControl({
}) { }) {
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control); const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
const setCallEmbed = useSetAtom(callEmbedAtom); const setCallEmbed = useSetAtom(callEmbedAtom);
const [hangupState, hangup] = useAsyncCallback( const [hangupState, hangup] = useAsyncCallback(
useCallback(() => callEmbed.hangup(), [callEmbed]) useCallback(() => callEmbed.hangup(), [callEmbed])
); );
@@ -36,6 +36,7 @@ export function CallRoomName({ room }: CallRoomNameProps) {
return ( return (
<Chip <Chip
variant="Background" variant="Background"
fill="None"
radii="Pill" radii="Pill"
before={ before={
<RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled /> <RoomIcon size="200" joinRule={room.getJoinRule()} roomType={room.getType()} filled />
+1 -3
View File
@@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import { Box, Spinner } from 'folds'; import { Box, Spinner } from 'folds';
import classNames from 'classnames';
import { LiveChip } from './LiveChip'; import { LiveChip } from './LiveChip';
import * as css from './styles.css'; import * as css from './styles.css';
import { CallRoomName } from './CallRoomName'; import { CallRoomName } from './CallRoomName';
import { CallControl } from './CallControl'; import { CallControl } from './CallControl';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { useCallMembers, useCallSession } from '../../hooks/useCall'; import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize';
import { MemberGlance } from './MemberGlance'; import { MemberGlance } from './MemberGlance';
@@ -33,7 +31,7 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
return ( return (
<Box <Box
className={classNames(css.CallStatus, ContainerColor({ variant: 'Background' }))} className={css.CallStatus}
shrink="No" shrink="No"
gap="400" gap="400"
alignItems={compact ? undefined : 'Center'} alignItems={compact ? undefined : 'Center'}
+2 -136
View File
@@ -1,8 +1,7 @@
import React, { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react'; import React, { MouseEventHandler, useCallback, useRef, useState } from 'react';
import { import {
Box, Box,
Button, Button,
Chip,
config, config,
Icon, Icon,
IconButton, IconButton,
@@ -27,8 +26,6 @@ import {
VideoButton, VideoButton,
} from './Controls'; } from './Controls';
import { CallEmbed, useCallControlState } from '../../plugins/call'; import { CallEmbed, useCallControlState } from '../../plugins/call';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useResizeObserver } from '../../hooks/useResizeObserver'; import { useResizeObserver } from '../../hooks/useResizeObserver';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
@@ -54,25 +51,6 @@ export function CallControls({ callEmbed }: CallControlsProps) {
); );
const [cords, setCords] = useState<RectCords>(); const [cords, setCords] = useState<RectCords>();
const [shareConfirm, setShareConfirm] = useState(false);
const [pttMode] = useSetting(settingsAtom, 'pttMode');
const [pttKey] = useSetting(settingsAtom, 'pttKey');
const [pttActive, setPttActive] = useState(false);
// 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]);
// Handle PTT mode toggle mid-call
const pttModeRef = useRef(pttMode);
useEffect(() => {
if (pttMode && !pttModeRef.current) {
callEmbed.control.setMicrophone(false);
} else if (!pttMode && pttModeRef.current) {
callEmbed.control.setMicrophone(true);
}
pttModeRef.current = pttMode;
}, [pttMode, callEmbed]);
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => { const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect()); setCords(evt.currentTarget.getBoundingClientRect());
@@ -93,64 +71,12 @@ export function CallControls({ callEmbed }: CallControlsProps) {
setCords(undefined); setCords(undefined);
}; };
useEffect(() => {
if (!pttMode) return;
const iframeWindow = callEmbed.iframe.contentWindow;
const onKeyDown = (e: KeyboardEvent) => {
if (e.code !== pttKey || e.repeat) return;
// Don't intercept keys typed into a text input or editable element
const target = e.target as HTMLElement;
if (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.contentEditable === 'true'
) return;
e.preventDefault();
if (!microphoneRef.current) callEmbed.control.setMicrophone(true);
setPttActive(true);
};
const onKeyUp = (e: KeyboardEvent) => {
if (e.code !== pttKey) return;
callEmbed.control.setMicrophone(false);
setPttActive(false);
};
// Release PTT when the tab loses focus to prevent stuck-on mic
const onBlur = () => {
callEmbed.control.setMicrophone(false);
setPttActive(false);
};
// Re-mute on focus restore: EC can re-assert audio_enabled:true on audio-context resume
const onFocus = () => {
callEmbed.control.setMicrophone(false);
setPttActive(false);
};
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
iframeWindow?.addEventListener('keydown', onKeyDown);
iframeWindow?.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('blur', onBlur);
window.removeEventListener('focus', onFocus);
iframeWindow?.removeEventListener('keydown', onKeyDown);
iframeWindow?.removeEventListener('keyup', onKeyUp);
};
// 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( const [hangupState, hangup] = useAsyncCallback(
useCallback(() => callEmbed.hangup(), [callEmbed]) useCallback(() => callEmbed.hangup(), [callEmbed])
); );
const exiting = const exiting =
hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success; hangupState.status === AsyncStatus.Loading || hangupState.status === AsyncStatus.Success;
const pttKeyLabel = pttKey === 'Space' ? 'SPACE' : pttKey.replace('Key', '').replace('Digit', '');
return ( return (
<Box <Box
ref={controlRef} ref={controlRef}
@@ -158,63 +84,6 @@ export function CallControls({ callEmbed }: CallControlsProps) {
justifyContent="Center" justifyContent="Center"
alignItems="Center" alignItems="Center"
> >
{pttMode && (
<Chip
variant={pttActive ? 'Success' : 'Warning'}
fill="Soft"
radii="400"
style={{
position: 'absolute',
top: '-2.2rem',
left: '50%',
transform: 'translateX(-50%)',
pointerEvents: 'none',
whiteSpace: 'nowrap',
}}
outlined
>
<Text size="T200" style={{ fontWeight: 700 }}>
{pttActive ? '● Live' : `PTT — Hold ${pttKeyLabel}`}
</Text>
</Chip>
)}
{shareConfirm && (
<Box
style={{
position: 'absolute',
bottom: '110%',
left: '50%',
transform: 'translateX(-50%)',
background: 'var(--bg-surface)',
border: '1px solid var(--border-color)',
borderRadius: '0.75rem',
padding: '1rem 1.25rem',
zIndex: 100,
minWidth: '260px',
boxShadow: '0 8px 32px rgba(0,0,0,0.35)',
display: 'flex',
flexDirection: 'column',
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>
<Box gap="200">
<Button
size="300" variant="Success" fill="Solid" radii="300"
onClick={() => { callEmbed.control.toggleScreenshare(); setShareConfirm(false); }}
>
<Text size="B300">Share</Text>
</Button>
<Button
size="300" variant="Surface" fill="Soft" radii="300" outlined
onClick={() => setShareConfirm(false)}
>
<Text size="B300">Cancel</Text>
</Button>
</Box>
</Box>
)}
<SequenceCard <SequenceCard
className={css.ControlCard} className={css.ControlCard}
variant="SurfaceVariant" variant="SurfaceVariant"
@@ -236,10 +105,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} /> <VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
<ScreenShareButton <ScreenShareButton
enabled={screenshare} enabled={screenshare}
onToggle={() => screenshare onToggle={() => callEmbed.control.toggleScreenshare()}
? callEmbed.control.toggleScreenshare()
: setShareConfirm(true)
}
/> />
</Box> </Box>
</Box> </Box>
+1 -6
View File
@@ -1,7 +1,6 @@
import React, { RefObject, useRef } from 'react'; import React, { RefObject, useRef } from 'react';
import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds'; import { Badge, Box, color, Header, Scroll, Text, toRem } from 'folds';
import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed'; import { useCallEmbed, useCallJoined, useCallEmbedPlacementSync } from '../../hooks/useCallEmbed';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { PrescreenControls } from './PrescreenControls'; import { PrescreenControls } from './PrescreenControls';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom'; import { useRoom } from '../../hooks/useRoom';
@@ -138,11 +137,7 @@ export function CallView() {
const currentJoined = callEmbed?.roomId === room.roomId && callJoined; const currentJoined = callEmbed?.roomId === room.roomId && callJoined;
return ( return (
<Box <Box style={{ minWidth: toRem(280) }} grow="Yes">
className={ContainerColor({ variant: 'Surface' })}
style={{ minWidth: toRem(280) }}
grow="Yes"
>
{!currentJoined && <CallPrescreen />} {!currentJoined && <CallPrescreen />}
<CallJoined joined={currentJoined} containerRef={callContainerRef} /> <CallJoined joined={currentJoined} containerRef={callContainerRef} />
</Box> </Box>
+1 -1
View File
@@ -53,7 +53,7 @@ export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
delay={500} delay={500}
tooltip={ tooltip={
<Tooltip> <Tooltip>
<Text size="T200">{enabled ? 'Deafen' : 'Undeafen'}</Text> <Text size="T200">{enabled ? 'Turn Off Sound' : 'Turn On Sound'}</Text>
</Tooltip> </Tooltip>
} }
> >
+3 -33
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds'; import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
import { SequenceCard } from '../../components/sequence-card'; import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css'; import * as css from './styles.css';
@@ -7,28 +7,6 @@ import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed'; import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
import { useCallPreferences } from '../../state/hooks/callPreferences'; import { useCallPreferences } from '../../state/hooks/callPreferences';
type MediaPermState = 'granted' | 'denied' | 'prompt' | 'unknown';
function useMediaPermissions(): MediaPermState {
const [state, setState] = useState<MediaPermState>('unknown');
useEffect(() => {
if (!navigator.permissions) {
setState('unknown');
return;
}
navigator.permissions
.query({ name: 'microphone' as PermissionName })
.then((result) => {
setState(result.state as MediaPermState);
result.onchange = () => setState(result.state as MediaPermState);
})
.catch(() => setState('unknown'));
}, []);
return state;
}
type PrescreenControlsProps = { type PrescreenControlsProps = {
canJoin?: boolean; canJoin?: boolean;
}; };
@@ -43,10 +21,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
const startCall = useCallStart(direct); const startCall = useCallStart(direct);
const joining = callEmbed?.roomId === room.roomId && !callJoined; const joining = callEmbed?.roomId === room.roomId && !callJoined;
const micPermission = useMediaPermissions(); const disabled = inOtherCall || !canJoin;
const micDenied = micPermission === 'denied';
const disabled = inOtherCall || !canJoin || micDenied;
const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } = const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } =
useCallPreferences(); useCallPreferences();
@@ -70,12 +45,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
<VideoButton enabled={video} onToggle={toggleVideo} /> <VideoButton enabled={video} onToggle={toggleVideo} />
<ChatButton /> <ChatButton />
</Box> </Box>
<Box grow="Yes" direction="Column" gap="200"> <Box grow="Yes" direction="Column">
{micDenied && (
<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>
)}
<Button <Button
variant={disabled ? 'Secondary' : 'Success'} variant={disabled ? 'Secondary' : 'Success'}
fill={disabled ? 'Soft' : 'Solid'} fill={disabled ? 'Soft' : 'Solid'}
-1
View File
@@ -21,7 +21,6 @@ export const CallMemberCard = style({
export const CallControlContainer = style({ export const CallControlContainer = style({
padding: config.space.S400, padding: config.space.S400,
position: 'relative',
}); });
export const PrescreenMessage = style({ export const PrescreenMessage = style({
@@ -213,6 +213,7 @@ export function CreateRoomForm({
<Text size="L400">Options</Text> <Text size="L400">Options</Text>
<Box grow="Yes" justifyContent="End"> <Box grow="Yes" justifyContent="End">
<Chip <Chip
fill="None"
radii="Pill" radii="Pill"
before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />} before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />}
onClick={() => setAdvance(!advance)} onClick={() => setAdvance(!advance)}
@@ -180,6 +180,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
<Box grow="Yes" justifyContent="End"> <Box grow="Yes" justifyContent="End">
<Chip <Chip
radii="Pill" radii="Pill"
fill="None"
before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />} before={<Icon src={advance ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />}
onClick={() => setAdvance(!advance)} onClick={() => setAdvance(!advance)}
type="button" type="button"
@@ -10,6 +10,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList'; import { allRoomsAtom } from '../../state/room-list/roomList';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler'; import { BackRouteHandler } from '../../components/BackRouteHandler';
import { useTheme } from '../../hooks/useTheme';
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] }; type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
export function JoinBeforeNavigate({ export function JoinBeforeNavigate({
@@ -21,6 +22,7 @@ export function JoinBeforeNavigate({
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { navigateRoom, navigateSpace } = useRoomNavigate();
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const theme = useTheme();
const handleView = (roomId: string) => { const handleView = (roomId: string) => {
if (mx.getRoom(roomId)?.isSpaceRoom()) { if (mx.getRoom(roomId)?.isSpaceRoom()) {
@@ -31,7 +33,7 @@ export function JoinBeforeNavigate({
}; };
return ( return (
<Page> <Page transparent={theme.flat}>
<PageHeader balance> <PageHeader balance>
<Box grow="Yes" gap="200"> <Box grow="Yes" gap="200">
<Box shrink="No"> <Box shrink="No">
+3 -1
View File
@@ -56,6 +56,7 @@ import { useGetRoom } from '../../hooks/useGetRoom';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions'; import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators'; import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
import { useTheme } from '../../hooks/useTheme';
const useCanDropLobbyItem = ( const useCanDropLobbyItem = (
space: Room, space: Room,
@@ -151,6 +152,7 @@ const useCanDropLobbyItem = (
export function Lobby() { export function Lobby() {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
@@ -430,7 +432,7 @@ export function Lobby() {
return ( return (
<PowerLevelsContextProvider value={spacePowerLevels}> <PowerLevelsContextProvider value={spacePowerLevels}>
<Box grow="Yes"> <Box grow="Yes">
<Page> <Page transparent={theme.flat}>
<LobbyHeader <LobbyHeader
showProfile={!onTop} showProfile={!onTop}
powerLevels={roomsPowerLevels.get(space.roomId) ?? {}} powerLevels={roomsPowerLevels.get(space.roomId) ?? {}}
+4
View File
@@ -64,6 +64,7 @@ function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfil
as="span" as="span"
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
before={ before={
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
@@ -121,6 +122,7 @@ function UnjoinedSpaceProfile({
<Chip <Chip
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
onClick={join} onClick={join}
disabled={!canJoin} disabled={!canJoin}
@@ -187,6 +189,7 @@ function SpaceProfile({
onClick={handleClose} onClick={handleClose}
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
before={ before={
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
@@ -230,6 +233,7 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
onClick={handleClose} onClick={handleClose}
className={css.HeaderChip} className={css.HeaderChip}
variant="Surface" variant="Surface"
fill="None"
size="500" size="500"
after={<Icon src={closed ? Icons.ChevronRight : Icons.ChevronBottom} size="50" />} after={<Icon src={closed ? Icons.ChevronRight : Icons.ChevronBottom} size="50" />}
> >
-278
View File
@@ -1,278 +0,0 @@
import { CSSProperties } from 'react';
import { ChatBackground } from '../../state/settings';
export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
{ value: 'none', label: 'None' },
{ value: 'blueprint', label: 'Blueprint' },
{ value: 'carbon', label: 'Carbon' },
{ value: 'stars', label: 'Stars' },
{ value: 'topographic', label: 'Topographic' },
{ value: 'herringbone', label: 'Herringbone' },
{ value: 'crosshatch', label: 'Crosshatch' },
{ value: 'chevron', label: 'Chevron' },
{ value: 'polka', label: 'Polka' },
{ value: 'triangles', label: 'Triangles' },
{ value: 'plaid', label: 'Plaid' },
{ value: 'tactical', label: 'Tactical' },
{ value: 'circuit', label: 'Circuit' },
{ value: 'hexgrid', label: 'Hex Grid' },
];
const DARK: Record<ChatBackground, CSSProperties> = {
none: {},
blueprint: {
backgroundColor: '#0a1628',
backgroundImage: [
'linear-gradient(rgba(100,149,237,0.14) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(100,149,237,0.14) 1px, transparent 1px)',
'linear-gradient(rgba(100,149,237,0.05) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(100,149,237,0.05) 1px, transparent 1px)',
].join(','),
backgroundSize: '80px 80px, 80px 80px, 16px 16px, 16px 16px',
},
carbon: {
backgroundColor: '#0e0e0e',
backgroundImage: [
'repeating-linear-gradient(45deg, rgba(255,255,255,0.035) 0, rgba(255,255,255,0.035) 2px, transparent 0, transparent 50%)',
'repeating-linear-gradient(135deg, rgba(255,255,255,0.035) 0, rgba(255,255,255,0.035) 2px, transparent 0, transparent 50%)',
].join(','),
backgroundSize: '8px 8px',
},
stars: {
backgroundColor: '#050510',
backgroundImage: [
'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
'radial-gradient(circle, rgba(255,255,255,0.55) 1px, transparent 1px)',
'radial-gradient(circle, rgba(200,200,255,0.3) 1px, transparent 1px)',
].join(','),
backgroundSize: '130px 130px, 190px 190px, 260px 260px',
backgroundPosition: '0 0, 65px 32px, 32px 97px',
},
topographic: {
backgroundColor: '#0f0f17',
backgroundImage: [
'repeating-radial-gradient(circle at 20% 20%, transparent 0, transparent 30px, rgba(152,0,0,0.07) 31px, transparent 32px)',
'repeating-radial-gradient(circle at 80% 80%, transparent 0, transparent 25px, rgba(100,100,200,0.06) 26px, transparent 27px)',
'repeating-radial-gradient(circle at 50% 10%, transparent 0, transparent 45px, rgba(152,0,0,0.04) 46px, transparent 47px)',
].join(','),
},
herringbone: {
backgroundColor: '#111118',
backgroundImage: [
'repeating-linear-gradient(60deg, rgba(180,160,210,0.08) 0, rgba(180,160,210,0.08) 1px, transparent 0, transparent 50%)',
'repeating-linear-gradient(120deg, rgba(180,160,210,0.08) 0, rgba(180,160,210,0.08) 1px, transparent 0, transparent 50%)',
].join(','),
backgroundSize: '20px 36px',
},
crosshatch: {
backgroundColor: '#0f0f0f',
backgroundImage: [
'linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px)',
'linear-gradient(rgba(255,255,255,0.022) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(255,255,255,0.022) 1px, transparent 1px)',
].join(','),
backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
},
// Interlocking zigzag stripes
chevron: {
backgroundColor: '#0f0f17',
backgroundImage: [
'linear-gradient(135deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
'linear-gradient(225deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
'linear-gradient(315deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
'linear-gradient(45deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
].join(','),
backgroundSize: '20px 20px',
},
// Even dot grid
polka: {
backgroundColor: '#0e0e14',
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.2) 2px, transparent 2px)',
backgroundSize: '28px 28px',
},
// Isometric triangle grid
triangles: {
backgroundColor: '#111118',
backgroundImage: [
'linear-gradient(60deg, rgba(100,149,237,0.09) 25%, transparent 25%, transparent 75%, rgba(100,149,237,0.09) 75%)',
'linear-gradient(120deg, rgba(100,149,237,0.09) 25%, transparent 25%, transparent 75%, rgba(100,149,237,0.09) 75%)',
].join(','),
backgroundSize: '40px 70px',
backgroundPosition: '0 0, 20px 35px',
},
// Tartan-inspired crossing lines with accent colour
plaid: {
backgroundColor: '#0a1020',
backgroundImage: [
'repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(100,149,237,0.13) 39px, rgba(100,149,237,0.13) 40px)',
'repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(100,149,237,0.13) 39px, rgba(100,149,237,0.13) 40px)',
'repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(152,0,0,0.08) 7px, rgba(152,0,0,0.08) 8px)',
'repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(152,0,0,0.08) 7px, rgba(152,0,0,0.08) 8px)',
].join(','),
},
// LotusGuild TDS exact dot-grid
tactical: {
backgroundColor: '#030508',
backgroundImage: 'radial-gradient(circle, rgba(0,212,255,0.055) 1px, transparent 1px)',
backgroundSize: '28px 28px',
},
// Circuit board — green grid with node dots
circuit: {
backgroundColor: '#040a04',
backgroundImage: [
'linear-gradient(rgba(0,255,136,0.045) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(0,255,136,0.045) 1px, transparent 1px)',
'radial-gradient(circle, rgba(0,255,136,0.20) 1.5px, transparent 1.5px)',
].join(','),
backgroundSize: '40px 40px, 40px 40px, 40px 40px',
backgroundPosition: '0 0, 0 0, 20px 20px',
},
// 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")',
backgroundSize: '29px 50px',
},
};
const LIGHT: Record<ChatBackground, CSSProperties> = {
none: {},
blueprint: {
backgroundColor: '#eef3ff',
backgroundImage: [
'linear-gradient(rgba(50,100,220,0.16) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(50,100,220,0.16) 1px, transparent 1px)',
'linear-gradient(rgba(50,100,220,0.06) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(50,100,220,0.06) 1px, transparent 1px)',
].join(','),
backgroundSize: '80px 80px, 80px 80px, 16px 16px, 16px 16px',
},
carbon: {
backgroundColor: '#efefef',
backgroundImage: [
'repeating-linear-gradient(45deg, rgba(0,0,0,0.04) 0, rgba(0,0,0,0.04) 2px, transparent 0, transparent 50%)',
'repeating-linear-gradient(135deg, rgba(0,0,0,0.04) 0, rgba(0,0,0,0.04) 2px, transparent 0, transparent 50%)',
].join(','),
backgroundSize: '8px 8px',
},
// Stars is intentionally always dark — it's a night-sky theme
stars: {
backgroundColor: '#050510',
backgroundImage: [
'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
'radial-gradient(circle, rgba(255,255,255,0.55) 1px, transparent 1px)',
'radial-gradient(circle, rgba(200,200,255,0.3) 1px, transparent 1px)',
].join(','),
backgroundSize: '130px 130px, 190px 190px, 260px 260px',
backgroundPosition: '0 0, 65px 32px, 32px 97px',
},
topographic: {
backgroundColor: '#faf8f5',
backgroundImage: [
'repeating-radial-gradient(circle at 20% 20%, transparent 0, transparent 30px, rgba(100,60,60,0.09) 31px, transparent 32px)',
'repeating-radial-gradient(circle at 80% 80%, transparent 0, transparent 25px, rgba(60,60,130,0.07) 26px, transparent 27px)',
'repeating-radial-gradient(circle at 50% 10%, transparent 0, transparent 45px, rgba(100,60,60,0.05) 46px, transparent 47px)',
].join(','),
},
herringbone: {
backgroundColor: '#f9f9f9',
backgroundImage: [
'repeating-linear-gradient(60deg, rgba(80,70,110,0.09) 0, rgba(80,70,110,0.09) 1px, transparent 0, transparent 50%)',
'repeating-linear-gradient(120deg, rgba(80,70,110,0.09) 0, rgba(80,70,110,0.09) 1px, transparent 0, transparent 50%)',
].join(','),
backgroundSize: '20px 36px',
},
crosshatch: {
backgroundColor: '#ffffff',
backgroundImage: [
'linear-gradient(rgba(0,0,0,0.07) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(0,0,0,0.07) 1px, transparent 1px)',
'linear-gradient(rgba(0,0,0,0.025) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(0,0,0,0.025) 1px, transparent 1px)',
].join(','),
backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
},
chevron: {
backgroundColor: '#f9f8ff',
backgroundImage: [
'linear-gradient(135deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
'linear-gradient(225deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
'linear-gradient(315deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
'linear-gradient(45deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
].join(','),
backgroundSize: '20px 20px',
},
polka: {
backgroundColor: '#fafafa',
backgroundImage: 'radial-gradient(circle, rgba(0,0,0,0.18) 2px, transparent 2px)',
backgroundSize: '28px 28px',
},
triangles: {
backgroundColor: '#f4f7ff',
backgroundImage: [
'linear-gradient(60deg, rgba(50,100,220,0.1) 25%, transparent 25%, transparent 75%, rgba(50,100,220,0.1) 75%)',
'linear-gradient(120deg, rgba(50,100,220,0.1) 25%, transparent 25%, transparent 75%, rgba(50,100,220,0.1) 75%)',
].join(','),
backgroundSize: '40px 70px',
backgroundPosition: '0 0, 20px 35px',
},
plaid: {
backgroundColor: '#f5f0ff',
backgroundImage: [
'repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(100,50,180,0.15) 39px, rgba(100,50,180,0.15) 40px)',
'repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(100,50,180,0.15) 39px, rgba(100,50,180,0.15) 40px)',
'repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(200,0,0,0.09) 7px, rgba(200,0,0,0.09) 8px)',
'repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(200,0,0,0.09) 7px, rgba(200,0,0,0.09) 8px)',
].join(','),
},
tactical: {
backgroundColor: '#f0f4fa',
backgroundImage: 'radial-gradient(circle, rgba(0,100,200,0.08) 1px, transparent 1px)',
backgroundSize: '28px 28px',
},
circuit: {
backgroundColor: '#f0f8f0',
backgroundImage: [
'linear-gradient(rgba(0,160,80,0.06) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(0,160,80,0.06) 1px, transparent 1px)',
'radial-gradient(circle, rgba(0,160,80,0.22) 1.5px, transparent 1.5px)',
].join(','),
backgroundSize: '40px 40px, 40px 40px, 40px 40px',
backgroundPosition: '0 0, 0 0, 20px 20px',
},
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")',
backgroundSize: '29px 50px',
},
};
export const getChatBg = (bg: ChatBackground, isDark: boolean): CSSProperties =>
isDark ? DARK[bg] : LIGHT[bg];
@@ -9,6 +9,7 @@ export const RoomNavCategoryButton = as<'button', { closed?: boolean }>(
className={classNames(css.CategoryButton, className)} className={classNames(css.CategoryButton, className)}
variant="Background" variant="Background"
radii="Pill" radii="Pill"
fill="None"
before={ before={
<Icon <Icon
className={css.CategoryButtonIcon} className={css.CategoryButtonIcon}
+4 -1
View File
@@ -6,9 +6,11 @@ import { Page, PageHeader } from '../../components/page';
import { callChatAtom } from '../../state/callEmbed'; import { callChatAtom } from '../../state/callEmbed';
import { RoomView } from './RoomView'; import { RoomView } from './RoomView';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { useTheme } from '../../hooks/useTheme';
export function CallChatView() { export function CallChatView() {
const { eventId } = useParams(); const { eventId } = useParams();
const theme = useTheme();
const setChat = useSetAtom(callChatAtom); const setChat = useSetAtom(callChatAtom);
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
@@ -16,6 +18,7 @@ export function CallChatView() {
return ( return (
<Page <Page
transparent={theme.flat}
style={{ style={{
width: screenSize === ScreenSize.Desktop ? toRem(456) : '100%', width: screenSize === ScreenSize.Desktop ? toRem(456) : '100%',
flexShrink: 0, flexShrink: 0,
@@ -41,7 +44,7 @@ export function CallChatView() {
} }
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton ref={triggerRef} variant="Surface" onClick={handleClose}> <IconButton ref={triggerRef} variant="Surface" fill="None" onClick={handleClose}>
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
)} )}
+5 -6
View File
@@ -56,7 +56,6 @@ import { MembershipFilterMenu } from '../../components/MembershipFilterMenu';
import { MemberSortMenu } from '../../components/MemberSortMenu'; import { MemberSortMenu } from '../../components/MemberSortMenu';
import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile'; import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../hooks/useSpace'; import { useSpaceOptionally } from '../../hooks/useSpace';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag'; import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomCreators } from '../../hooks/useRoomCreators';
@@ -89,6 +88,7 @@ function MemberDrawerHeader({ room }: MemberDrawerHeaderProps) {
<IconButton <IconButton
ref={triggerRef} ref={triggerRef}
variant="Background" variant="Background"
fill="None"
onClick={() => setPeopleDrawer(false)} onClick={() => setPeopleDrawer(false)}
> >
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
@@ -132,6 +132,7 @@ function MemberItem({
aria-pressed={pressed} aria-pressed={pressed}
data-user-id={member.userId} data-user-id={member.userId}
variant="Background" variant="Background"
fill="None"
radii="400" radii="400"
onClick={onClick} onClick={onClick}
before={ before={
@@ -245,11 +246,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
}; };
return ( return (
<Box <Box className={classNames(css.MembersDrawer)} shrink="No" direction="Column">
className={classNames(css.MembersDrawer, ContainerColor({ variant: 'Background' }))}
shrink="No"
direction="Column"
>
<MemberDrawerHeader room={room} /> <MemberDrawerHeader room={room} />
<Box className={css.MemberDrawerContentBase} grow="Yes"> <Box className={css.MemberDrawerContentBase} grow="Yes">
<Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover" hideTrack> <Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover" hideTrack>
@@ -279,6 +276,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
)) as MouseEventHandler<HTMLButtonElement> )) as MouseEventHandler<HTMLButtonElement>
} }
variant="Background" variant="Background"
fill="None"
size="400" size="400"
radii="300" radii="300"
before={<Icon src={Icons.Filter} size="50" />} before={<Icon src={Icons.Filter} size="50" />}
@@ -311,6 +309,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
)) as MouseEventHandler<HTMLButtonElement> )) as MouseEventHandler<HTMLButtonElement>
} }
variant="Background" variant="Background"
fill="None"
size="400" size="400"
radii="300" radii="300"
after={<Icon src={Icons.Sort} size="50" />} after={<Icon src={Icons.Sort} size="50" />}
+7 -4
View File
@@ -18,11 +18,14 @@ import { CallView } from '../call/CallView';
import { RoomViewHeader } from './RoomViewHeader'; import { RoomViewHeader } from './RoomViewHeader';
import { callChatAtom } from '../../state/callEmbed'; import { callChatAtom } from '../../state/callEmbed';
import { CallChatView } from './CallChatView'; import { CallChatView } from './CallChatView';
import { Page } from '../../components/page';
import { useTheme } from '../../hooks/useTheme';
export function Room() { export function Room() {
const { eventId } = useParams(); const { eventId } = useParams();
const room = useRoom(); const room = useRoom();
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
@@ -49,20 +52,20 @@ export function Room() {
<PowerLevelsContextProvider value={powerLevels}> <PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes"> <Box grow="Yes">
{callView && (screenSize === ScreenSize.Desktop || !chat) && ( {callView && (screenSize === ScreenSize.Desktop || !chat) && (
<Box grow="Yes" direction="Column"> <Page transparent={theme.flat}>
<RoomViewHeader callView /> <RoomViewHeader callView />
<Box grow="Yes"> <Box grow="Yes">
<CallView /> <CallView />
</Box> </Box>
</Box> </Page>
)} )}
{!callView && ( {!callView && (
<Box grow="Yes" direction="Column"> <Page transparent={theme.flat}>
<RoomViewHeader /> <RoomViewHeader />
<Box grow="Yes"> <Box grow="Yes">
<RoomView eventId={eventId} /> <RoomView eventId={eventId} />
</Box> </Box>
</Box> </Page>
)} )}
{callView && chat && ( {callView && chat && (
-2
View File
@@ -436,7 +436,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [messageLayout] = useSetting(settingsAtom, 'messageLayout'); const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
const [perMessageProfiles] = useSetting(settingsAtom, 'perMessageProfiles');
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing'); const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const direct = useIsDirectRoom(); const direct = useIsDirectRoom();
@@ -1650,7 +1649,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
} }
const collapsed = const collapsed =
!perMessageProfiles &&
isPrevRendered && isPrevRendered &&
!dayDivider && !dayDivider &&
(!newDivider || eventSender === mx.getUserId()) && (!newDivider || eventSender === mx.getUserId()) &&
+2 -12
View File
@@ -14,11 +14,6 @@ import { RoomViewTyping } from './RoomViewTyping';
import { RoomTombstone } from './RoomTombstone'; import { RoomTombstone } from './RoomTombstone';
import { RoomInput } from './RoomInput'; import { RoomInput } from './RoomInput';
import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing'; import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollowing';
import { Page } from '../../components/page';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useTheme, ThemeKind } from '../../hooks/useTheme';
import { getChatBg } from '../lotus/chatBackground';
import { useKeyDown } from '../../hooks/useKeyDown'; import { useKeyDown } from '../../hooks/useKeyDown';
import { editableActiveElement } from '../../utils/dom'; import { editableActiveElement } from '../../utils/dom';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
@@ -58,14 +53,9 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
return true; return true;
}; };
export function RoomView({ eventId }: { eventId?: string }) { export function RoomView({ eventId }: { eventId?: string }) {
const roomInputRef = useRef<HTMLDivElement>(null); const roomInputRef = useRef<HTMLDivElement>(null);
const roomViewRef = useRef<HTMLDivElement>(null); const roomViewRef = useRef<HTMLDivElement>(null);
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
const theme = useTheme();
const isDark = theme.kind === ThemeKind.Dark;
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
@@ -100,7 +90,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
); );
return ( return (
<Page ref={roomViewRef} style={getChatBg(chatBackground, isDark)}> <Box grow="Yes" direction="Column" ref={roomViewRef}>
<Box grow="Yes" direction="Column"> <Box grow="Yes" direction="Column">
<RoomTimeline <RoomTimeline
key={roomId} key={roomId}
@@ -144,6 +134,6 @@ export function RoomView({ eventId }: { eventId?: string }) {
</div> </div>
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />} {hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />}
</Box> </Box>
</Page> </Box>
); );
} }
@@ -16,8 +16,6 @@ export const RoomViewFollowing = recipe({
minHeight: toRem(28), minHeight: toRem(28),
padding: `0 ${config.space.S400}`, padding: `0 ${config.space.S400}`,
width: '100%', width: '100%',
backgroundColor: color.Surface.Container,
color: color.Surface.OnContainer,
outline: 'none', outline: 'none',
}, },
], ],
+1 -24
View File
@@ -32,7 +32,6 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer';
import { StateEvent } from '../../../types/matrix/room'; import { StateEvent } from '../../../types/matrix/room';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom'; import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
import { useCallStart } from '../../hooks/useCallEmbed';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace'; import { useSpaceOptionally } from '../../hooks/useSpace';
@@ -67,7 +66,6 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { RoomSettingsPage } from '../../state/roomSettings'; import { RoomSettingsPage } from '../../state/roomSettings';
type RoomMenuProps = { type RoomMenuProps = {
@@ -264,7 +262,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>(); const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
const direct = useIsDirectRoom(); const direct = useIsDirectRoom();
const startCall = useCallStart(true);
const pinnedEvents = useRoomPinnedEvents(room); const pinnedEvents = useRoomPinnedEvents(room);
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption); const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
@@ -307,10 +304,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
}; };
return ( return (
<PageHeader <PageHeader balance={screenSize === ScreenSize.Mobile}>
className={ContainerColor({ variant: 'Surface' })}
balance={screenSize === ScreenSize.Mobile}
>
<Box grow="Yes" gap="300"> <Box grow="Yes" gap="300">
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<BackRouteHandler> <BackRouteHandler>
@@ -381,23 +375,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
{direct && !callView && (
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>Start Call</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton fill="None" ref={triggerRef} onClick={() => startCall(room)}>
<Icon size="400" src={Icons.Phone} />
</IconButton>
)}
</TooltipProvider>
)}
{!encryptedRoom && ( {!encryptedRoom && (
<TooltipProvider <TooltipProvider
position="Bottom" position="Bottom"
+8 -9
View File
@@ -4,8 +4,7 @@ import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css'; import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import LotusLogo from '../../../../../public/res/Lotus.png'; import CinnySVG from '../../../../../public/res/svg/cinny.svg';
import pkg from '../../../../../package.json';
import { clearCacheAndReload } from '../../../../client/initMatrix'; import { clearCacheAndReload } from '../../../../client/initMatrix';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
@@ -39,23 +38,23 @@ export function About({ requestClose }: AboutProps) {
<Box shrink="No"> <Box shrink="No">
<img <img
style={{ width: toRem(60), height: toRem(60) }} style={{ width: toRem(60), height: toRem(60) }}
src={LotusLogo} src={CinnySVG}
alt="Lotus Chat logo" alt="Cinny logo"
/> />
</Box> </Box>
<Box direction="Column" gap="300"> <Box direction="Column" gap="300">
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Box gap="100" alignItems="End"> <Box gap="100" alignItems="End">
<Text size="H3">Lotus Chat</Text> <Text size="H3">Cinny</Text>
<Text size="T200">v{pkg.version}</Text> <Text size="T200">v4.11.0</Text>
</Box> </Box>
<Text>A Matrix client for Lotus Guild.</Text> <Text>Yet another matrix client.</Text>
</Box> </Box>
<Box gap="200" wrap="Wrap"> <Box gap="200" wrap="Wrap">
<Button <Button
as="a" as="a"
href="https://code.lotusguild.org/LotusGuild/cinny" href="https://github.com/cinnyapp/cinny"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
variant="Secondary" variant="Secondary"
@@ -68,7 +67,7 @@ export function About({ requestClose }: AboutProps) {
</Button> </Button>
<Button <Button
as="a" as="a"
href="https://matrix.lotusguild.org" href="https://cinny.in/#sponsor"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
variant="Critical" variant="Critical"
@@ -28,7 +28,7 @@ function ExportKeys() {
const blob = new Blob([encKeys], { const blob = new Blob([encKeys], {
type: 'text/plain;charset=us-ascii', type: 'text/plain;charset=us-ascii',
}); });
FileSaver.saveAs(blob, 'lotus-keys.txt'); FileSaver.saveAs(blob, 'cinny-keys.txt');
}, },
[mx] [mx]
) )
+1 -180
View File
@@ -32,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
import { Page, PageContent, PageHeader } from '../../../components/page'; import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { useSetting } from '../../../state/hooks/settings'; import { useSetting } from '../../../state/hooks/settings';
import { ChatBackground, DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings'; import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import { KeySymbol } from '../../../utils/key-symbol'; import { KeySymbol } from '../../../utils/key-symbol';
import { isMacOS } from '../../../utils/user-agent'; import { isMacOS } from '../../../utils/user-agent';
@@ -42,13 +42,10 @@ import {
Theme, Theme,
ThemeKind, ThemeKind,
useSystemThemeKind, useSystemThemeKind,
useTheme,
useThemeNames, useThemeNames,
useThemes, useThemes,
} from '../../../hooks/useTheme'; } from '../../../hooks/useTheme';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { BG_OPTIONS, getChatBg } from '../../lotus/chatBackground';
import { resetBootSequence, runLotusBootSequence } from '../../../../lotus-boot';
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout'; import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing'; import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
import { useDateFormatItems } from '../../../hooks/useDateFormat'; import { useDateFormatItems } from '../../../hooks/useDateFormat';
@@ -310,8 +307,6 @@ function Appearance() {
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme'); const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode'); const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
const [perMessageProfiles, setPerMessageProfiles] = useSetting(settingsAtom, 'perMessageProfiles');
const [lotusTerminal, setLotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
return ( return (
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
@@ -355,56 +350,6 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Page Zoom" after={<PageZoomInput />} /> <SettingTile title="Page Zoom" after={<PageZoomInput />} />
</SequenceCard> </SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="200">
<SettingTile
title="Chat Background"
description="Pattern applied behind the message timeline."
/>
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
<ChatBgGrid />
</Box>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<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} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Lotus Terminal Mode"
description="LotusGuild Terminal Design System: Anduril Orange + Ice Cyan + Matrix Green, dot-grid background, CRT scanlines, and boot sequence animation."
after={
<Box direction="Row" gap="200" align="Center">
{lotusTerminal && (
<button
type="button"
onClick={() => { resetBootSequence(); runLotusBootSequence(true); }}
title="Replay boot sequence"
style={{
background: 'transparent',
border: '1px solid rgba(255,107,0,0.35)',
color: '#FF6B00',
fontSize: '0.65rem',
padding: '0.2rem 0.6rem',
cursor: 'pointer',
fontFamily: 'inherit',
letterSpacing: '0.1em',
textTransform: 'uppercase',
whiteSpace: 'nowrap',
}}
>
Boot
</button>
)}
<Switch variant="Primary" value={lotusTerminal} onChange={setLotusTerminal} />
</Box>
}
/>
</SequenceCard>
</Box> </Box>
); );
} }
@@ -766,7 +711,6 @@ function Editor() {
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
const [editorToolbar, setEditorToolbar] = useSetting(settingsAtom, 'editorToolbar');
return ( return (
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
@@ -786,13 +730,6 @@ function Editor() {
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />} after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
/> />
</SequenceCard> </SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Formatting Toolbar"
description="Show bold, italic, code and other formatting buttons above the message input."
after={<Switch variant="Primary" value={editorToolbar} onChange={setEditorToolbar} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile <SettingTile
title="Hide Typing & Read Receipts" title="Hide Typing & Read Receipts"
@@ -804,121 +741,6 @@ function Editor() {
); );
} }
function Calls() {
const [cameraOnJoin, setCameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
const [callNoiseSuppression, setCallNoiseSuppression] = useSetting(settingsAtom, 'callNoiseSuppression');
const [pttMode, setPttMode] = useSetting(settingsAtom, 'pttMode');
const [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey');
const [listeningForKey, setListeningForKey] = useState(false);
const handleKeyBind = () => {
setListeningForKey(true);
const onKey = (e: KeyboardEvent) => {
e.preventDefault();
if (e.code === 'Escape') {
setListeningForKey(false);
} else {
setPttKey(e.code);
setListeningForKey(false);
}
window.removeEventListener('keydown', onKey, true);
};
window.addEventListener('keydown', onKey, true);
};
const keyLabel = (code: string) => code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', '');
return (
<Box direction="Column" gap="100">
<Text size="L400">Calls</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Join with Camera On"
description="Enable camera automatically when joining a voice or video call. Camera is off by default."
after={<Switch variant="Primary" value={cameraOnJoin} onChange={setCameraOnJoin} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<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} />}
/>
</SequenceCard>
<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."
after={<Switch variant="Primary" value={pttMode} onChange={setPttMode} />}
/>
{pttMode && (
<SettingTile
title="PTT Key Binding"
description="Press a key to bind it as your push-to-talk key."
after={
<Button
size="300"
variant={listeningForKey ? 'Warning' : 'Secondary'}
fill={listeningForKey ? 'Solid' : 'Soft'}
radii="300"
outlined
onClick={handleKeyBind}
style={{ minWidth: '90px' }}
>
<Text size="B300">
{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}
</Text>
</Button>
}
/>
)}
</SequenceCard>
</Box>
);
}
function ChatBgGrid() {
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
const theme = useTheme();
const isDark = theme.kind === ThemeKind.Dark;
return (
<Box wrap="Wrap" gap="200">
{BG_OPTIONS.map((opt) => (
<Box key={opt.value} direction="Column" gap="100" style={{ alignItems: 'center' }}>
<button
type="button"
aria-label={opt.label}
aria-pressed={chatBackground === opt.value}
onClick={() => setChatBackground(opt.value as ChatBackground)}
style={{
display: 'block',
width: toRem(76),
height: toRem(50),
borderRadius: toRem(8),
cursor: 'pointer',
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}
>
{opt.label}
</Text>
</Box>
))}
</Box>
);
}
function SelectMessageLayout() { function SelectMessageLayout() {
const [menuCords, setMenuCords] = useState<RectCords>(); const [menuCords, setMenuCords] = useState<RectCords>();
const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout'); const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout');
@@ -1184,7 +1006,6 @@ export function General({ requestClose }: GeneralProps) {
<DateAndTime /> <DateAndTime />
<Editor /> <Editor />
<Messages /> <Messages />
<Calls />
</Box> </Box>
</PageContent> </PageContent>
</Scroll> </Scroll>
@@ -27,7 +27,7 @@ function EmailNotification() {
device_display_name: email, device_display_name: email,
lang: 'en', lang: 'en',
data: { data: {
brand: 'Lotus Chat', brand: 'Cinny',
}, },
append: true, append: true,
}); });
+5 -13
View File
@@ -15,8 +15,6 @@ import { useResizeObserver } from './useResizeObserver';
import { CallControlState } from '../plugins/call/CallControlState'; import { CallControlState } from '../plugins/call/CallControlState';
import { useCallMembersChange, useCallSession } from './useCall'; import { useCallMembersChange, useCallSession } from './useCall';
import { CallPreferences } from '../state/callPreferences'; import { CallPreferences } from '../state/callPreferences';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
const CallEmbedContext = createContext<CallEmbed | undefined>(undefined); const CallEmbedContext = createContext<CallEmbed | undefined>(undefined);
@@ -44,19 +42,15 @@ export const createCallEmbed = (
dm: boolean, dm: boolean,
themeKind: ElementCallThemeKind, themeKind: ElementCallThemeKind,
container: HTMLElement, container: HTMLElement,
pref?: CallPreferences, pref?: CallPreferences
noiseSuppression = true,
forceAudioOff = false
): CallEmbed => { ): CallEmbed => {
const rtcSession = mx.matrixRTC.getRoomSession(room); const rtcSession = mx.matrixRTC.getRoomSession(room);
const ongoing = const ongoing =
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0; MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0;
const intent = CallEmbed.getIntent(dm, ongoing); const intent = CallEmbed.getIntent(dm, ongoing);
const initialAudio = forceAudioOff ? false : (pref?.microphone ?? true); const widget = CallEmbed.getWidget(mx, room, intent, themeKind);
const initialVideo = pref?.video ?? false; const controlState = pref && new CallControlState(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); const embed = new CallEmbed(mx, room, widget, container, controlState);
@@ -68,8 +62,6 @@ export const useCallStart = (dm = false) => {
const theme = useTheme(); const theme = useTheme();
const setCallEmbed = useSetAtom(callEmbedAtom); const setCallEmbed = useSetAtom(callEmbedAtom);
const callEmbedRef = useCallEmbedRef(); const callEmbedRef = useCallEmbedRef();
const [callNoiseSuppression] = useSetting(settingsAtom, 'callNoiseSuppression');
const [pttMode] = useSetting(settingsAtom, 'pttMode');
const startCall = useCallback( const startCall = useCallback(
(room: Room, pref?: CallPreferences) => { (room: Room, pref?: CallPreferences) => {
@@ -77,11 +69,11 @@ export const useCallStart = (dm = false) => {
if (!container) { if (!container) {
throw new Error('Failed to start call, No embed container element found!'); throw new Error('Failed to start call, No embed container element found!');
} }
const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref, callNoiseSuppression ?? true, !!pttMode); const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref);
setCallEmbed(callEmbed); setCallEmbed(callEmbed);
}, },
[mx, dm, theme, setCallEmbed, callEmbedRef, callNoiseSuppression, pttMode] [mx, dm, theme, setCallEmbed, callEmbedRef]
); );
return startCall; return startCall;
+16 -14
View File
@@ -1,7 +1,6 @@
import { lightTheme } from 'folds';
import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { onDarkFontWeight, onLightFontWeight } from '../../config.css'; import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
import { butterTheme, darkTheme, lotusTerminalLightTheme, lotusTerminalTheme, silverTheme } from '../../colors.css'; import { lightTheme, butterTheme, darkTheme, silverTheme, moonlightTheme } from '../../colors.css';
import { settingsAtom } from '../state/settings'; import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings'; import { useSetting } from '../state/hooks/settings';
@@ -14,6 +13,8 @@ export type Theme = {
id: string; id: string;
kind: ThemeKind; kind: ThemeKind;
classNames: string[]; classNames: string[];
flat?: boolean;
gradient?: string;
}; };
export const LightTheme: Theme = { export const LightTheme: Theme = {
@@ -25,31 +26,31 @@ export const LightTheme: Theme = {
export const SilverTheme: Theme = { export const SilverTheme: Theme = {
id: 'silver-theme', id: 'silver-theme',
kind: ThemeKind.Light, kind: ThemeKind.Light,
classNames: ['silver-theme', silverTheme, onLightFontWeight, 'prism-light'], classNames: [silverTheme, onLightFontWeight, 'prism-light'],
}; };
export const DarkTheme: Theme = { export const DarkTheme: Theme = {
id: 'dark-theme', id: 'dark-theme',
kind: ThemeKind.Dark, kind: ThemeKind.Dark,
classNames: ['dark-theme', darkTheme, onDarkFontWeight, 'prism-dark'], classNames: ['global-dark', darkTheme, onDarkFontWeight, 'prism-dark'],
}; };
export const ButterTheme: Theme = { export const ButterTheme: Theme = {
id: 'butter-theme', id: 'butter-theme',
kind: ThemeKind.Dark, kind: ThemeKind.Dark,
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'], classNames: ['global-dark', butterTheme, onDarkFontWeight, 'prism-dark'],
}; };
export const LotusTerminalTheme: Theme = { export const MoonlightTheme: Theme = {
id: 'lotus-terminal-theme', id: 'moonlight-theme',
kind: ThemeKind.Dark, kind: ThemeKind.Dark,
classNames: ['lotus-terminal-theme', lotusTerminalTheme, onDarkFontWeight, 'prism-dark'], classNames: ['global-dark', moonlightTheme, onDarkFontWeight, 'prism-dark'],
}; flat: true,
export const LotusTerminalLightTheme: Theme = {
id: 'lotus-terminal-light-theme',
kind: ThemeKind.Light,
classNames: ['lotus-terminal-light-theme', lotusTerminalLightTheme, onLightFontWeight, 'prism-light'],
}; };
export const useThemes = (): Theme[] => { export const useThemes = (): Theme[] => {
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []); const themes: Theme[] = useMemo(
() => [LightTheme, SilverTheme, DarkTheme, ButterTheme, MoonlightTheme],
[]
);
return themes; return themes;
}; };
@@ -61,6 +62,7 @@ export const useThemeNames = (): Record<string, string> =>
[SilverTheme.id]: 'Silver', [SilverTheme.id]: 'Silver',
[DarkTheme.id]: 'Dark', [DarkTheme.id]: 'Dark',
[ButterTheme.id]: 'Butter', [ButterTheme.id]: 'Butter',
[MoonlightTheme.id]: 'Moonlight',
}), }),
[] []
); );
+13 -36
View File
@@ -3,39 +3,27 @@ import { configClass, varsClass } from 'folds';
import { import {
DarkTheme, DarkTheme,
LightTheme, LightTheme,
LotusTerminalLightTheme,
LotusTerminalTheme,
ThemeContextProvider, ThemeContextProvider,
ThemeKind, ThemeKind,
useActiveTheme, useActiveTheme,
useSystemThemeKind, useSystemThemeKind,
} from '../hooks/useTheme'; } from '../hooks/useTheme';
import { lotusTerminalBodyClass } from '../../lotus-terminal.css';
import { useSetting } from '../state/hooks/settings'; import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings'; import { settingsAtom } from '../state/settings';
import { runLotusBootSequence } from '../../lotus-boot';
export function UnAuthRouteThemeManager() { export function UnAuthRouteThemeManager() {
const systemThemeKind = useSystemThemeKind(); const systemThemeKind = useSystemThemeKind();
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
useEffect(() => { useEffect(() => {
document.body.className = ''; document.body.className = '';
document.body.classList.add(configClass, varsClass); document.body.classList.add(configClass, varsClass);
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(lotusTerminalBodyClass);
} else {
document.documentElement.removeAttribute('data-theme');
if (systemThemeKind === ThemeKind.Dark) { if (systemThemeKind === ThemeKind.Dark) {
document.body.classList.add(...DarkTheme.classNames); document.body.classList.add(...DarkTheme.classNames);
} else { }
if (systemThemeKind === ThemeKind.Light) {
document.body.classList.add(...LightTheme.classNames); document.body.classList.add(...LightTheme.classNames);
} }
} }, [systemThemeKind]);
}, [systemThemeKind, lotusTerminal]);
return null; return null;
} }
@@ -43,30 +31,19 @@ export function UnAuthRouteThemeManager() {
export function AuthRouteThemeManager({ children }: { children: ReactNode }) { export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
const activeTheme = useActiveTheme(); const activeTheme = useActiveTheme();
const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode'); const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
const terminalIsLight = lotusTerminal && activeTheme.kind === ThemeKind.Light;
const effectiveTheme = lotusTerminal
? (terminalIsLight ? LotusTerminalLightTheme : LotusTerminalTheme)
: activeTheme;
useEffect(() => { useEffect(() => {
document.body.className = ''; document.body.className = '';
document.body.classList.add(configClass, varsClass); document.body.classList.add(configClass, varsClass);
document.body.classList.add(...effectiveTheme.classNames);
if (lotusTerminal) {
document.documentElement.setAttribute('data-theme', terminalIsLight ? 'light' : 'dark');
document.body.classList.add(lotusTerminalBodyClass);
runLotusBootSequence();
} else {
document.documentElement.removeAttribute('data-theme');
}
if (monochromeMode && !lotusTerminal) {
document.body.style.filter = 'grayscale(1)';
} else {
document.body.style.filter = '';
}
}, [effectiveTheme, monochromeMode, lotusTerminal, terminalIsLight]);
return <ThemeContextProvider value={effectiveTheme}>{children}</ThemeContextProvider>; document.body.classList.add(...activeTheme.classNames);
if (monochromeMode) {
document.body.style.setProperty('filter', 'grayscale(1)');
} else {
document.body.style.removeProperty('filter');
}
}, [activeTheme, monochromeMode]);
return <ThemeContextProvider value={activeTheme}>{children}</ThemeContextProvider>;
} }
+5 -6
View File
@@ -1,25 +1,24 @@
import React from 'react'; import React from 'react';
import { Box, Text } from 'folds'; import { Box, Text } from 'folds';
import * as css from './styles.css'; import * as css from './styles.css';
import pkg from '../../../../package.json';
export function AuthFooter() { export function AuthFooter() {
return ( return (
<Box className={css.AuthFooter} justifyContent="Center" gap="400" wrap="Wrap"> <Box className={css.AuthFooter} justifyContent="Center" gap="400" wrap="Wrap">
<Text as="a" size="T300" href="https://lotusguild.org" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://cinny.in" target="_blank" rel="noreferrer">
About About
</Text> </Text>
<Text <Text
as="a" as="a"
size="T300" size="T300"
href="https://code.lotusguild.org/LotusGuild/cinny/releases" href="https://github.com/ajbura/cinny/releases"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
v{pkg.version} v4.11.0
</Text> </Text>
<Text as="a" size="T300" href="https://matrix.lotusguild.org" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
Community Twitter
</Text> </Text>
<Text as="a" size="T300" href="https://matrix.org" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://matrix.org" target="_blank" rel="noreferrer">
Powered by Matrix Powered by Matrix
+3 -3
View File
@@ -20,7 +20,7 @@ import {
} from '../../hooks/useClientConfig'; } from '../../hooks/useClientConfig';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH } from '../paths'; import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH } from '../paths';
import LotusLogo from '../../../../public/res/Lotus.png'; import CinnySVG from '../../../../public/res/svg/cinny.svg';
import { ServerPicker } from './ServerPicker'; import { ServerPicker } from './ServerPicker';
import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api'; import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api';
import { SpecVersionsLoader } from '../../components/SpecVersionsLoader'; import { SpecVersionsLoader } from '../../components/SpecVersionsLoader';
@@ -134,8 +134,8 @@ export function AuthLayout() {
<Box direction="Column" className={css.AuthCard}> <Box direction="Column" className={css.AuthCard}>
<Header className={css.AuthHeader} size="600" variant="Surface"> <Header className={css.AuthHeader} size="600" variant="Surface">
<Box grow="Yes" direction="Row" gap="300" alignItems="Center"> <Box grow="Yes" direction="Row" gap="300" alignItems="Center">
<img className={css.AuthLogo} src={LotusLogo} alt="Lotus Chat Logo" /> <img className={css.AuthLogo} src={CinnySVG} alt="Cinny Logo" />
<Text size="H3">Lotus Chat</Text> <Text size="H3">Cinny</Text>
</Box> </Box>
</Header> </Header>
<Box className={css.AuthCardContent} direction="Column"> <Box className={css.AuthCardContent} direction="Column">
@@ -133,7 +133,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
user: username, user: username,
}, },
password, password,
initial_device_display_name: 'Lotus Chat Web', initial_device_display_name: 'Cinny Web',
}); });
}; };
@@ -151,7 +151,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
user: mxIdUsername, user: mxIdUsername,
}, },
password, password,
initial_device_display_name: 'Lotus Chat Web', initial_device_display_name: 'Cinny Web',
}); });
}; };
const handleEmailLogin = (email: string, password: string) => { const handleEmailLogin = (email: string, password: string) => {
@@ -163,7 +163,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
address: email, address: email,
}, },
password, password,
initial_device_display_name: 'Lotus Chat Web', initial_device_display_name: 'Cinny Web',
}); });
}; };
+1 -1
View File
@@ -57,7 +57,7 @@ export function TokenLogin({ token }: TokenLoginProps) {
startLogin(baseUrl, { startLogin(baseUrl, {
type: 'm.login.token', type: 'm.login.token',
token, token,
initial_device_display_name: 'Lotus Chat Web', initial_device_display_name: 'Cinny Web',
}); });
}, [baseUrl, token, startLogin]); }, [baseUrl, token, startLogin]);
@@ -109,7 +109,7 @@ function RegisterUIAFlow({
auth: authDict, auth: authDict,
password, password,
username, username,
initial_device_display_name: 'Lotus Chat Web', initial_device_display_name: 'Cinny Web',
}); });
}, },
[onRegister, formData] [onRegister, formData]
@@ -250,7 +250,7 @@ export function PasswordRegisterForm({
auth: { auth: {
session: authData.session, session: authData.session,
}, },
initial_device_display_name: 'Lotus Chat Web', initial_device_display_name: 'Cinny Web',
}); });
}; };
+3 -3
View File
@@ -3,9 +3,9 @@ import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk'; import { RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
import { roomToUnreadAtom, unreadEqual, unreadInfoToUnread } from '../../state/room/roomToUnread'; import { roomToUnreadAtom, unreadEqual, unreadInfoToUnread } from '../../state/room/roomToUnread';
import LogoSVG from '../../../../public/res/lotus.png'; import LogoSVG from '../../../../public/res/svg/cinny.svg';
import LogoUnreadSVG from '../../../../public/res/lotus-unread.png'; import LogoUnreadSVG from '../../../../public/res/svg/cinny-unread.svg';
import LogoHighlightSVG from '../../../../public/res/lotus-highlight.png'; import LogoHighlightSVG from '../../../../public/res/svg/cinny-highlight.svg';
import NotificationSound from '../../../../public/sound/notification.ogg'; import NotificationSound from '../../../../public/sound/notification.ogg';
import InviteSound from '../../../../public/sound/invite.ogg'; import InviteSound from '../../../../public/sound/invite.ogg';
import { notificationPermission, setFavicon } from '../../utils/dom'; import { notificationPermission, setFavicon } from '../../utils/dom';
+11 -10
View File
@@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import { Box, Button, Icon, Icons, Text, config, toRem } from 'folds'; import { Box, Button, Icon, Icons, Text, config, toRem } from 'folds';
import { Page, PageHero, PageHeroSection } from '../../components/page'; import { Page, PageHero, PageHeroSection } from '../../components/page';
import LotusLogo from '../../../../public/res/Lotus.png'; import CinnySVG from '../../../../public/res/svg/cinny.svg';
import pkg from '../../../../package.json'; import { useTheme } from '../../hooks/useTheme';
export function WelcomePage() { export function WelcomePage() {
const theme = useTheme();
return ( return (
<Page> <Page transparent={theme.flat}>
<Box <Box
grow="Yes" grow="Yes"
style={{ padding: config.space.S400, paddingBottom: config.space.S700 }} style={{ padding: config.space.S400, paddingBottom: config.space.S700 }}
@@ -15,17 +16,17 @@ export function WelcomePage() {
> >
<PageHeroSection> <PageHeroSection>
<PageHero <PageHero
icon={<img width="70" height="70" src={LotusLogo} alt="Lotus Chat" style={{ objectFit: "contain" }} />} icon={<img width="70" height="70" src={CinnySVG} alt="Cinny Logo" />}
title="Welcome to Lotus Chat" title="Welcome to Cinny"
subTitle={ subTitle={
<span> <span>
A Matrix client for Lotus Guild.{' '} Yet another matrix client.{' '}
<a <a
href="https://code.lotusguild.org/LotusGuild/cinny/releases" href="https://github.com/cinnyapp/cinny/releases"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
v{pkg.version} v4.11.0
</a> </a>
</span> </span>
} }
@@ -34,7 +35,7 @@ export function WelcomePage() {
<Box grow="Yes" style={{ maxWidth: toRem(300) }} direction="Column" gap="300"> <Box grow="Yes" style={{ maxWidth: toRem(300) }} direction="Column" gap="300">
<Button <Button
as="a" as="a"
href="https://code.lotusguild.org/LotusGuild/cinny" href="https://github.com/cinnyapp/cinny"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
before={<Icon size="200" src={Icons.Code} />} before={<Icon size="200" src={Icons.Code} />}
@@ -45,7 +46,7 @@ export function WelcomePage() {
</Button> </Button>
<Button <Button
as="a" as="a"
href="https://matrix.lotusguild.org" href="https://cinny.in/#sponsor"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
fill="Soft" fill="Soft"
+3 -1
View File
@@ -9,12 +9,14 @@ import {
} from '../../../components/page'; } from '../../../components/page';
import { CreateSpaceForm } from '../../../features/create-space'; import { CreateSpaceForm } from '../../../features/create-space';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useTheme } from '../../../hooks/useTheme';
export function Create() { export function Create() {
const { navigateSpace } = useRoomNavigate(); const { navigateSpace } = useRoomNavigate();
const theme = useTheme();
return ( return (
<Page> <Page transparent={theme.flat}>
<Box grow="Yes"> <Box grow="Yes">
<Scroll hideTrack visibility="Hover"> <Scroll hideTrack visibility="Hover">
<PageContent> <PageContent>
+6 -1
View File
@@ -107,7 +107,12 @@ function DirectHeader() {
</Text> </Text>
</Box> </Box>
<Box> <Box>
<IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu}> <IconButton
aria-pressed={!!menuAnchor}
variant="Background"
fill="None"
onClick={handleOpenMenu}
>
<Icon src={Icons.VerticalDots} size="200" /> <Icon src={Icons.VerticalDots} size="200" />
</IconButton> </IconButton>
</Box> </Box>
+3 -1
View File
@@ -17,10 +17,12 @@ import {
} from '../../../components/page'; } from '../../../components/page';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { CreateChat } from '../../../features/create-chat'; import { CreateChat } from '../../../features/create-chat';
import { useTheme } from '../../../hooks/useTheme';
export function DirectCreate() { export function DirectCreate() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -38,7 +40,7 @@ export function DirectCreate() {
}, [mx, navigate, directs, userId]); }, [mx, navigate, directs, userId]);
return ( return (
<Page> <Page transparent={theme.flat}>
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<PageHeader balance outlined={false}> <PageHeader balance outlined={false}>
<Box grow="Yes" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
+3 -1
View File
@@ -18,8 +18,10 @@ import * as css from './style.css';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useTheme } from '../../../hooks/useTheme';
export function FeaturedRooms() { export function FeaturedRooms() {
const theme = useTheme();
const { featuredCommunities } = useClientConfig(); const { featuredCommunities } = useClientConfig();
const { rooms, spaces } = featuredCommunities ?? {}; const { rooms, spaces } = featuredCommunities ?? {};
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
@@ -27,7 +29,7 @@ export function FeaturedRooms() {
const { navigateSpace, navigateRoom } = useRoomNavigate(); const { navigateSpace, navigateRoom } = useRoomNavigate();
return ( return (
<Page> <Page transparent={theme.flat}>
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<PageHeader> <PageHeader>
<Box shrink="No"> <Box shrink="No">
+3 -1
View File
@@ -45,6 +45,7 @@ import { getMxIdServer } from '../../../utils/matrix';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useTheme } from '../../../hooks/useTheme';
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams => const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
useMemo( useMemo(
@@ -343,6 +344,7 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
export function PublicRooms() { export function PublicRooms() {
const { server } = useParams(); const { server } = useParams();
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const userId = mx.getUserId(); const userId = mx.getUserId();
const userServer = userId && getMxIdServer(userId); const userServer = userId && getMxIdServer(userId);
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
@@ -469,7 +471,7 @@ export function PublicRooms() {
}; };
return ( return (
<Page> <Page transparent={theme.flat}>
<PageHeader balance> <PageHeader balance>
{isSearch ? ( {isSearch ? (
<> <>
+3 -1
View File
@@ -12,14 +12,16 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { CreateRoomForm } from '../../../features/create-room'; import { CreateRoomForm } from '../../../features/create-room';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useTheme } from '../../../hooks/useTheme';
export function HomeCreateRoom() { export function HomeCreateRoom() {
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const theme = useTheme();
const { navigateRoom } = useRoomNavigate(); const { navigateRoom } = useRoomNavigate();
return ( return (
<Page> <Page transparent={theme.flat}>
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<PageHeader balance outlined={false}> <PageHeader balance outlined={false}>
<Box grow="Yes" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
+6 -1
View File
@@ -121,7 +121,12 @@ function HomeHeader() {
</Text> </Text>
</Box> </Box>
<Box> <Box>
<IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu}> <IconButton
aria-pressed={!!menuAnchor}
variant="Background"
fill="None"
onClick={handleOpenMenu}
>
<Icon src={Icons.VerticalDots} size="200" /> <Icon src={Icons.VerticalDots} size="200" />
</IconButton> </IconButton>
</Box> </Box>
+3 -1
View File
@@ -5,14 +5,16 @@ import { MessageSearch } from '../../../features/message-search';
import { useHomeRooms } from './useHomeRooms'; import { useHomeRooms } from './useHomeRooms';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useTheme } from '../../../hooks/useTheme';
export function HomeSearch() { export function HomeSearch() {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const rooms = useHomeRooms(); const rooms = useHomeRooms();
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const theme = useTheme();
return ( return (
<Page> <Page transparent={theme.flat}>
<PageHeader balance> <PageHeader balance>
<Box grow="Yes" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes" basis="No"> <Box grow="Yes" basis="No">
+3 -1
View File
@@ -67,6 +67,7 @@ import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported'; import { useReportRoomSupported } from '../../../hooks/useReportRoomSupported';
import { useSetting } from '../../../state/hooks/settings'; import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings'; import { settingsAtom } from '../../../state/settings';
import { useTheme } from '../../../hooks/useTheme';
const COMPACT_CARD_WIDTH = 548; const COMPACT_CARD_WIDTH = 548;
@@ -693,6 +694,7 @@ function SpamInvites({
export function Invites() { export function Invites() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { navigateRoom, navigateSpace } = useRoomNavigate();
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
@@ -746,7 +748,7 @@ export function Invites() {
}; };
return ( return (
<Page> <Page transparent={theme.flat}>
<PageHeader balance> <PageHeader balance>
<Box grow="Yes" gap="200"> <Box grow="Yes" gap="200">
<Box grow="Yes" basis="No"> <Box grow="Yes" basis="No">
+2 -1
View File
@@ -563,6 +563,7 @@ const DEFAULT_REFRESH_MS = 7000;
export function Notifications() { export function Notifications() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
@@ -635,7 +636,7 @@ export function Notifications() {
}, [timelineState, notificationTimeline, lastVItemIndex, loadTimeline]); }, [timelineState, notificationTimeline, lastVItemIndex, loadTimeline]);
return ( return (
<Page> <Page transparent={theme.flat}>
<PageHeader balance> <PageHeader balance>
<Box grow="Yes" gap="200"> <Box grow="Yes" gap="200">
<Box grow="Yes" basis="No"> <Box grow="Yes" basis="No">
+3 -1
View File
@@ -11,9 +11,11 @@ import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useTheme } from '../../../hooks/useTheme';
export function SpaceSearch() { export function SpaceSearch() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const theme = useTheme();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const space = useSpace(); const space = useSpace();
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
@@ -27,7 +29,7 @@ export function SpaceSearch() {
); );
return ( return (
<Page> <Page transparent={theme.flat}>
<PageHeader balance> <PageHeader balance>
<Box grow="Yes" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes" basis="No"> <Box grow="Yes" basis="No">
+6 -1
View File
@@ -273,7 +273,12 @@ function SpaceHeader() {
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />} {joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
<IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu}> <IconButton
aria-pressed={!!menuAnchor}
variant="Background"
fill="None"
onClick={handleOpenMenu}
>
<Icon src={Icons.VerticalDots} size="200" /> <Icon src={Icons.VerticalDots} size="200" />
</IconButton> </IconButton>
</Box> </Box>
+8 -24
View File
@@ -29,13 +29,17 @@ export class CallControl extends EventEmitter implements CallControlState {
} }
private get settingsButton(): HTMLElement | undefined { private get settingsButton(): HTMLElement | undefined {
// EC 0.19.3: settings button has data-testid="settings-bottom-center" const leaveBtn = this.document?.querySelector('[data-testid="incall_leave"]');
return (this.document?.querySelector('[data-testid="settings-bottom-center"]') as HTMLElement) ?? undefined;
const settingsButton = leaveBtn?.previousElementSibling as HTMLElement | null;
return settingsButton ?? undefined;
} }
private get reactionsButton(): HTMLElement | undefined { private get reactionsButton(): HTMLElement | undefined {
// EC 0.19.3: reactions/raise-hand button has a CSS module class containing "raiseHand" const reactionsButton = this.settingsButton?.previousElementSibling as HTMLElement | null;
return (this.document?.querySelector('[class*="raiseHand"]') as HTMLElement) ?? undefined;
return reactionsButton ?? undefined;
} }
private get spotlightButton(): HTMLInputElement | undefined { private get spotlightButton(): HTMLInputElement | undefined {
@@ -97,11 +101,6 @@ export class CallControl extends EventEmitter implements CallControlState {
this.emitStateUpdate(); this.emitStateUpdate();
} }
public async forceState(desired: CallControlState) {
this.state = new CallControlState(desired.microphone, desired.video, desired.sound, this.screenshare, this.spotlight);
await this.applyState();
}
public startObserving() { public startObserving() {
this.controlMutationObserver.disconnect(); this.controlMutationObserver.disconnect();
@@ -161,8 +160,6 @@ export class CallControl extends EventEmitter implements CallControlState {
} }
public onControlMutation() { public onControlMutation() {
const prevScreenshare = this.screenshare;
const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary'; const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
const spotlight: boolean = this.spotlightButton?.checked ?? false; const spotlight: boolean = this.spotlightButton?.checked ?? false;
@@ -174,19 +171,6 @@ export class CallControl extends EventEmitter implements CallControlState {
spotlight spotlight
); );
this.emitStateUpdate(); this.emitStateUpdate();
// EC auto-switches to spotlight when screenshare starts — revert to grid
if (!prevScreenshare && screenshare) {
setTimeout(() => { if (this.spotlight) this.gridButton?.click(); }, 600);
}
}
public setMicrophone(enabled: boolean) {
const payload: ElementMediaStatePayload = {
audio_enabled: enabled,
video_enabled: this.video,
};
return this.setMediaState(payload);
} }
public toggleMicrophone() { public toggleMicrophone() {
+3 -39
View File
@@ -47,10 +47,6 @@ export class CallEmbed {
private readonly disposables: Array<() => void> = []; private readonly disposables: Array<() => void> = [];
private readonly initialState: CallControlState;
private styleRetryObserver?: MutationObserver;
static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent { static getIntent(dm: boolean, ongoing: boolean): ElementCallIntent {
if (ongoing) { if (ongoing) {
return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting; return dm ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExisting;
@@ -63,10 +59,7 @@ export class CallEmbed {
mx: MatrixClient, mx: MatrixClient,
room: Room, room: Room,
intent: ElementCallIntent, intent: ElementCallIntent,
themeKind: ElementCallThemeKind, themeKind: ElementCallThemeKind
noiseSuppression = true,
initialAudio = true,
initialVideo = false
): Widget { ): Widget {
const userId = mx.getSafeUserId(); const userId = mx.getSafeUserId();
const deviceId = mx.getDeviceId() ?? ''; const deviceId = mx.getDeviceId() ?? '';
@@ -88,9 +81,6 @@ export class CallEmbed {
perParticipantE2EE: room.hasEncryptionStateEvent().toString(), perParticipantE2EE: room.hasEncryptionStateEvent().toString(),
lang: 'en-EN', lang: 'en-EN',
theme: themeKind, theme: themeKind,
noiseSuppression: noiseSuppression.toString(),
audio: initialAudio.toString(),
video: initialVideo.toString(),
}); });
const widgetUrl = new URL( const widgetUrl = new URL(
@@ -153,7 +143,6 @@ export class CallEmbed {
const controlState = initialControlState ?? new CallControlState(true, false, true); const controlState = initialControlState ?? new CallControlState(true, false, true);
this.control = new CallControl(controlState, call, iframe); this.control = new CallControl(controlState, call, iframe);
this.initialState = controlState;
let initialMediaEvent = true; let initialMediaEvent = true;
this.disposables.push( this.disposables.push(
@@ -238,7 +227,6 @@ export class CallEmbed {
this.disposables.forEach((disposable) => { this.disposables.forEach((disposable) => {
disposable(); disposable();
}); });
this.styleRetryObserver?.disconnect();
this.call.stop(); this.call.stop();
this.container.removeChild(this.iframe); this.container.removeChild(this.iframe);
this.control.dispose(); this.control.dispose();
@@ -257,8 +245,6 @@ export class CallEmbed {
this.joined = true; this.joined = true;
this.applyStyles(); this.applyStyles();
this.control.startObserving(); this.control.startObserving();
// EC ignores io.element.device_mute before join; re-apply desired state now that EC is live
this.control.forceState(this.initialState);
} }
private applyStyles(): void { private applyStyles(): void {
@@ -266,34 +252,12 @@ export class CallEmbed {
if (!doc) return; if (!doc) return;
doc.body.style.setProperty('background', 'none', 'important'); doc.body.style.setProperty('background', 'none', 'important');
const controls = doc.body.querySelector('[data-testid="incall_leave"]')?.parentElement
// Inject CSS for things that can't be reliably caught by DOM timing ?.parentElement;
if (!doc.getElementById('lotus-ec-styles')) {
const style = doc.createElement('style');
style.id = 'lotus-ec-styles';
style.textContent = [
'body { background: none !important; }',
// Hide "using to Device key transport" status line
'[style*="height: 0"][style*="z-index: 1"][style*="align-self: center"] { display: none !important; }',
].join('\n');
(doc.head ?? doc.body).appendChild(style);
}
// Hide EC built-in controls (we provide our own)
const leaveBtn = doc.body.querySelector('[data-testid="incall_leave"]');
if (leaveBtn) {
this.styleRetryObserver?.disconnect();
this.styleRetryObserver = undefined;
const controls = leaveBtn.parentElement?.parentElement;
if (controls) { if (controls) {
controls.style.setProperty('position', 'absolute'); controls.style.setProperty('position', 'absolute');
controls.style.setProperty('visibility', 'hidden'); controls.style.setProperty('visibility', 'hidden');
} }
} else if (!this.styleRetryObserver) {
// Controls not in DOM yet — observe and retry when they appear
this.styleRetryObserver = new MutationObserver(() => this.applyStyles());
this.styleRetryObserver.observe(doc.body, { childList: true, subtree: true });
}
} }
private onEvent(ev: MatrixEvent): void { private onEvent(ev: MatrixEvent): void {
+2 -5
View File
@@ -28,13 +28,10 @@ export const makeCallPreferencesAtom = (userId: string): CallPreferencesAtom =>
storeKey, storeKey,
(key) => { (key) => {
const v = getLocalStorageItem<CallPreferences>(key, DEFAULT_PREFERENCES); const v = getLocalStorageItem<CallPreferences>(key, DEFAULT_PREFERENCES);
// Never restore camera state — always start with camera off for privacy. return v;
// Users can toggle it in the prescreen before joining.
return { ...v, video: false };
}, },
(key, value) => { (key, value) => {
// Don't persist video state — always resets to off on next load. setLocalStorageItem(key, value);
setLocalStorageItem(key, { ...value, video: false });
} }
); );
-4
View File
@@ -1,8 +1,6 @@
import { createContext, useCallback, useContext } from 'react'; import { createContext, useCallback, useContext } from 'react';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { CallPreferences, CallPreferencesAtom } from '../callPreferences'; import { CallPreferences, CallPreferencesAtom } from '../callPreferences';
import { useSetting } from './settings';
import { settingsAtom } from '../settings';
const CallPreferencesAtomContext = createContext<CallPreferencesAtom | null>(null); const CallPreferencesAtomContext = createContext<CallPreferencesAtom | null>(null);
export const CallPreferencesProvider = CallPreferencesAtomContext.Provider; export const CallPreferencesProvider = CallPreferencesAtomContext.Provider;
@@ -23,7 +21,6 @@ export const useCallPreferences = (): CallPreferences & {
} => { } => {
const callPrefAtom = useCallPreferencesAtom(); const callPrefAtom = useCallPreferencesAtom();
const [pref, setPref] = useAtom(callPrefAtom); const [pref, setPref] = useAtom(callPrefAtom);
const [cameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
const toggleMicrophone = useCallback(() => { const toggleMicrophone = useCallback(() => {
const microphone = !pref.microphone; const microphone = !pref.microphone;
@@ -57,7 +54,6 @@ export const useCallPreferences = (): CallPreferences & {
return { return {
...pref, ...pref,
video: cameraOnJoin ? pref.video : false,
toggleMicrophone, toggleMicrophone,
toggleVideo, toggleVideo,
toggleSound, toggleSound,
-19
View File
@@ -3,7 +3,6 @@ import { atom } from 'jotai';
const STORAGE_KEY = 'settings'; const STORAGE_KEY = 'settings';
export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | ''; export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500'; export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export type ChatBackground = 'none' | 'blueprint' | 'carbon' | 'stars' | 'topographic' | 'herringbone' | 'crosshatch' | 'chevron' | 'polka' | 'triangles' | 'plaid' | 'tactical' | 'circuit' | 'hexgrid';
export enum MessageLayout { export enum MessageLayout {
Modern = 0, Modern = 0,
Compact = 1, Compact = 1,
@@ -42,15 +41,6 @@ export interface Settings {
dateFormatString: string; dateFormatString: string;
developerTools: boolean; developerTools: boolean;
lotusTerminal: boolean;
chatBackground: ChatBackground;
perMessageProfiles: boolean;
cameraOnJoin: boolean;
callNoiseSuppression: boolean;
pttMode: boolean;
pttKey: string;
} }
const defaultSettings: Settings = { const defaultSettings: Settings = {
@@ -85,15 +75,6 @@ const defaultSettings: Settings = {
dateFormatString: 'D MMM YYYY', dateFormatString: 'D MMM YYYY',
developerTools: false, developerTools: false,
lotusTerminal: false,
chatBackground: 'none',
perMessageProfiles: false,
cameraOnJoin: false,
callNoiseSuppression: true,
pttMode: false,
pttKey: 'Space',
}; };
export const getSettings = () => { export const getSettings = () => {
+3 -3
View File
@@ -2,8 +2,8 @@ import { style } from '@vanilla-extract/css';
import { color, toRem } from 'folds'; import { color, toRem } from 'folds';
export const BackgroundDotPattern = style({ export const BackgroundDotPattern = style({
backgroundImage: `radial-gradient(${color.Background.ContainerActive} ${toRem(2)}, ${ backgroundImage: `radial-gradient(${color.Background.ContainerActive} ${toRem(
color.Background.Container 2
} ${toRem(2)})`, )}, transparent ${toRem(2)})`,
backgroundSize: `${toRem(40)} ${toRem(40)}`, backgroundSize: `${toRem(40)} ${toRem(40)}`,
}); });
+189 -162
View File
@@ -1,7 +1,115 @@
import { createTheme } from '@vanilla-extract/css'; import { createTheme, createThemeContract } from '@vanilla-extract/css';
import { color } from 'folds'; import { color } from 'folds';
export const silverTheme = createTheme(color, { export const extendedColor = createThemeContract({
background: '',
});
const ThemeContract = {
...extendedColor,
...color,
};
export const lightTheme = createTheme(ThemeContract, {
background: '#F2F2F2',
Background: {
Container: '#F2F2F2',
ContainerHover: '#E5E5E5',
ContainerActive: '#D9D9D9',
ContainerLine: '#CCCCCC',
OnContainer: '#000000',
},
Surface: {
Container: '#FFFFFF',
ContainerHover: '#F2F2F2',
ContainerActive: '#E5E5E5',
ContainerLine: '#D9D9D9',
OnContainer: '#000000',
},
SurfaceVariant: {
Container: '#F2F2F2',
ContainerHover: '#E5E5E5',
ContainerActive: '#D9D9D9',
ContainerLine: '#CCCCCC',
OnContainer: '#000000',
},
Primary: {
Main: '#1858D5',
MainHover: '#164FC0',
MainActive: '#144BB5',
MainLine: '#1346AA',
OnMain: '#FFFFFF',
Container: '#E8EEFB',
ContainerHover: '#DCE6F9',
ContainerActive: '#D1DEF7',
ContainerLine: '#C5D5F5',
OnContainer: '#113E95',
},
Secondary: {
Main: '#000000',
MainHover: '#1A1A1A',
MainActive: '#262626',
MainLine: '#333333',
OnMain: '#FFFFFF',
Container: '#D9D9D9',
ContainerHover: '#CCCCCC',
ContainerActive: '#BFBFBF',
ContainerLine: '#B2B2B2',
OnContainer: '#0D0D0D',
},
Success: {
Main: '#00844C',
MainHover: '#007744',
MainActive: '#007041',
MainLine: '#006A3D',
OnMain: '#FFFFFF',
Container: '#E5F3ED',
ContainerHover: '#D9EDE4',
ContainerActive: '#CCE6DB',
ContainerLine: '#BFE0D2',
OnContainer: '#005C35',
},
Warning: {
Main: '#A85400',
MainHover: '#974C00',
MainActive: '#8F4700',
MainLine: '#864300',
OnMain: '#FFFFFF',
Container: '#F6EEE5',
ContainerHover: '#F2E5D9',
ContainerActive: '#EEDDCC',
ContainerLine: '#E9D4BF',
OnContainer: '#763B00',
},
Critical: {
Main: '#C40E0E',
MainHover: '#AC0909',
MainActive: '#A60C0C',
MainLine: '#9C0B0B',
OnMain: '#FFFFFF',
Container: '#F9E7E7',
ContainerHover: '#F6DBDB',
ContainerActive: '#F3CFCF',
ContainerLine: '#F0C3C3',
OnContainer: '#890A0A',
},
Other: {
FocusRing: 'rgba(0 0 0 / 50%)',
Shadow: 'rgba(0 0 0 / 20%)',
Overlay: 'rgba(0 0 0 / 50%)',
},
});
export const silverTheme = createTheme(ThemeContract, {
background: '#DEDEDE',
Background: { Background: {
Container: '#DEDEDE', Container: '#DEDEDE',
ContainerHover: '#D3D3D3', ContainerHover: '#D3D3D3',
@@ -99,6 +207,7 @@ export const silverTheme = createTheme(color, {
}); });
const darkThemeData = { const darkThemeData = {
background: color.Background.Container,
Background: { Background: {
Container: '#1A1A1A', Container: '#1A1A1A',
ContainerHover: '#262626', ContainerHover: '#262626',
@@ -195,9 +304,9 @@ const darkThemeData = {
}, },
}; };
export const darkTheme = createTheme(color, darkThemeData); export const darkTheme = createTheme(ThemeContract, darkThemeData);
export const butterTheme = createTheme(color, { export const butterTheme = createTheme(ThemeContract, {
...darkThemeData, ...darkThemeData,
Background: { Background: {
Container: '#1A1916', Container: '#1A1916',
@@ -237,188 +346,106 @@ export const butterTheme = createTheme(color, {
}, },
}); });
export const lotusTerminalTheme = createTheme(color, { export const moonlightTheme = createTheme(ThemeContract, {
background: 'linear-gradient(to top right, #000546, #000)',
Background: { Background: {
Container: '#030508', Container: '#000000',
ContainerHover: '#060c14', ContainerHover: '#01032B',
ContainerActive: '#0d1520', ContainerActive: '#020546',
ContainerLine: '#07101a', ContainerLine: '#030977',
OnContainer: '#c4d9ee', OnContainer: '#EAEBFF',
}, },
Surface: { Surface: {
Container: '#060c14', Container: '#01032B',
ContainerHover: '#0d1520', ContainerHover: '#020546',
ContainerActive: '#07101a', ContainerActive: '#03075E',
ContainerLine: '#111d2b', ContainerLine: '#030977',
OnContainer: '#c4d9ee', OnContainer: '#EAEBFF',
}, },
SurfaceVariant: { SurfaceVariant: {
Container: '#0d1520', Container: '#020546',
ContainerHover: '#07101a', ContainerHover: '#03075E',
ContainerActive: '#111d2b', ContainerActive: '#030977',
ContainerLine: '#162436', ContainerLine: '#030977',
OnContainer: '#c4d9ee', OnContainer: '#EAEBFF',
}, },
Primary: { Primary: {
Main: '#FF6B00', Main: '#0043FF',
MainHover: '#FF8C2B', MainHover: '#1A56FF',
MainActive: '#FF9940', MainActive: '#3369FF',
MainLine: '#FFA655', MainLine: '#4D7BFF',
OnMain: '#030508', OnMain: '#FFFFFF',
Container: '#2a1200',
ContainerHover: '#3a1a00', Container: '#00144D',
ContainerActive: '#4a2200', ContainerHover: '#001B66',
ContainerLine: '#5a2a00', ContainerActive: '#002180',
OnContainer: '#FFB87A', ContainerLine: '#002899',
OnContainer: '#FFFFFF',
}, },
Secondary: { Secondary: {
Main: '#00D4FF', Main: '#E5E7FF',
MainHover: '#33DFFF', MainHover: '#CCCFFF',
MainActive: '#4DE5FF', MainActive: '#B2B6FF',
MainLine: '#66EBFF', MainLine: '#999EFF',
OnMain: '#030508', OnMain: '#000000',
Container: '#001a22',
ContainerHover: '#002233', Container: '#030763',
ContainerActive: '#002a3d', ContainerHover: '#04097C',
ContainerLine: '#003347', ContainerActive: '#050B94',
OnContainer: '#99E8FF', ContainerLine: '#050CAD',
OnContainer: '#FFFFFF',
}, },
Success: { Success: {
Main: '#00FF88', Main: '#00FF95',
MainHover: '#33FFAA', MainHover: '#00E586',
MainActive: '#4DFFB8', MainActive: '#00CC77',
MainLine: '#66FFC6', MainLine: '#4DFFB5',
OnMain: '#030508', OnMain: '#0B2E22',
Container: '#002211',
ContainerHover: '#002a16', Container: '#004D3B',
ContainerActive: '#00331b', ContainerHover: '#00664F',
ContainerLine: '#003b20', ContainerActive: '#008062',
OnContainer: '#99FFCC', ContainerLine: '#008062',
OnContainer: '#CCF2E2',
}, },
Warning: { Warning: {
Main: '#FFB300', Main: '#FF9933',
MainHover: '#FFC233', MainHover: '#FFA64D',
MainActive: '#FFCC4D', MainActive: '#FFB266',
MainLine: '#FFD566', MainLine: '#FFBF80',
OnMain: '#030508', OnMain: '#000000',
Container: '#221900',
ContainerHover: '#2a2000', Container: '#4D2600',
ContainerActive: '#332800', ContainerHover: '#663200',
ContainerLine: '#3b2e00', ContainerActive: '#803F00',
OnContainer: '#FFE08A', ContainerLine: '#994C00',
OnContainer: '#F3E2D1',
}, },
Critical: { Critical: {
Main: '#FF2D55', Main: '#FF0000',
MainHover: '#FF4D6D', MainHover: '#E50000',
MainActive: '#FF6080', MainActive: '#CC0000',
MainLine: '#FF7390', MainLine: '#FF4D4D',
OnMain: '#030508', OnMain: '#ffffff',
Container: '#22000d',
ContainerHover: '#2a0011', Container: '#4D0000',
ContainerActive: '#330015', ContainerHover: '#660000',
ContainerLine: '#3b0019', ContainerActive: '#800000',
OnContainer: '#FF99AA', ContainerLine: '#990000',
OnContainer: '#F5D6D6',
}, },
Other: { Other: {
FocusRing: 'rgba(0, 212, 255, 0.5)', FocusRing: 'hsla(237, 100%, 70%, 0.6)',
Shadow: 'rgba(0, 0, 0, 1)', Shadow: 'rgba(0, 0, 0, 0.9)',
Overlay: 'rgba(3, 5, 8, 0.94)', Overlay: 'rgba(0, 0, 0, 0.8)',
}, },
}); });
export const lotusTerminalLightTheme = createTheme(color, {
Background: {
Container: '#edf0f5',
ContainerHover: '#e2e7ef',
ContainerActive: '#d4dae6',
ContainerLine: '#c8d0de',
OnContainer: '#111827',
},
Surface: {
Container: '#f4f6fa',
ContainerHover: '#e8ecf4',
ContainerActive: '#dde2ed',
ContainerLine: '#d0d7e5',
OnContainer: '#111827',
},
SurfaceVariant: {
Container: '#e2e7ef',
ContainerHover: '#d4dae6',
ContainerActive: '#c8d0de',
ContainerLine: '#bdc6d6',
OnContainer: '#2d3d56',
},
Primary: {
Main: '#c44e00',
MainHover: '#a33f00',
MainActive: '#8f3700',
MainLine: '#d45800',
OnMain: '#ffffff',
Container: 'rgba(196,78,0,0.10)',
ContainerHover: 'rgba(196,78,0,0.16)',
ContainerActive: 'rgba(196,78,0,0.22)',
ContainerLine: 'rgba(196,78,0,0.28)',
OnContainer: '#6b2200',
},
Secondary: {
Main: '#0062b8',
MainHover: '#0052a0',
MainActive: '#004488',
MainLine: '#0070cc',
OnMain: '#ffffff',
Container: 'rgba(0,98,184,0.10)',
ContainerHover: 'rgba(0,98,184,0.16)',
ContainerActive: 'rgba(0,98,184,0.22)',
ContainerLine: 'rgba(0,98,184,0.28)',
OnContainer: '#003580',
},
Success: {
Main: '#006d35',
MainHover: '#005c2c',
MainActive: '#004a22',
MainLine: '#007d3e',
OnMain: '#ffffff',
Container: 'rgba(0,109,53,0.10)',
ContainerHover: 'rgba(0,109,53,0.16)',
ContainerActive: 'rgba(0,109,53,0.22)',
ContainerLine: 'rgba(0,109,53,0.28)',
OnContainer: '#003520',
},
Warning: {
Main: '#8a5a00',
MainHover: '#7a4e00',
MainActive: '#6a4400',
MainLine: '#9a6600',
OnMain: '#ffffff',
Container: 'rgba(138,90,0,0.10)',
ContainerHover: 'rgba(138,90,0,0.16)',
ContainerActive: 'rgba(138,90,0,0.22)',
ContainerLine: 'rgba(138,90,0,0.28)',
OnContainer: '#4a3000',
},
Critical: {
Main: '#b5001f',
MainHover: '#9a0019',
MainActive: '#800015',
MainLine: '#cc0023',
OnMain: '#ffffff',
Container: 'rgba(181,0,31,0.10)',
ContainerHover: 'rgba(181,0,31,0.16)',
ContainerActive: 'rgba(181,0,31,0.22)',
ContainerLine: 'rgba(181,0,31,0.28)',
OnContainer: '#600010',
},
Other: {
FocusRing: 'rgba(0, 98, 184, 0.50)',
Shadow: 'rgba(0, 0, 0, 0.15)',
Overlay: 'rgba(237, 240, 245, 0.97)',
},
});
+1 -2
View File
@@ -22,8 +22,7 @@
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif; --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
} }
.dark-theme, .global-dark {
.butter-theme {
--tc-link: hsl(213deg 100% 80%); --tc-link: hsl(213deg 100% 80%);
--mx-uc-1: hsl(208, 100%, 75%); --mx-uc-1: hsl(208, 100%, 75%);
+10
View File
@@ -0,0 +1,10 @@
import { globalStyle } from '@vanilla-extract/css';
import { color } from 'folds';
import { extendedColor } from './colors.css';
globalStyle('body', {
background: extendedColor.background,
borderColor: color.Background.ContainerLine,
outlineColor: color.Background.ContainerLine,
color: color.Background.OnContainer,
});

Some files were not shown because too many files have changed in this diff Show More