Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6648ec68a2 | |||
| 12541cf987 | |||
| 74963b6bf2 | |||
| bf544ebc84 | |||
| 1ab38281f3 | |||
| 0d5ba83f40 | |||
| b2d36d79e6 | |||
| 549634dca0 | |||
| a2331eab1f | |||
| e30212f409 | |||
| 0d28f10c95 | |||
| 0a2ba171b9 | |||
| 8e9936b829 | |||
| 0a29e42b49 | |||
| 977b45f6da | |||
| 0afd77deaa |
@@ -1,6 +1,6 @@
|
||||
# Lotus Chat
|
||||
|
||||
A Matrix client for [Lotus Guild](https://lotusguild.org) — forked from [Cinny](https://github.com/cinnyapp/cinny) v4.11.1.
|
||||
A Matrix client for [Lotus Guild](https://lotusguild.org) — forked from [Cinny](https://github.com/cinnyapp/cinny) v4.12.1.
|
||||
|
||||
Deployed at [chat.lotusguild.org](https://chat.lotusguild.org).
|
||||
|
||||
@@ -58,8 +58,78 @@ A full custom theme engine layered on top of Cinny's vanilla-extract theming:
|
||||
- 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
|
||||
- **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.
|
||||
- **Call button scoping**: The upstream Cinny 4.12.1 call button (voice + video dropdown) is restricted to DMs and private group chats only. Specifically: direct messages, or invite-only rooms that have no `m.space.parent` state event (i.e. not a space/guild text channel). Public rooms and space channels are excluded to prevent accidental mass-notifications. `Room.tsx` switches to CallView layout when a call embed is active in the current room.
|
||||
- **Poll display**: `m.poll.start` events (both stable Matrix 1.7 `m.poll` content key and MSC3381 unstable `org.matrix.msc3381.poll.start`) render as read-only poll cards inside the standard message bubble — question and answer options shown. Registered as top-level event renderers AND inside the `EncryptedContent` callback so encrypted polls also display after decryption. "Open in Element to vote" note displayed. Implemented in `PollContent.tsx`.
|
||||
- **Deleted message placeholder**: Redacted `m.room.message`, `m.room.encrypted`, and `m.sticker` events no longer disappear from the timeline. Instead they reach the existing `RedactedContent` component (trash icon + italic "This message has been deleted" with reason if provided), matching Element, FluffyChat, Commet, and Nheko behaviour. One-line change in the `eventRenderer` filter in `RoomTimeline.tsx`.
|
||||
- **Incoming DM call notification**: When another user starts a call in a DM room, a fixed-position notification appears with the caller's avatar and display name, Answer and Decline buttons, and a Web Audio API double-pulse ring tone. The notification auto-dismisses after 30 seconds or when the caller leaves. Fully themed for both LotusGuild Terminal TDS (dark navy `#060c14` background, orange border, neon-green Answer button, JetBrains Mono font, pulsing indicator dot) and standard Cinny dark/light mode (CSS variable theming, folds `Button` with `variant="Success"/"Critical"`). Implemented in `useIncomingDmCall` hook (watches `MatrixRTCSessionManagerEvents.SessionStarted`, filters encrypted ≤2-member rooms, ignores self) and `IncomingCallNotification` component mounted inside `CallEmbedProvider` in `Router.tsx`.
|
||||
- **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.
|
||||
|
||||
### Messaging Enhancements
|
||||
|
||||
- **GIF picker**: Giphy-powered GIF search and send. Button appears in the message composer only when `gifApiKey` is set in `config.json`. Sends GIF as `m.image` — fetches blob, uploads via `mx.uploadContent`, sends with `mx.sendMessage`. `FocusTrap` handles click-outside / Escape to close. When TDS is active: dark navy background (`#060c14`), orange dim border, `// GIF_SEARCH` header, injected `<style>` overrides Giphy SDK search bar (dark bg, orange border/focus ring, JetBrains Mono), custom orange scrollbar.
|
||||
- **Message forwarding**: Forward any message to any room from the message context menu.
|
||||
- **Image/video captions**: Caption text field on image and video upload — sent as a single event with the media.
|
||||
- **Location sharing**: Map embed view for incoming location events + static share button. Renders `m.location` events inline with a map tile.
|
||||
- **Deleted message placeholders**: Redacted `m.room.message`, `m.room.encrypted`, and `m.sticker` events render as "This message has been deleted" with reason (if provided) rather than disappearing. One-line change in the `eventRenderer` filter in `RoomTimeline.tsx`.
|
||||
|
||||
### Per-Message Read Receipts
|
||||
|
||||
Full per-message read receipt system — shows who has read each message directly in the timeline.
|
||||
|
||||
**Architecture:**
|
||||
- `useRoomReadPositions(room)` hook — computes a `Map<eventId, userId[]>` from all joined members' `room.getEventReadUpTo()` positions. Subscribes to `RoomEvent.Receipt` for live updates.
|
||||
- `nearestRenderableId(liveEvents, evtId)` — receipts can land on reaction/edit events that `RoomTimeline` skips (renders `null`). This walks backwards from the receipt event through the live timeline until it finds a non-reaction/non-edit event to attach to.
|
||||
- `ReadPositionsContext` — React context providing the positions map from `RoomTimeline` down to all `Message` instances without prop drilling.
|
||||
- `ReadReceiptAvatars` component — renders a pill-shaped row of overlapping `StackedAvatar` circles (24px, `SurfaceVariant` outline) below messages with readers. Pill uses `color.SurfaceVariant.Container` background for visibility on any wallpaper. Max 5 avatars shown + `+N` overflow count. Avatar fallback uses `colorMXID(userId)` for distinctive per-user color.
|
||||
- Clicking the pill opens the **"Seen by" modal** (`EventReaders`) listing all readers with their avatar, display name, and a formatted read timestamp ("Today at 3:42 PM", "Yesterday at 10:15 AM", "May 14 at 9:00 AM"). Timestamps use `room.getReadReceiptForUserId(userId)?.data.ts` and respect the user's 24-hour clock setting.
|
||||
- Authenticated media (`mxcUrlToHttp` utility) used for all avatar loads, matching the correct Lotus utility signature.
|
||||
|
||||
### DM Call Improvements
|
||||
|
||||
- **Incoming call ring**: DM calls trigger a ring tone with Answer/Decline UI. 30-second auto-dismiss if unanswered. Implemented in `Room.tsx` and `RoomViewHeader.tsx`.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- **Authenticated media**: All avatar/media loads use `mxcUrlToHttp(mx, mxcUrl, useAuthentication, w, h, 'crop')` from `../../utils/matrix` — the Lotus utility that handles MSC3916 authenticated media. (Upstream Cinny uses the SDK method with incorrect argument order for authenticated endpoints.)
|
||||
- **Upstream tracking**: `git remote add upstream https://github.com/cinnyapp/cinny.git`. Merge strategy: `git fetch upstream && git merge upstream/main`. Daily check via `cinny-upstream-check.sh` on LXC 106 — notifies Matrix on new upstream commits.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build # outputs to dist/
|
||||
```
|
||||
|
||||
Vite's render-chunks phase requires ~6 GB Node heap. If OOM killed, set:
|
||||
```bash
|
||||
NODE_OPTIONS=--max_old_space_size=6144 npm run build
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Built files are served from `/var/www/html/` on LXC 106 (nginx). Config lives at `/opt/lotus-cinny/config.json` (vite copies it to `dist/`):
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": ["matrix.lotusguild.org"],
|
||||
"allowCustomHomeservers": false,
|
||||
"gifApiKey": "<giphy_key>"
|
||||
}
|
||||
```
|
||||
|
||||
## Key Custom Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/lotus-terminal.css.ts` | All TDS CSS tokens, global styles, light/dark variants |
|
||||
| `src/lotus-boot.ts` | Boot sequence animation (runs once per session) |
|
||||
| `src/app/hooks/useRoomReadPositions.ts` | Per-message read receipt position map |
|
||||
| `src/app/features/room/ReadPositionsContext.ts` | React context for read positions |
|
||||
| `src/app/components/read-receipt-avatars/` | Read receipt avatar pill component |
|
||||
| `src/app/components/event-readers/EventReaders.tsx` | "Seen by" modal with timestamps |
|
||||
| `src/app/components/GifPicker.tsx` | GIF search + send |
|
||||
| `src/app/features/call/CallControls.tsx` | PTT badge + keybind logic |
|
||||
| `src/app/plugins/call/CallControl.ts` | EC widget bridge (screenshare revert, PTT mic) |
|
||||
| `src/app/components/CallEmbedProvider.tsx` | PiP + draggable call embed |
|
||||
|
||||
Generated
+471
-13
@@ -1,18 +1,21 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "4.12.1",
|
||||
"name": "lotus-chat",
|
||||
"version": "4.12.1-lotus",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cinny",
|
||||
"version": "4.12.1",
|
||||
"name": "lotus-chat",
|
||||
"version": "4.12.1-lotus",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
||||
"@fontsource/inter": "4.5.14",
|
||||
"@giphy/js-fetch-api": "5.8.0",
|
||||
"@giphy/js-types": "5.1.0",
|
||||
"@giphy/react-components": "10.1.2",
|
||||
"@tanstack/react-query": "5.24.1",
|
||||
"@tanstack/react-query-devtools": "5.24.1",
|
||||
"@tanstack/react-virtual": "3.2.0",
|
||||
@@ -63,10 +66,11 @@
|
||||
"slate-dom": "0.123.0",
|
||||
"slate-history": "0.113.1",
|
||||
"slate-react": "0.123.0",
|
||||
"styled-components": "6.4.1",
|
||||
"ua-parser-js": "1.0.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-hq/element-call-embedded": "0.19.1",
|
||||
"@element-hq/element-call-embedded": "0.19.3",
|
||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
@@ -1837,9 +1841,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@element-hq/element-call-embedded": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@element-hq/element-call-embedded/-/element-call-embedded-0.19.1.tgz",
|
||||
"integrity": "sha512-RDZY3P3LTx10ACaGhzkwh2+boNB3x54zHF/7v/cCyoQlAVfEYMhgMEb4CRTwJFwwYFe1r++6Higa0A0G5XxZ8Q==",
|
||||
"version": "0.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@element-hq/element-call-embedded/-/element-call-embedded-0.19.3.tgz",
|
||||
"integrity": "sha512-ZwAK6PrMDiIFf3uDZuYpRR+Ho7OgfCSnENd9hxRH6LhWlRwuo42/4Nhn6wknqRjAeMJtJo/yFIn0Zh+jZ/Gf7Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
@@ -1847,6 +1851,21 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
|
||||
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
|
||||
},
|
||||
"node_modules/@emotion/is-prop-valid": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/memoize": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
|
||||
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild-plugins/node-globals-polyfill": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz",
|
||||
@@ -2302,6 +2321,84 @@
|
||||
"tslib": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/@giphy/colors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/colors/-/colors-1.0.1.tgz",
|
||||
"integrity": "sha512-CMUY2UR8Ujl/wTRVjwu9spss6Cf2qkQNATKfoWCTWQp7CXdSfPWUDepp0c0/F0n7lYphLFvD9qzOC4NSdBt7nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@giphy/js-analytics": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-5.0.0.tgz",
|
||||
"integrity": "sha512-jBZG6OqyMWB6meLi8Sz3iLplXYnhkbj+DJhS4ChmRX8Y6UA7i5dbbsUN/So1s7tTjhZOvu0rxA6rWJE73S1FvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@giphy/js-types": "*",
|
||||
"@giphy/js-util": "*",
|
||||
"append-query": "^2.1.0",
|
||||
"throttle-debounce": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@giphy/js-fetch-api": {
|
||||
"version": "5.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-5.8.0.tgz",
|
||||
"integrity": "sha512-DWdNauYaBSLPeAe/O+uzXtPIoUvmSnYV8Q49p+cPjEQ7Ncj1mDg1fuSKyl0OmPxCm6fzGJ4kuJjsWzyWn9ubbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@giphy/js-types": "*",
|
||||
"@giphy/js-util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@giphy/js-types": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-5.1.0.tgz",
|
||||
"integrity": "sha512-BZYCDtYNRR7cUWkbDLB4wmm3qmWMsVCQdUiBNOfmZ3yAazCgygKJoDI/5Rq4CK5MBaOc5LVdF8viC2WtoBdaPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@giphy/js-util": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-5.2.0.tgz",
|
||||
"integrity": "sha512-Qt7pGh2cqiNmXLeWAgb459wK8+BuMLtIxTfg4ZksnPHPsLthiHT9hhzs2QhqUh7Pp/HOq+Cbv2etGDfnq+xiKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@giphy/js-types": "*",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@giphy/js-util/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@giphy/react-components": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/react-components/-/react-components-10.1.2.tgz",
|
||||
"integrity": "sha512-DVyIgf5az0uzHwnkS8tUaqO4qNx8DoxJupnRLhOl8M5CcvV9ykeZBLDxLj+jyDTySPYaIIJtrlR+8SeV2dHZwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@giphy/colors": "*",
|
||||
"@giphy/js-analytics": "*",
|
||||
"@giphy/js-fetch-api": "*",
|
||||
"@giphy/js-types": "*",
|
||||
"@giphy/js-util": "*",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"react-use": "17.6.0",
|
||||
"throttle-debounce": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "18 - 19",
|
||||
"styled-components": ">= 5"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
@@ -5613,6 +5710,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/js-cookie": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
|
||||
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -6038,6 +6141,12 @@
|
||||
"vite": "^4.2.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xobotyi/scrollbar-width": {
|
||||
"version": "1.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
|
||||
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@@ -6200,6 +6309,15 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/append-query": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/append-query/-/append-query-2.1.1.tgz",
|
||||
"integrity": "sha512-adm0E8o1o7ay+HbkWvGIpNNeciLB/rxJ0heThHuzSSVq5zcdQ5/ZubFnUoY0imFmk6gZVghSpwoubLVtwi9EHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
@@ -6780,6 +6898,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelize": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001692",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
|
||||
@@ -7394,6 +7521,15 @@
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"node_modules/copy-to-clipboard": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
|
||||
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.40.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz",
|
||||
@@ -7513,6 +7649,57 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/css-in-js-utils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
|
||||
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hyphenate-style-name": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/css-to-react-native": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.0",
|
||||
"css-color-keywords": "^1.0.0",
|
||||
"postcss-value-parser": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.14",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz",
|
||||
@@ -7536,9 +7723,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cz-conventional-changelog": {
|
||||
"version": "3.3.0",
|
||||
@@ -8296,6 +8484,15 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-stack-parser": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
||||
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stackframe": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-abstract": {
|
||||
"version": "1.23.9",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
|
||||
@@ -9091,6 +9288,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/external-editor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
||||
@@ -9126,8 +9329,7 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
@@ -9169,6 +9371,11 @@
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-shallow-equal": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
|
||||
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz",
|
||||
@@ -9185,6 +9392,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/fastest-stable-stringify": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
|
||||
"integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||
@@ -10263,6 +10476,12 @@
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/hyphenate-style-name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
||||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "23.12.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.2.tgz",
|
||||
@@ -10472,6 +10691,15 @@
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
|
||||
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
|
||||
},
|
||||
"node_modules/inline-style-prefixer": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
|
||||
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-in-js-utils": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "8.2.5",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz",
|
||||
@@ -10539,6 +10767,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/intersection-observer": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
|
||||
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
|
||||
"deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "10.7.12",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.12.tgz",
|
||||
@@ -11152,6 +11387,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
|
||||
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-sdsl": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz",
|
||||
@@ -12152,6 +12393,12 @@
|
||||
"events": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/media-query-parser": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz",
|
||||
@@ -12402,6 +12649,26 @@
|
||||
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nano-css": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz",
|
||||
"integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"css-tree": "^1.1.2",
|
||||
"csstype": "^3.1.2",
|
||||
"fastest-stable-stringify": "^2.0.2",
|
||||
"inline-style-prefixer": "^7.0.1",
|
||||
"rtl-css-js": "^1.16.1",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
"stylis": "^4.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@@ -15305,6 +15572,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -15671,6 +15944,41 @@
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-universal-interface": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
|
||||
"integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"tslib": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use": {
|
||||
"version": "17.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz",
|
||||
"integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
"@xobotyi/scrollbar-width": "^1.9.5",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-shallow-equal": "^1.0.0",
|
||||
"js-cookie": "^2.2.1",
|
||||
"nano-css": "^5.6.2",
|
||||
"react-universal-interface": "^0.6.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"screenfull": "^5.1.0",
|
||||
"set-harmonic-interval": "^1.0.1",
|
||||
"throttle-debounce": "^3.0.1",
|
||||
"ts-easing": "^0.2.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/read-package-up": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz",
|
||||
@@ -15963,6 +16271,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -16099,6 +16413,15 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rtl-css-js": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
|
||||
"integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/run-async": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||
@@ -16244,6 +16567,18 @@
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/screenfull": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
|
||||
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
|
||||
@@ -16600,6 +16935,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/set-harmonic-interval": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz",
|
||||
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==",
|
||||
"license": "Unlicense",
|
||||
"engines": {
|
||||
"node": ">=6.9"
|
||||
}
|
||||
},
|
||||
"node_modules/set-proto": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
|
||||
@@ -17105,6 +17449,51 @@
|
||||
"through2": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-generator": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
|
||||
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stackframe": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/stackframe": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
||||
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stacktrace-gps": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
|
||||
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"source-map": "0.5.6",
|
||||
"stackframe": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/stacktrace-gps/node_modules/source-map": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
|
||||
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stacktrace-js": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
|
||||
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"error-stack-parser": "^2.0.6",
|
||||
"stack-generator": "^2.0.5",
|
||||
"stacktrace-gps": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-combiner2": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",
|
||||
@@ -17360,6 +17749,54 @@
|
||||
"inline-style-parser": "0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.1.tgz",
|
||||
"integrity": "sha512-ADu2dF53esUzzM4I0ewxhxFtsDd6v4V6dNkg3vG0iFKhnt06sJneTZnRvujAosZwW0XD58IKgGMQoqri4wHRqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/is-prop-valid": "1.4.0",
|
||||
"css-to-react-native": "3.2.0",
|
||||
"csstype": "3.2.3",
|
||||
"stylis": "4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/styled-components"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"css-to-react-native": ">= 3.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"react-dom": ">= 16.8.0",
|
||||
"react-native": ">= 0.68.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-to-react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components/node_modules/stylis": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
|
||||
"integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/super-regex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz",
|
||||
@@ -17533,6 +17970,15 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
|
||||
"integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
@@ -17665,6 +18111,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@@ -17683,6 +18135,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-easing": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
|
||||
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
|
||||
@@ -49,7 +49,7 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
load().catch(() => {});
|
||||
}, [load]);
|
||||
|
||||
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
||||
|
||||
@@ -68,6 +68,8 @@ function normaliseToTopLeft(el: HTMLElement) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
el.style.left = `${rect.left}px`;
|
||||
el.style.top = `${rect.top}px`;
|
||||
el.style.width = `${rect.width}px`;
|
||||
el.style.height = `${rect.height}px`;
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
}
|
||||
@@ -126,6 +128,13 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
||||
}
|
||||
}, [playSound, info.notificationType]);
|
||||
|
||||
useEffect(() => {
|
||||
const remaining = info.senderTs + info.lifetime - Date.now();
|
||||
if (remaining <= 0) { onIgnore(); return; }
|
||||
const id = setTimeout(onIgnore, remaining);
|
||||
return () => clearTimeout(id);
|
||||
}, [info.senderTs, info.lifetime, onIgnore]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
@@ -177,7 +186,7 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
||||
Your homeserver does not support calling.
|
||||
</Text>
|
||||
)}
|
||||
{!webRTCSupported && (
|
||||
{!webRTCSupported() && (
|
||||
<Text
|
||||
style={{ margin: 'auto', color: color.Critical.Main }}
|
||||
size="L400"
|
||||
@@ -396,6 +405,8 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
const pipDragRef = React.useRef<{
|
||||
startX: number; startY: number; origLeft: number; origTop: number; dragged: boolean;
|
||||
} | null>(null);
|
||||
const activeDragCleanupRef = React.useRef<(() => void) | null>(null);
|
||||
React.useEffect(() => () => { activeDragCleanupRef.current?.(); }, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = callEmbedRef.current;
|
||||
@@ -427,10 +438,41 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
el.style.right = 'auto'; el.style.bottom = 'auto';
|
||||
}
|
||||
};
|
||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0); };
|
||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; activeDragCleanupRef.current = null; setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0); };
|
||||
activeDragCleanupRef.current = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; };
|
||||
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
const handlePipTouchStart = (e: React.TouchEvent) => {
|
||||
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) {
|
||||
el.style.left = `${Math.max(0, Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx))}px`;
|
||||
el.style.top = `${Math.max(0, Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy))}px`;
|
||||
el.style.right = 'auto'; el.style.bottom = 'auto';
|
||||
}
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
activeDragCleanupRef.current = null;
|
||||
setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0);
|
||||
};
|
||||
activeDragCleanupRef.current = () => { document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); };
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
|
||||
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
|
||||
e.stopPropagation(); e.preventDefault();
|
||||
const el = callEmbedRef.current; if (!el) return;
|
||||
@@ -447,7 +489,8 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
l=Math.max(0,Math.min(l,window.innerWidth-PIP_MIN_W)); t=Math.max(0,Math.min(t,window.innerHeight-PIP_MIN_H));
|
||||
el.style.width=`${w}px`; el.style.height=`${h}px`; el.style.left=`${l}px`; el.style.top=`${t}px`;
|
||||
};
|
||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; };
|
||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; activeDragCleanupRef.current = null; };
|
||||
activeDragCleanupRef.current = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; };
|
||||
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
@@ -477,6 +520,7 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
tabIndex={0}
|
||||
aria-label="Return to call"
|
||||
onMouseDown={handlePipMouseDown}
|
||||
onTouchStart={handlePipTouchStart}
|
||||
onClick={() => { if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId); }}
|
||||
onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
|
||||
style={{ position:'absolute', inset:0, zIndex:1, background:'transparent', cursor:'grab', display:'flex', alignItems:'flex-start', justifyContent:'flex-end', padding:'6px' }}
|
||||
@@ -485,10 +529,20 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
↗ Return to call
|
||||
</div>
|
||||
</div>
|
||||
{(['se','sw','ne','nw'] as Corner[]).map((corner) => (
|
||||
<div key={corner} onMouseDown={(e) => handleResizeMouseDown(e, corner)} onClick={(e) => e.stopPropagation()}
|
||||
style={{ position:'absolute', width:'18px', height:'18px', [corner.includes('s')?'bottom':'top']:0, [corner.includes('e')?'right':'left']:0, cursor:`${corner}-resize`, zIndex:2 }} />
|
||||
))}
|
||||
{(['se','sw','ne','nw'] as Corner[]).map((corner) => {
|
||||
const s = corner.includes('s'); const e2 = corner.includes('e');
|
||||
const dots = [[2,2],[2,7],[7,2]].map(([a,b]) => ({
|
||||
position:'absolute' as const, width:4, height:4, borderRadius:'50%',
|
||||
background:'rgba(255,255,255,0.45)',
|
||||
[s?'bottom':'top']:a, [e2?'right':'left']:b,
|
||||
}));
|
||||
return (
|
||||
<div key={corner} onMouseDown={(ev) => handleResizeMouseDown(ev, corner)} onClick={(ev) => ev.stopPropagation()}
|
||||
style={{ position:'absolute', width:'18px', height:'18px', [s?'bottom':'top']:0, [e2?'right':'left']:0, cursor:`${corner}-resize`, zIndex:2 }}>
|
||||
{dots.map((style, i) => <div key={i} style={style} />)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
|
||||
const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(), [mx]));
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
load().catch(() => {});
|
||||
}, [load]);
|
||||
|
||||
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { Avatar, Box, Button, Text } from 'folds';
|
||||
import { UserAvatar } from './user-avatar';
|
||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||
import { useCallStart } from '../hooks/useCallEmbed';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useIncomingDmCall } from '../hooks/useIncomingDmCall';
|
||||
import { mxcUrlToHttp } from '../utils/matrix';
|
||||
|
||||
function useRingTone(active: boolean) {
|
||||
const stopRef = useRef<(() => void) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
stopRef.current?.();
|
||||
stopRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx: AudioContext;
|
||||
try {
|
||||
ctx = new AudioContext();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const pulse = (t: number, freq: number, dur: number) => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.frequency.value = freq;
|
||||
osc.type = 'sine';
|
||||
gain.gain.setValueAtTime(0, t);
|
||||
gain.gain.linearRampToValueAtTime(0.22, t + 0.04);
|
||||
gain.gain.setValueAtTime(0.22, t + dur - 0.04);
|
||||
gain.gain.linearRampToValueAtTime(0, t + dur);
|
||||
osc.start(t);
|
||||
osc.stop(t + dur);
|
||||
};
|
||||
|
||||
let ringTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const ring = () => {
|
||||
if (cancelled) return;
|
||||
const now = ctx.currentTime;
|
||||
pulse(now, 880, 0.18);
|
||||
pulse(now + 0.28, 880, 0.18);
|
||||
ringTimer = setTimeout(ring, 2200);
|
||||
};
|
||||
|
||||
ring();
|
||||
|
||||
const stop = () => {
|
||||
cancelled = true;
|
||||
if (ringTimer !== null) clearTimeout(ringTimer);
|
||||
ctx.close();
|
||||
};
|
||||
|
||||
stopRef.current = stop;
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
stopRef.current = null;
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
return stopRef;
|
||||
}
|
||||
|
||||
function IncomingCallCard() {
|
||||
const mx = useMatrixClient();
|
||||
const startCall = useCallStart(true);
|
||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
const [incoming, dismiss] = useIncomingDmCall();
|
||||
const stopRingRef = useRingTone(!!incoming);
|
||||
|
||||
const stopAndDismiss = useCallback(() => {
|
||||
stopRingRef.current?.();
|
||||
dismiss();
|
||||
}, [dismiss, stopRingRef]);
|
||||
|
||||
const handleAnswer = useCallback(() => {
|
||||
if (!incoming) return;
|
||||
stopRingRef.current?.();
|
||||
startCall(incoming.room);
|
||||
dismiss();
|
||||
}, [incoming, startCall, dismiss, stopRingRef]);
|
||||
|
||||
if (!incoming) return null;
|
||||
|
||||
const callerUser = mx.getUser(incoming.callerId);
|
||||
const callerName = callerUser?.displayName ?? incoming.callerId;
|
||||
const avatarMxc = callerUser?.avatarUrl;
|
||||
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, undefined, 64, 64, 'crop') ?? undefined : undefined;
|
||||
|
||||
if (lotusTerminal) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '80px',
|
||||
right: '24px',
|
||||
zIndex: 9999,
|
||||
background: '#060c14',
|
||||
border: '1px solid rgba(255,107,0,0.45)',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 4px 32px rgba(255,107,0,0.18), 0 0 0 1px rgba(255,107,0,0.08)',
|
||||
padding: '16px',
|
||||
width: '272px',
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#00D4FF',
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '7px',
|
||||
height: '7px',
|
||||
borderRadius: '50%',
|
||||
background: '#00FF88',
|
||||
boxShadow: '0 0 6px #00FF88',
|
||||
animation: 'lotus-ring-pulse 1s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
INCOMING CALL
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px' }}>
|
||||
<Avatar size="300" style={{ border: '1px solid rgba(0,212,255,0.3)', borderRadius: '2px', overflow: 'hidden' }}>
|
||||
<UserAvatar
|
||||
userId={incoming.callerId}
|
||||
src={avatarUrl}
|
||||
alt={callerName}
|
||||
renderFallback={() => (
|
||||
<span style={{ fontSize: '16px', color: '#00D4FF', fontFamily: 'JetBrains Mono, monospace' }}>
|
||||
{callerName[0]?.toUpperCase() ?? '?'}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', color: '#e8edf5', fontWeight: 600 }}>{callerName}</div>
|
||||
<div style={{ fontSize: '10px', color: 'rgba(232,237,245,0.45)', marginTop: '2px' }}>
|
||||
{incoming.callerId}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={handleAnswer}
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'transparent',
|
||||
border: '1px solid #00FF88',
|
||||
borderRadius: '3px',
|
||||
color: '#00FF88',
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
padding: '9px 4px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 0 10px rgba(0,255,136,0.2)',
|
||||
transition: 'box-shadow 0.15s, background 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(0,255,136,0.08)';
|
||||
(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 16px rgba(0,255,136,0.35)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'transparent';
|
||||
(e.currentTarget as HTMLButtonElement).style.boxShadow = '0 0 10px rgba(0,255,136,0.2)';
|
||||
}}
|
||||
>
|
||||
ANSWER
|
||||
</button>
|
||||
<button
|
||||
onClick={stopAndDismiss}
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'transparent',
|
||||
border: '1px solid rgba(255,107,0,0.5)',
|
||||
borderRadius: '3px',
|
||||
color: '#FF6B00',
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
padding: '9px 4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'opacity 0.15s, background 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,107,0,0.08)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'transparent';
|
||||
}}
|
||||
>
|
||||
DECLINE
|
||||
</button>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes lotus-ring-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.7); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '80px',
|
||||
right: '24px',
|
||||
zIndex: 9999,
|
||||
background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--bg-surface-border)',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 8px 40px rgba(0,0,0,0.45)',
|
||||
padding: '16px',
|
||||
width: '272px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-secondary)',
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '7px',
|
||||
height: '7px',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--clr-success-main)',
|
||||
animation: 'lotus-ring-pulse 1s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
Incoming call
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '14px' }}>
|
||||
<Avatar size="300">
|
||||
<UserAvatar
|
||||
userId={incoming.callerId}
|
||||
src={avatarUrl}
|
||||
alt={callerName}
|
||||
renderFallback={() => (
|
||||
<Text size="H4">{callerName[0]?.toUpperCase() ?? '?'}</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
<div>
|
||||
<Text size="T400" style={{ fontWeight: 600 }}>{callerName}</Text>
|
||||
<Text size="T200" style={{ opacity: 0.5 }}>{incoming.callerId}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Box direction="Row" gap="200">
|
||||
<Button
|
||||
onClick={handleAnswer}
|
||||
variant="Success"
|
||||
fill="Solid"
|
||||
size="400"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Text size="B400">Answer</Text>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={stopAndDismiss}
|
||||
variant="Critical"
|
||||
fill="Solid"
|
||||
size="400"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Text size="B400">Decline</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
<style>{`
|
||||
@keyframes lotus-ring-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.7); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function IncomingCallNotification() {
|
||||
return <IncomingCallCard />;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function MediaConfigLoader({ children }: MediaConfigLoaderProps) {
|
||||
const [state, load] = useAsyncCallback(useCallback(() => mx.getMediaConfig(), [mx]));
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
load().catch(() => {});
|
||||
}, [load]);
|
||||
|
||||
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
||||
|
||||
@@ -22,7 +22,7 @@ export function SpecVersionsLoader({
|
||||
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
load().catch(() => {});
|
||||
}, [load]);
|
||||
|
||||
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
||||
|
||||
@@ -393,6 +393,7 @@ export function MLocation({ content }: MLocationProps) {
|
||||
|
||||
const lat = parseFloat(location.latitude);
|
||||
const lon = parseFloat(location.longitude);
|
||||
if (!isFinite(lat) || !isFinite(lon)) return <BrokenContent />;
|
||||
const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${lat - 0.004},${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,11 +8,7 @@ export const MessageDeletedContent = as<'div', { children?: never; reason?: stri
|
||||
({ reason, ...props }, ref) => (
|
||||
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
|
||||
<Icon size="50" src={Icons.Delete} />
|
||||
{reason ? (
|
||||
<i>This message has been deleted. {reason}</i>
|
||||
) : (
|
||||
<i>This message has been deleted</i>
|
||||
)}
|
||||
<i>{reason ? `This message has been deleted — ${reason}` : 'This message has been deleted'}</i>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Box, Text } from 'folds';
|
||||
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
|
||||
type PollTextValue = Array<{ body: string }> | string;
|
||||
@@ -22,6 +23,75 @@ type PollData = {
|
||||
answers?: PollAnswer[];
|
||||
};
|
||||
|
||||
type VoteState = {
|
||||
counts: Map<string, number>;
|
||||
myVote: string | null;
|
||||
total: number;
|
||||
};
|
||||
|
||||
function computeVotes(
|
||||
mx: ReturnType<typeof useMatrixClient>,
|
||||
roomId: string,
|
||||
eventId: string,
|
||||
isStable: boolean
|
||||
): VoteState {
|
||||
const empty: VoteState = { counts: new Map(), myVote: null, total: 0 };
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return empty;
|
||||
|
||||
const timelineSet = room.getUnfilteredTimelineSet();
|
||||
const stableRels = timelineSet.relations.getChildEventsForEvent(
|
||||
eventId,
|
||||
'm.reference',
|
||||
'm.poll.response'
|
||||
);
|
||||
const unstableRels = timelineSet.relations.getChildEventsForEvent(
|
||||
eventId,
|
||||
'org.matrix.msc3381.poll.response' as any,
|
||||
'org.matrix.msc3381.poll.response'
|
||||
);
|
||||
|
||||
// Collect all response events; per-sender keep only latest
|
||||
const latestBySender = new Map<string, { ts: number; answerId: string }>();
|
||||
const myUserId = mx.getSafeUserId();
|
||||
|
||||
const processRelations = (rels: typeof stableRels, stable: boolean) => {
|
||||
const events = rels?.getRelations() ?? [];
|
||||
for (const ev of events) {
|
||||
if (ev.isRedacted()) continue;
|
||||
const sender = ev.getSender();
|
||||
if (!sender) continue;
|
||||
const content = ev.getContent();
|
||||
let answerId: string | undefined;
|
||||
if (stable) {
|
||||
answerId = (content['m.selections'] as string[] | undefined)?.[0];
|
||||
} else {
|
||||
answerId = (
|
||||
(content['org.matrix.msc3381.poll.response'] as any)?.answers as string[] | undefined
|
||||
)?.[0];
|
||||
}
|
||||
if (!answerId) continue;
|
||||
const ts = ev.getTs();
|
||||
const existing = latestBySender.get(sender);
|
||||
if (!existing || ts > existing.ts) {
|
||||
latestBySender.set(sender, { ts, answerId });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processRelations(stableRels, true);
|
||||
processRelations(unstableRels, false);
|
||||
|
||||
const counts = new Map<string, number>();
|
||||
let myVote: string | null = null;
|
||||
for (const [sender, { answerId }] of latestBySender) {
|
||||
counts.set(answerId, (counts.get(answerId) ?? 0) + 1);
|
||||
if (sender === myUserId) myVote = answerId;
|
||||
}
|
||||
|
||||
return { counts, myVote, total: latestBySender.size };
|
||||
}
|
||||
|
||||
export function PollContent({
|
||||
content,
|
||||
roomId,
|
||||
@@ -32,13 +102,56 @@ export function PollContent({
|
||||
eventId?: string;
|
||||
}) {
|
||||
const mx = useMatrixClient();
|
||||
const [myVote, setMyVote] = useState<string | null>(null);
|
||||
const isStable = !!content['m.poll'];
|
||||
|
||||
const poll = (
|
||||
content['m.poll'] ?? content['org.matrix.msc3381.poll.start']
|
||||
) as PollData | undefined;
|
||||
|
||||
const [votes, setVotes] = useState<VoteState>(() => {
|
||||
if (!roomId || !eventId) return { counts: new Map(), myVote: null, total: 0 };
|
||||
return computeVotes(mx, roomId, eventId, isStable);
|
||||
});
|
||||
|
||||
// Refresh votes whenever Relations events fire
|
||||
const refresh = useCallback(() => {
|
||||
if (!roomId || !eventId) return;
|
||||
setVotes(computeVotes(mx, roomId, eventId, isStable));
|
||||
}, [mx, roomId, eventId, isStable]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomId || !eventId) return;
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return;
|
||||
const timelineSet = room.getUnfilteredTimelineSet();
|
||||
|
||||
const stableRels = timelineSet.relations.getChildEventsForEvent(
|
||||
eventId,
|
||||
'm.reference',
|
||||
'm.poll.response'
|
||||
);
|
||||
const unstableRels = timelineSet.relations.getChildEventsForEvent(
|
||||
eventId,
|
||||
'org.matrix.msc3381.poll.response' as any,
|
||||
'org.matrix.msc3381.poll.response'
|
||||
);
|
||||
|
||||
stableRels?.on(RelationsEvent.Add, refresh);
|
||||
stableRels?.on(RelationsEvent.Remove, refresh);
|
||||
stableRels?.on(RelationsEvent.Redaction, refresh);
|
||||
unstableRels?.on(RelationsEvent.Add, refresh);
|
||||
unstableRels?.on(RelationsEvent.Remove, refresh);
|
||||
unstableRels?.on(RelationsEvent.Redaction, refresh);
|
||||
return () => {
|
||||
stableRels?.off(RelationsEvent.Add, refresh);
|
||||
stableRels?.off(RelationsEvent.Remove, refresh);
|
||||
stableRels?.off(RelationsEvent.Redaction, refresh);
|
||||
unstableRels?.off(RelationsEvent.Add, refresh);
|
||||
unstableRels?.off(RelationsEvent.Remove, refresh);
|
||||
unstableRels?.off(RelationsEvent.Redaction, refresh);
|
||||
};
|
||||
}, [mx, roomId, eventId, refresh]);
|
||||
|
||||
if (!poll) {
|
||||
return (
|
||||
<Text style={{ opacity: 0.6 }}>
|
||||
@@ -53,14 +166,25 @@ export function PollContent({
|
||||
'Untitled poll';
|
||||
|
||||
const canVote = !!roomId && !!eventId;
|
||||
const { counts, myVote, total } = votes;
|
||||
|
||||
const handleVote = (answerId: string) => {
|
||||
if (!roomId || !eventId) return;
|
||||
setMyVote(answerId);
|
||||
// Optimistic local update
|
||||
setVotes((prev) => {
|
||||
const next = new Map(prev.counts);
|
||||
if (prev.myVote) {
|
||||
const prevCount = next.get(prev.myVote) ?? 1;
|
||||
if (prevCount <= 1) next.delete(prev.myVote);
|
||||
else next.set(prev.myVote, prevCount - 1);
|
||||
}
|
||||
next.set(answerId, (next.get(answerId) ?? 0) + 1);
|
||||
return { counts: next, myVote: answerId, total: prev.myVote ? prev.total : prev.total + 1 };
|
||||
});
|
||||
if (isStable) {
|
||||
mx.sendEvent(roomId, 'm.poll.response' as any, {
|
||||
'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
|
||||
'm.responses': [{ 'm.id': answerId }],
|
||||
'm.selections': [answerId],
|
||||
});
|
||||
} else {
|
||||
mx.sendEvent(roomId, 'org.matrix.msc3381.poll.response' as any, {
|
||||
@@ -70,6 +194,12 @@ export function PollContent({
|
||||
}
|
||||
};
|
||||
|
||||
const answers = poll.answers ?? [];
|
||||
const maxVotes = answers.reduce((m, a, i) => {
|
||||
const id = a['m.id'] ?? a.id ?? String(i);
|
||||
return Math.max(m, counts.get(id) ?? 0);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Box
|
||||
direction="Column"
|
||||
@@ -94,13 +224,15 @@ export function PollContent({
|
||||
{questionText}
|
||||
</Text>
|
||||
<Box direction="Column" gap="100" style={{ marginTop: '2px' }}>
|
||||
{(poll.answers ?? []).map((answer, i) => {
|
||||
{answers.map((answer, i) => {
|
||||
const text =
|
||||
extractText((answer as any)['m.text']) ||
|
||||
(answer as any)['org.matrix.msc3381.poll.answer']?.body ||
|
||||
`Option ${i + 1}`;
|
||||
const id = answer['m.id'] ?? answer.id ?? String(i);
|
||||
const selected = myVote === id;
|
||||
const voteCount = counts.get(id) ?? 0;
|
||||
const pct = total > 0 ? Math.round((voteCount / total) * 100) : 0;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
@@ -117,28 +249,54 @@ export function PollContent({
|
||||
cursor: canVote ? 'pointer' : 'default',
|
||||
color: 'var(--text-primary)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span style={{ flexGrow: 1 }}>{text}</span>
|
||||
{selected && (
|
||||
<span style={{ opacity: 0.8, fontSize: '1rem', flexShrink: 0 }}>✓</span>
|
||||
{/* vote progress bar */}
|
||||
{total > 0 && (
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: `${pct}%`,
|
||||
background: selected
|
||||
? 'rgba(255,255,255,0.10)'
|
||||
: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '8px',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '8px', position: 'relative' }}>
|
||||
<span style={{ flexGrow: 1 }}>{text}</span>
|
||||
{selected && (
|
||||
<span style={{ opacity: 0.8, fontSize: '1rem', flexShrink: 0 }}>✓</span>
|
||||
)}
|
||||
{total > 0 && (
|
||||
<span style={{ opacity: 0.6, fontSize: '0.78rem', flexShrink: 0 }}>
|
||||
{pct}%
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{canVote ? (
|
||||
<Text size="T200" style={{ opacity: 0.5, marginTop: '2px' }}>
|
||||
<i>{myVote ? 'Vote cast — click another to change' : 'Click an option to vote'}</i>
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="T200" style={{ opacity: 0.4, marginTop: '2px' }}>
|
||||
<i>Open in Element to vote</i>
|
||||
</Text>
|
||||
)}
|
||||
<Text size="T200" style={{ opacity: 0.5, marginTop: '2px' }}>
|
||||
<i>
|
||||
{total > 0 ? `${total} vote${total === 1 ? '' : 's'} — ` : ''}
|
||||
{canVote
|
||||
? myVote
|
||||
? 'click another to change'
|
||||
: 'click an option to vote'
|
||||
: 'voting not available'}
|
||||
</i>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { Avatar, Icon, Icons, Modal, Overlay, OverlayBackdrop, OverlayCenter, Text } from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { getMemberDisplayName } from '../../utils/room';
|
||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { UserAvatar } from '../user-avatar';
|
||||
import { EventReaders } from '../event-readers';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
||||
const MAX_DISPLAY = 5;
|
||||
|
||||
export function ReadReceiptAvatars({
|
||||
room,
|
||||
eventId,
|
||||
userIds,
|
||||
}: {
|
||||
room: Room;
|
||||
eventId: string;
|
||||
userIds: string[];
|
||||
}) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (userIds.length === 0) return null;
|
||||
|
||||
const displayed = userIds.slice(0, MAX_DISPLAY);
|
||||
const extra = userIds.length - MAX_DISPLAY;
|
||||
const tooltipNames = userIds
|
||||
.slice(0, 5)
|
||||
.map((id) => getMemberDisplayName(room, id) ?? getMxIdLocalPart(id) ?? id)
|
||||
.join(', ') + (extra > 0 ? ` +${extra} more` : '');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Overlay open={open} backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setOpen(false),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal variant="Surface" size="300">
|
||||
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
|
||||
</Modal>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
title={tooltipNames}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '1px 0 0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{displayed.map((userId, i) => {
|
||||
const name =
|
||||
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
|
||||
const avatarMxc = room.getMember(userId)?.getMxcAvatarUrl();
|
||||
const avatarUrl = avatarMxc
|
||||
? mx.mxcUrlToHttp(avatarMxc, 32, 32, 'crop', undefined, false, useAuthentication) ??
|
||||
undefined
|
||||
: undefined;
|
||||
return (
|
||||
<span
|
||||
key={userId}
|
||||
title={name}
|
||||
style={{
|
||||
marginLeft: i === 0 ? 0 : -5,
|
||||
display: 'block',
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: '50%',
|
||||
border: '1.5px solid var(--bg-surface)',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Avatar size="100">
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
{extra > 0 && (
|
||||
<Text size="T100" style={{ opacity: 0.6 }}>
|
||||
+{extra}
|
||||
</Text>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ReadReceiptAvatars } from './ReadReceiptAvatars';
|
||||
@@ -28,7 +28,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadPreview();
|
||||
loadPreview().catch(() => {});
|
||||
}, [loadPreview]);
|
||||
|
||||
if (previewStatus.status === AsyncStatus.Error) return null;
|
||||
|
||||
@@ -239,7 +239,6 @@ export function MutualRoomsChip({ userId }: { userId: string }) {
|
||||
const mutualRoomSupported = useMutualRoomsSupport();
|
||||
const mutualRoomUnstable = useUnstableMutualRoomsSupport();
|
||||
const mutualRoomsState = useMutualRooms(userId);
|
||||
console.log(mutualRoomSupported, mutualRoomsState);
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const closeUserRoomProfile = useCloseUserRoomProfile();
|
||||
const directs = useDirectRooms();
|
||||
|
||||
@@ -102,11 +102,19 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
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;
|
||||
// Skip PTT if key is pressed inside any text-input or editable surface
|
||||
const isEditable = (el: HTMLElement): boolean => {
|
||||
const tag = el.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
||||
let node: HTMLElement | null = el;
|
||||
while (node && node !== document.body) {
|
||||
if (node.contentEditable === 'true') return true;
|
||||
if (node.contentEditable === 'false') return false;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (isEditable(target)) return;
|
||||
e.preventDefault();
|
||||
if (!microphoneRef.current) callEmbed.control.setMicrophone(true);
|
||||
setPttActive(true);
|
||||
@@ -227,7 +235,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
<Text size="B300">Share</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300" variant="Surface" fill="Soft" radii="300" outlined
|
||||
size="300" variant="Secondary" fill="Soft" radii="300" outlined
|
||||
onClick={() => setShareConfirm(false)}
|
||||
>
|
||||
<Text size="B300">Cancel</Text>
|
||||
|
||||
@@ -81,15 +81,18 @@ export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
|
||||
type VideoButtonProps = {
|
||||
enabled: boolean;
|
||||
onToggle: () => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
|
||||
export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||
return (
|
||||
<TooltipProvider
|
||||
position="Top"
|
||||
delay={500}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text size="T200">{enabled ? 'Stop Camera' : 'Start Camera'}</Text>
|
||||
<Text size="T200">
|
||||
{disabled ? 'Camera disabled in settings' : enabled ? 'Stop Camera' : 'Start Camera'}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@@ -102,6 +105,8 @@ export function VideoButton({ enabled, onToggle }: VideoButtonProps) {
|
||||
size="400"
|
||||
onClick={() => onToggle()}
|
||||
outlined
|
||||
disabled={disabled}
|
||||
style={disabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
|
||||
>
|
||||
<Icon
|
||||
size="400"
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton
|
||||
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
|
||||
import { useCallPreferences } from '../../state/hooks/callPreferences';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
|
||||
type MediaPermState = 'granted' | 'denied' | 'prompt' | 'unknown';
|
||||
|
||||
@@ -50,6 +52,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
||||
|
||||
const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } =
|
||||
useCallPreferences();
|
||||
const [cameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
@@ -67,7 +70,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
||||
</Box>
|
||||
<ControlDivider />
|
||||
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
|
||||
<VideoButton enabled={video} onToggle={toggleVideo} />
|
||||
<VideoButton enabled={video} onToggle={toggleVideo} disabled={!cameraOnJoin} />
|
||||
<ChatButton />
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column" gap="200">
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export const ReadPositionsContext = createContext<Map<string, string[]>>(new Map());
|
||||
export const useReadPositions = () => useContext(ReadPositionsContext);
|
||||
@@ -457,8 +457,20 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
const handleGifSelect = useCallback(
|
||||
async (gifUrl: string, w: number, h: number) => {
|
||||
try {
|
||||
// Only fetch from trusted Giphy CDN domains
|
||||
const allowed = ['media.giphy.com', 'i.giphy.com', 'media0.giphy.com',
|
||||
'media1.giphy.com', 'media2.giphy.com', 'media3.giphy.com', 'media4.giphy.com'];
|
||||
const { hostname } = new URL(gifUrl);
|
||||
if (!allowed.includes(hostname)) return;
|
||||
|
||||
const res = await fetch(gifUrl);
|
||||
if (!res.ok) return;
|
||||
const contentType = res.headers.get('content-type') ?? '';
|
||||
if (!contentType.startsWith('image/')) return;
|
||||
|
||||
const blob = await res.blob();
|
||||
if (blob.size > 20 * 1024 * 1024) return; // 20 MB cap
|
||||
|
||||
const uploadRes = await mx.uploadContent(
|
||||
new File([blob], 'image.gif', { type: 'image/gif' }),
|
||||
{ type: 'image/gif', name: 'image.gif', includeFilename: false }
|
||||
|
||||
@@ -89,6 +89,8 @@ import { useSetting } from '../../state/hooks/settings';
|
||||
import { MessageLayout, settingsAtom } from '../../state/settings';
|
||||
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
||||
import { Reactions, Message, Event, EncryptedContent } from './message';
|
||||
import { ReadPositionsContext } from './ReadPositionsContext';
|
||||
import { useRoomReadPositions } from '../../hooks/useRoomReadPositions';
|
||||
import { useMemberEventParser } from '../../hooks/useMemberEventParser';
|
||||
import * as customHtmlCss from '../../styles/CustomHtml.css';
|
||||
import { RoomIntro } from '../../components/room-intro';
|
||||
@@ -441,6 +443,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
|
||||
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
|
||||
const direct = useIsDirectRoom();
|
||||
const readPositions = useRoomReadPositions(room);
|
||||
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
|
||||
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
@@ -1837,6 +1840,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
};
|
||||
|
||||
return (
|
||||
<ReadPositionsContext.Provider value={readPositions}>
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
|
||||
<TimelineFloat position="Top">
|
||||
@@ -1962,5 +1966,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
</TimelineFloat>
|
||||
)}
|
||||
</Box>
|
||||
</ReadPositionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import { useTheme, ThemeKind } from '../../hooks/useTheme';
|
||||
import { getChatBg } from '../lotus/chatBackground';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
import { editableActiveElement } from '../../utils/dom';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
|
||||
@@ -33,12 +33,12 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||
import { useCallStart } from '../../hooks/useCallEmbed';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
|
||||
import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { getStateEvents } from '../../utils/room';
|
||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||
import * as css from './RoomViewHeader.css';
|
||||
import { useRoomUnread } from '../../state/hooks/unread';
|
||||
@@ -405,7 +405,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const [pinMenuAnchor, setPinMenuAnchor] = useState<RectCords>();
|
||||
const direct = useIsDirectRoom();
|
||||
const startCall = useCallStart(true);
|
||||
|
||||
const pinnedEvents = useRoomPinnedEvents(room);
|
||||
const encryptionEvent = useStateEvent(room, StateEvent.RoomEncryption);
|
||||
@@ -522,23 +521,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
</Box>
|
||||
|
||||
<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 && (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
@@ -613,7 +595,9 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
{!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission && (
|
||||
{!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission &&
|
||||
(direct || (room.getJoinRule() === 'invite' &&
|
||||
getStateEvents(room, StateEvent.SpaceParent).length === 0)) && (
|
||||
<CallButton />
|
||||
)}
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
const forward = (roomId: string, roomName: string) => {
|
||||
const fwdContent: Record<string, unknown> = { ...mEvent.getContent() };
|
||||
delete fwdContent['m.relates_to'];
|
||||
mx.sendMessage(roomId, fwdContent as any);
|
||||
mx.sendEvent(roomId, mEvent.getType(), fwdContent as any);
|
||||
setSentTo(roomName);
|
||||
setTimeout(onClose, 1200);
|
||||
};
|
||||
|
||||
@@ -63,6 +63,8 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
import * as css from './styles.css';
|
||||
import { EventReaders } from '../../../components/event-readers';
|
||||
import { ReadReceiptAvatars } from '../../../components/read-receipt-avatars';
|
||||
import { useReadPositions } from '../ReadPositionsContext';
|
||||
import { TextViewer } from '../../../components/text-viewer';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { EmojiBoard } from '../../../components/emoji-board';
|
||||
@@ -721,6 +723,10 @@ export const Message = as<'div', MessageProps>(
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const readPositions = useReadPositions();
|
||||
const readReceiptUsers = hideReadReceipts
|
||||
? []
|
||||
: (readPositions.get(mEvent.getId() ?? '') ?? []);
|
||||
|
||||
const [hover, setHover] = useState(false);
|
||||
const { hoverProps } = useHover({ onHoverChange: setHover });
|
||||
@@ -833,6 +839,13 @@ export const Message = as<'div', MessageProps>(
|
||||
children
|
||||
)}
|
||||
{reactions}
|
||||
{readReceiptUsers.length > 0 && (
|
||||
<ReadReceiptAvatars
|
||||
room={room}
|
||||
eventId={mEvent.getId() ?? ''}
|
||||
userIds={readReceiptUsers}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import React, {
|
||||
FormEventHandler,
|
||||
KeyboardEventHandler,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -378,7 +380,7 @@ function Appearance() {
|
||||
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">
|
||||
<Box direction="Row" gap="200" alignItems="Center">
|
||||
{lotusTerminal && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -811,8 +813,18 @@ function Calls() {
|
||||
const [pttMode, setPttMode] = useSetting(settingsAtom, 'pttMode');
|
||||
const [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey');
|
||||
const [listeningForKey, setListeningForKey] = useState(false);
|
||||
const keyListenerRef = useRef<((e: KeyboardEvent) => void) | null>(null);
|
||||
|
||||
const handleKeyBind = () => {
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (keyListenerRef.current)
|
||||
window.removeEventListener('keydown', keyListenerRef.current, true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleKeyBind = useCallback(() => {
|
||||
if (listeningForKey) return;
|
||||
setListeningForKey(true);
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -823,9 +835,11 @@ function Calls() {
|
||||
setListeningForKey(false);
|
||||
}
|
||||
window.removeEventListener('keydown', onKey, true);
|
||||
keyListenerRef.current = null;
|
||||
};
|
||||
keyListenerRef.current = onKey;
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
};
|
||||
}, [listeningForKey, setPttKey]);
|
||||
|
||||
const keyLabel = (code: string) => code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', '');
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import {
|
||||
MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
|
||||
import { MatrixRTCSessionManagerEvents } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSessionManager';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { callEmbedAtom } from '../state/callEmbed';
|
||||
|
||||
export type IncomingDmCall = {
|
||||
room: Room;
|
||||
callerId: string;
|
||||
};
|
||||
|
||||
const isDmRoom = (room: Room): boolean =>
|
||||
room.hasEncryptionStateEvent() && room.getMembers().length <= 2;
|
||||
|
||||
export const useIncomingDmCall = (): [IncomingDmCall | null, () => void] => {
|
||||
const mx = useMatrixClient();
|
||||
const callEmbed = useAtomValue(callEmbedAtom);
|
||||
const [incoming, setIncoming] = useState<IncomingDmCall | null>(null);
|
||||
const sessionRef = useRef<MatrixRTCSession | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const myUserId = mx.getUserId();
|
||||
|
||||
const handleSessionStarted = (roomId: string, session: MatrixRTCSession) => {
|
||||
if (callEmbed) return;
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room || !isDmRoom(room)) return;
|
||||
const memberships = session.memberships ?? [];
|
||||
if (memberships.length === 0) return;
|
||||
const callerMembership = memberships.find((m) => m.sender !== myUserId);
|
||||
if (!callerMembership) return;
|
||||
if (memberships.find((m) => m.sender === myUserId)) return;
|
||||
sessionRef.current = session;
|
||||
setIncoming({ room, callerId: callerMembership.sender ?? callerMembership.deviceId });
|
||||
};
|
||||
|
||||
const handleSessionEnded = (roomId: string) => {
|
||||
setIncoming((prev) => (prev?.room.roomId === roomId ? null : prev));
|
||||
};
|
||||
|
||||
mx.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, handleSessionStarted);
|
||||
mx.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, handleSessionEnded);
|
||||
return () => {
|
||||
mx.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, handleSessionStarted);
|
||||
mx.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, handleSessionEnded);
|
||||
};
|
||||
}, [mx, callEmbed]);
|
||||
|
||||
// Dismiss if caller leaves before answered
|
||||
useEffect(() => {
|
||||
const session = sessionRef.current;
|
||||
if (!session || !incoming) return;
|
||||
const myUserId = mx.getUserId();
|
||||
const check = () => {
|
||||
const memberships = session.memberships ?? [];
|
||||
if (!memberships.some((m) => m.sender !== myUserId)) setIncoming(null);
|
||||
};
|
||||
session.on(MatrixRTCSessionEvent.MembershipsChanged, check);
|
||||
return () => {
|
||||
session.off(MatrixRTCSessionEvent.MembershipsChanged, check);
|
||||
};
|
||||
}, [incoming, mx]);
|
||||
|
||||
// Auto-dismiss after 30 seconds
|
||||
useEffect(() => {
|
||||
if (!incoming) return;
|
||||
const t = setTimeout(() => setIncoming(null), 30_000);
|
||||
return () => clearTimeout(t);
|
||||
}, [incoming]);
|
||||
|
||||
// Dismiss when user joins a call
|
||||
useEffect(() => {
|
||||
if (callEmbed) setIncoming(null);
|
||||
}, [callEmbed]);
|
||||
|
||||
const dismiss = () => {
|
||||
sessionRef.current = null;
|
||||
setIncoming(null);
|
||||
};
|
||||
|
||||
return [incoming, dismiss];
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Room, RoomEvent } from 'matrix-js-sdk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
function computePositions(room: Room, myUserId: string): Map<string, string[]> {
|
||||
const map = new Map<string, string[]>();
|
||||
for (const member of room.getJoinedMembers()) {
|
||||
if (member.userId === myUserId) continue;
|
||||
const evtId = room.getEventReadUpTo(member.userId);
|
||||
if (!evtId) continue;
|
||||
const arr = map.get(evtId);
|
||||
if (arr) arr.push(member.userId);
|
||||
else map.set(evtId, [member.userId]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function useRoomReadPositions(room: Room): Map<string, string[]> {
|
||||
const mx = useMatrixClient();
|
||||
const myUserId = mx.getUserId() ?? '';
|
||||
const [positions, setPositions] = useState(() => computePositions(room, myUserId));
|
||||
|
||||
useEffect(() => {
|
||||
setPositions(computePositions(room, myUserId));
|
||||
const onReceipt = () => setPositions(computePositions(room, myUserId));
|
||||
room.on(RoomEvent.Receipt, onReceipt);
|
||||
return () => {
|
||||
room.removeListener(RoomEvent.Receipt, onReceipt);
|
||||
};
|
||||
}, [room, myUserId]);
|
||||
|
||||
return positions;
|
||||
}
|
||||
@@ -69,7 +69,6 @@ import { CreateSpaceModalRenderer } from '../features/create-space';
|
||||
import { SearchModalRenderer } from '../features/search';
|
||||
import { getFallbackSession } from '../state/sessions';
|
||||
import { CallStatusRenderer } from './CallStatusRenderer';
|
||||
import { IncomingCallNotification } from '../components/IncomingCallNotification';
|
||||
import { CallEmbedProvider } from '../components/CallEmbedProvider';
|
||||
|
||||
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
|
||||
@@ -138,7 +137,6 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
<Outlet />
|
||||
</ClientLayout>
|
||||
<CallStatusRenderer />
|
||||
<IncomingCallNotification />
|
||||
</CallEmbedProvider>
|
||||
<SearchModalRenderer />
|
||||
<UserRoomProfileRenderer />
|
||||
|
||||
@@ -51,6 +51,12 @@ export class CallEmbed {
|
||||
|
||||
private styleRetryObserver?: MutationObserver;
|
||||
|
||||
// Arrow-function class fields so dispose() passes the exact same reference to mx.off()
|
||||
private readonly boundOnEvent = (ev: MatrixEvent) => this.onEvent(ev);
|
||||
private readonly boundOnEventDecrypted = (ev: MatrixEvent) => this.onEventDecrypted(ev);
|
||||
private readonly boundOnStateUpdate = (ev: MatrixEvent) => this.onStateUpdate(ev);
|
||||
private readonly boundOnToDeviceEvent = (ev: MatrixEvent) => this.onToDeviceEvent(ev);
|
||||
|
||||
static getIntent(dm: boolean, ongoing: boolean, video?: boolean): ElementCallIntent {
|
||||
if (dm && ongoing) {
|
||||
return video ? ElementCallIntent.JoinExistingDM : ElementCallIntent.JoinExistingDMVoice;
|
||||
@@ -252,10 +258,10 @@ export class CallEmbed {
|
||||
});
|
||||
|
||||
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
|
||||
this.mx.on(ClientEvent.Event, this.onEvent.bind(this));
|
||||
this.mx.on(MatrixEventEvent.Decrypted, this.onEventDecrypted.bind(this));
|
||||
this.mx.on(RoomStateEvent.Events, this.onStateUpdate.bind(this));
|
||||
this.mx.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent.bind(this));
|
||||
this.mx.on(ClientEvent.Event, this.boundOnEvent);
|
||||
this.mx.on(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
||||
this.mx.on(RoomStateEvent.Events, this.boundOnStateUpdate);
|
||||
this.mx.on(ClientEvent.ToDeviceEvent, this.boundOnToDeviceEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,10 +278,10 @@ export class CallEmbed {
|
||||
this.container.removeChild(this.iframe);
|
||||
this.control.dispose();
|
||||
|
||||
this.mx.off(ClientEvent.Event, this.onEvent.bind(this));
|
||||
this.mx.off(MatrixEventEvent.Decrypted, this.onEventDecrypted.bind(this));
|
||||
this.mx.off(RoomStateEvent.Events, this.onStateUpdate.bind(this));
|
||||
this.mx.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent.bind(this));
|
||||
this.mx.off(ClientEvent.Event, this.boundOnEvent);
|
||||
this.mx.off(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
||||
this.mx.off(RoomStateEvent.Events, this.boundOnStateUpdate);
|
||||
this.mx.off(ClientEvent.ToDeviceEvent, this.boundOnToDeviceEvent);
|
||||
|
||||
// Clear internal state
|
||||
this.readUpToMap = {};
|
||||
|
||||
@@ -36,6 +36,7 @@ export const useCallPreferences = (): CallPreferences & {
|
||||
}, [setPref, pref]);
|
||||
|
||||
const toggleVideo = useCallback(() => {
|
||||
if (!cameraOnJoin) return;
|
||||
const video = !pref.video;
|
||||
|
||||
setPref({
|
||||
@@ -43,7 +44,7 @@ export const useCallPreferences = (): CallPreferences & {
|
||||
video,
|
||||
sound: pref.sound,
|
||||
});
|
||||
}, [setPref, pref]);
|
||||
}, [setPref, pref, cameraOnJoin]);
|
||||
|
||||
const toggleSound = useCallback(() => {
|
||||
const sound = !pref.sound;
|
||||
|
||||
Reference in New Issue
Block a user