Merge upstream v4.12.3 (Element Call 0.20.1) into lotus
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,11 +22,11 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Login to Docker Hub #Do not update this action from a outside PR
|
- name: Login to Docker Hub #Do not update this action from a outside PR
|
||||||
if: github.event.pull_request.head.repo.fork == false
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
||||||
if: github.event.pull_request.head.repo.fork == false
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -43,14 +43,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker, GHCR
|
- name: Extract metadata (tags, labels) for Docker, GHCR
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ajbura/cinny
|
ajbura/cinny
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build Docker image (no push)
|
- name: Build Docker image (no push)
|
||||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|||||||
@@ -70,27 +70,27 @@ jobs:
|
|||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
- name: Login to Docker Hub #Do not update this action from a outside PR
|
- name: Login to Docker Hub #Do not update this action from a outside PR
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Extract metadata (tags, labels) for Docker, GHCR
|
- name: Extract metadata (tags, labels) for Docker, GHCR
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lotus-chat",
|
"name": "lotus-chat",
|
||||||
"version": "4.12.2-lotus",
|
"version": "4.12.3-lotus",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lotus-chat",
|
"name": "lotus-chat",
|
||||||
"version": "4.12.2-lotus",
|
"version": "4.12.3-lotus",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"ua-parser-js": "2.0.10"
|
"ua-parser-js": "2.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@element-hq/element-call-embedded": "0.19.4",
|
"@element-hq/element-call-embedded": "0.20.1",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
"@sentry/vite-plugin": "5.3.0",
|
"@sentry/vite-plugin": "5.3.0",
|
||||||
@@ -1792,9 +1792,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@element-hq/element-call-embedded": {
|
"node_modules/@element-hq/element-call-embedded": {
|
||||||
"version": "0.19.4",
|
"version": "0.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/@element-hq/element-call-embedded/-/element-call-embedded-0.19.4.tgz",
|
"resolved": "https://registry.npmjs.org/@element-hq/element-call-embedded/-/element-call-embedded-0.20.1.tgz",
|
||||||
"integrity": "sha512-crawgHughTv6yYoCqgq7cKLxUDtYU/Xr7KgSFCT0NM++QHoYsM5WGmIU/yY2Q0QYPuzmHXIEK95IZmJaQ1jIJA==",
|
"integrity": "sha512-ODg2r7UmR8UjRpapLKbn6v1PS8fu/r58zdbvXMYaAlUEAC2f6L/9Moc9S4noG1+ARgWxY+m2vLmNDK9G9uFZYQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lotus-chat",
|
"name": "lotus-chat",
|
||||||
"version": "4.12.2-lotus",
|
"version": "4.12.3-lotus",
|
||||||
"description": "Lotus Chat — Matrix client for Lotus Guild",
|
"description": "Lotus Chat — Matrix client for Lotus Guild",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
"ua-parser-js": "2.0.10"
|
"ua-parser-js": "2.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@element-hq/element-call-embedded": "0.19.4",
|
"@element-hq/element-call-embedded": "0.20.1",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
"@sentry/vite-plugin": "5.3.0",
|
"@sentry/vite-plugin": "5.3.0",
|
||||||
|
|||||||
@@ -192,11 +192,11 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
|||||||
/>
|
/>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Box>
|
</Box>
|
||||||
<Box grow="Yes" direction="Column" gap="100">
|
<Box grow="Yes" direction="Column" gap="100" alignItems="Center">
|
||||||
<Text size="H3" align="Center" truncate>
|
<Text size="H3" align="Center" truncate>
|
||||||
{roomName}
|
{roomName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T300">
|
<Text size="T300" align="Center">
|
||||||
{info.intent === 'video' ? 'Incoming Video Call' : 'Incoming Voice Call'}
|
{info.intent === 'video' ? 'Incoming Video Call' : 'Incoming Voice Call'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -283,6 +283,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
|||||||
// only process rtc notification reference events.
|
// only process rtc notification reference events.
|
||||||
// we do not want to wait to decrypt all events.
|
// we do not want to wait to decrypt all events.
|
||||||
if (event.getRelation()?.rel_type !== RelationType.Reference) return;
|
if (event.getRelation()?.rel_type !== RelationType.Reference) return;
|
||||||
|
if (room?.isCallRoom()) return;
|
||||||
|
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
if (!event.isBeingDecrypted()) {
|
if (!event.isBeingDecrypted()) {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export const PageHeroSection = style([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
export const PageContentCenter = style([
|
export const PageContentCenter = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ type MicrophoneButtonProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
|
function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps) {
|
||||||
|
const [micState, toggleMic] = useAsyncCallback(onToggle);
|
||||||
|
const loading = micState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
@@ -28,9 +31,9 @@ function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps
|
|||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="300"
|
radii="300"
|
||||||
size="300"
|
size="300"
|
||||||
onClick={() => onToggle()}
|
onClick={toggleMic}
|
||||||
outlined
|
outlined
|
||||||
disabled={disabled}
|
disabled={disabled || loading}
|
||||||
aria-label={enabled ? 'Turn off microphone' : 'Turn on microphone'}
|
aria-label={enabled ? 'Turn off microphone' : 'Turn on microphone'}
|
||||||
aria-pressed={!enabled}
|
aria-pressed={!enabled}
|
||||||
>
|
>
|
||||||
@@ -85,6 +88,9 @@ type VideoButtonProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||||
|
const [videoState, toggleVideo] = useAsyncCallback(onToggle);
|
||||||
|
const loading = videoState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
@@ -101,10 +107,10 @@ function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
|||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="300"
|
radii="300"
|
||||||
size="300"
|
size="300"
|
||||||
onClick={() => onToggle()}
|
onClick={toggleVideo}
|
||||||
aria-label={enabled ? 'Stop Video' : 'Start Video'}
|
aria-label={enabled ? 'Stop Video' : 'Start Video'}
|
||||||
outlined
|
outlined
|
||||||
disabled={disabled}
|
disabled={disabled || loading}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
size="100"
|
size="100"
|
||||||
@@ -162,6 +168,13 @@ export function CallControl({
|
|||||||
}) {
|
}) {
|
||||||
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control);
|
||||||
const setCallEmbed = useSetAtom(callEmbedAtom);
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||||
|
|
||||||
|
const handleMicrophoneToggle = useCallback(
|
||||||
|
() => callEmbed.control.toggleMicrophone(),
|
||||||
|
[callEmbed],
|
||||||
|
);
|
||||||
|
const handleVideoToggle = useCallback(() => callEmbed.control.toggleVideo(), [callEmbed]);
|
||||||
|
|
||||||
const [hangupState, hangup] = useAsyncCallback(
|
const [hangupState, hangup] = useAsyncCallback(
|
||||||
useCallback(() => callEmbed.hangup(), [callEmbed]),
|
useCallback(() => callEmbed.hangup(), [callEmbed]),
|
||||||
);
|
);
|
||||||
@@ -181,7 +194,7 @@ export function CallControl({
|
|||||||
<Box alignItems="Inherit" gap="200">
|
<Box alignItems="Inherit" gap="200">
|
||||||
<MicrophoneButton
|
<MicrophoneButton
|
||||||
enabled={microphone}
|
enabled={microphone}
|
||||||
onToggle={() => callEmbed.control.toggleMicrophone()}
|
onToggle={handleMicrophoneToggle}
|
||||||
disabled={!callJoined}
|
disabled={!callJoined}
|
||||||
/>
|
/>
|
||||||
<SoundButton
|
<SoundButton
|
||||||
@@ -190,11 +203,7 @@ export function CallControl({
|
|||||||
disabled={!callJoined}
|
disabled={!callJoined}
|
||||||
/>
|
/>
|
||||||
{!compact && <StatusDivider />}
|
{!compact && <StatusDivider />}
|
||||||
<VideoButton
|
<VideoButton enabled={video} onToggle={handleVideoToggle} disabled={!callJoined} />
|
||||||
enabled={video}
|
|
||||||
onToggle={() => callEmbed.control.toggleVideo()}
|
|
||||||
disabled={!callJoined}
|
|
||||||
/>
|
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<ScreenShareButton
|
<ScreenShareButton
|
||||||
enabled={screenshare}
|
enabled={screenshare}
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
setCords(undefined);
|
setCords(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMicrophoneToggle = useCallback(
|
||||||
|
() => callEmbed.control.toggleMicrophone(),
|
||||||
|
[callEmbed],
|
||||||
|
);
|
||||||
|
const handleVideoToggle = useCallback(() => callEmbed.control.toggleVideo(), [callEmbed]);
|
||||||
|
|
||||||
const pttActiveRef = useRef(false);
|
const pttActiveRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -368,10 +374,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
>
|
>
|
||||||
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
||||||
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
<MicrophoneButton
|
<MicrophoneButton enabled={microphone} onToggle={handleMicrophoneToggle} />
|
||||||
enabled={microphone}
|
|
||||||
onToggle={() => callEmbed.control.toggleMicrophone()}
|
|
||||||
/>
|
|
||||||
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
||||||
<ScreenshareAudioButton
|
<ScreenshareAudioButton
|
||||||
muted={screenshareAudioMuted}
|
muted={screenshareAudioMuted}
|
||||||
@@ -380,7 +383,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
{!compact && <ControlDivider />}
|
{!compact && <ControlDivider />}
|
||||||
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
<VideoButton enabled={video} onToggle={handleVideoToggle} />
|
||||||
<ScreenShareButton
|
<ScreenShareButton
|
||||||
enabled={screenshare}
|
enabled={screenshare}
|
||||||
onToggle={() =>
|
onToggle={() =>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'f
|
|||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
import { callChatAtom } from '../../state/callEmbed';
|
import { callChatAtom } from '../../state/callEmbed';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
|
||||||
export function ControlDivider() {
|
export function ControlDivider() {
|
||||||
return (
|
return (
|
||||||
@@ -12,9 +13,12 @@ export function ControlDivider() {
|
|||||||
|
|
||||||
type MicrophoneButtonProps = {
|
type MicrophoneButtonProps = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => Promise<unknown>;
|
||||||
};
|
};
|
||||||
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
|
export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
|
||||||
|
const [micState, toggleMic] = useAsyncCallback(onToggle);
|
||||||
|
const loading = micState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
@@ -32,9 +36,10 @@ export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
|
|||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="400"
|
radii="400"
|
||||||
size="400"
|
size="400"
|
||||||
onClick={() => onToggle()}
|
onClick={toggleMic}
|
||||||
aria-label={enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}
|
aria-label={enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}
|
||||||
outlined
|
outlined
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Icon size="400" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
|
<Icon size="400" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -82,10 +87,13 @@ export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
|
|||||||
|
|
||||||
type VideoButtonProps = {
|
type VideoButtonProps = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => Promise<unknown>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||||
|
const [videoState, toggleVideo] = useAsyncCallback(onToggle);
|
||||||
|
const loading = videoState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Top"
|
position="Top"
|
||||||
@@ -105,9 +113,9 @@ export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
|||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="400"
|
radii="400"
|
||||||
size="400"
|
size="400"
|
||||||
onClick={() => onToggle()}
|
onClick={toggleVideo}
|
||||||
outlined
|
outlined
|
||||||
disabled={disabled}
|
disabled={disabled || loading}
|
||||||
aria-label={
|
aria-label={
|
||||||
disabled ? 'Camera disabled in settings' : enabled ? 'Stop camera' : 'Start camera'
|
disabled ? 'Camera disabled in settings' : enabled ? 'Stop camera' : 'Start camera'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
|
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
|
||||||
import { SequenceCard } from '../../components/sequence-card';
|
import { SequenceCard } from '../../components/sequence-card';
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
@@ -54,6 +54,9 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
|||||||
useCallPreferences();
|
useCallPreferences();
|
||||||
const [cameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
const [cameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
||||||
|
|
||||||
|
const handleMicrophoneToggle = useCallback(async () => toggleMicrophone(), [toggleMicrophone]);
|
||||||
|
const handleVideoToggle = useCallback(async () => toggleVideo(), [toggleVideo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={css.ControlCard}
|
className={css.ControlCard}
|
||||||
@@ -65,12 +68,12 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
|||||||
wrap="Wrap"
|
wrap="Wrap"
|
||||||
>
|
>
|
||||||
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
|
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
|
||||||
<MicrophoneButton enabled={microphone} onToggle={toggleMicrophone} />
|
<MicrophoneButton enabled={microphone} onToggle={handleMicrophoneToggle} />
|
||||||
<SoundButton enabled={sound} onToggle={toggleSound} />
|
<SoundButton enabled={sound} onToggle={toggleSound} />
|
||||||
</Box>
|
</Box>
|
||||||
<ControlDivider />
|
<ControlDivider />
|
||||||
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
|
<Box shrink="No" alignItems="Inherit" justifyContent="SpaceBetween" gap="200">
|
||||||
<VideoButton enabled={video} onToggle={toggleVideo} disabled={!cameraOnJoin} />
|
<VideoButton enabled={video} onToggle={handleVideoToggle} disabled={!cameraOnJoin} />
|
||||||
<ChatButton />
|
<ChatButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box grow="Yes" direction="Column" gap="200">
|
<Box grow="Yes" direction="Column" gap="200">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const LobbyHeroTopic = style({
|
|||||||
WebkitLineClamp: 3,
|
WebkitLineClamp: 3,
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
|
||||||
':hover': {
|
':hover': {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
|
|
||||||
private iframe: HTMLIFrameElement;
|
private iframe: HTMLIFrameElement;
|
||||||
|
|
||||||
|
private bodyMutationObserver: MutationObserver;
|
||||||
|
|
||||||
private controlMutationObserver: MutationObserver;
|
private controlMutationObserver: MutationObserver;
|
||||||
|
|
||||||
private _pipMode = false;
|
private _pipMode = false;
|
||||||
|
|
||||||
|
private mediaStatePromiseResolver: undefined | (() => void);
|
||||||
|
|
||||||
private get document(): Document | undefined {
|
private get document(): Document | undefined {
|
||||||
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
|
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
|
||||||
}
|
}
|
||||||
@@ -30,17 +34,29 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
return screenshareBtn ?? undefined;
|
return screenshareBtn ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get leaveButton(): Element | undefined {
|
||||||
|
const leaveBtn = this.document?.querySelector('[data-testid="incall_leave"]');
|
||||||
|
|
||||||
|
return leaveBtn ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private get settingsButton(): HTMLElement | undefined {
|
private get settingsButton(): HTMLElement | undefined {
|
||||||
// EC 0.19.3: settings button has data-testid="settings-bottom-center"
|
// EC 0.20.1: settings button moved to bottom-left; fall back to bottom-center.
|
||||||
return (
|
const settingsButtonLeft = this.document?.querySelector(
|
||||||
(this.document?.querySelector('[data-testid="settings-bottom-center"]') as HTMLElement) ??
|
'[data-testid="settings-bottom-left"]',
|
||||||
undefined
|
) as HTMLButtonElement | undefined;
|
||||||
);
|
const settingsButtonCenter = this.document?.querySelector(
|
||||||
|
'[data-testid="settings-bottom-center"]',
|
||||||
|
) as HTMLButtonElement | undefined;
|
||||||
|
|
||||||
|
return settingsButtonLeft ?? settingsButtonCenter ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get reactionsButton(): HTMLElement | undefined {
|
private get reactionsButton(): HTMLElement | undefined {
|
||||||
// EC 0.19.3: reactions/raise-hand button has a CSS module class containing "raiseHand"
|
// EC 0.20.1: reactions/raise-hand button sits just before the leave button.
|
||||||
return (this.document?.querySelector('[class*="raiseHand"]') as HTMLElement) ?? undefined;
|
const reactionsButton = this.leaveButton?.previousElementSibling as HTMLElement | null;
|
||||||
|
|
||||||
|
return reactionsButton ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get spotlightButton(): HTMLInputElement | undefined {
|
private get spotlightButton(): HTMLInputElement | undefined {
|
||||||
@@ -66,6 +82,7 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.call = call;
|
this.call = call;
|
||||||
this.iframe = iframe;
|
this.iframe = iframe;
|
||||||
|
|
||||||
|
this.bodyMutationObserver = new MutationObserver(this.onBodyMutation.bind(this));
|
||||||
this.controlMutationObserver = new MutationObserver(this.onControlMutation.bind(this));
|
this.controlMutationObserver = new MutationObserver(this.onControlMutation.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +135,30 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public startObserving() {
|
public startObserving() {
|
||||||
|
if (!this.document) return;
|
||||||
|
|
||||||
|
this.bodyMutationObserver.observe(this.document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: false, // only direct children of body
|
||||||
|
});
|
||||||
|
this.onBodyMutation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBodyMutation() {
|
||||||
|
if (!this.document) return;
|
||||||
|
|
||||||
|
this.document.body.style.setProperty('background', 'none', 'important');
|
||||||
|
|
||||||
|
const controls = this.leaveButton?.parentElement?.parentElement;
|
||||||
|
if (controls) {
|
||||||
|
controls.style.setProperty('position', 'absolute');
|
||||||
|
controls.style.setProperty('visibility', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.observeControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private observeControls() {
|
||||||
this.controlMutationObserver.disconnect();
|
this.controlMutationObserver.disconnect();
|
||||||
|
|
||||||
const screenshareBtn = this.screenshareButton;
|
const screenshareBtn = this.screenshareButton;
|
||||||
@@ -141,8 +182,14 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.setSound(this.sound);
|
this.setSound(this.sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setMediaState(state: ElementMediaStatePayload) {
|
private async setMediaState(state: ElementMediaStatePayload) {
|
||||||
return this.call.transport.send(ElementWidgetActions.DeviceMute, state);
|
const data = await this.call.transport.send(ElementWidgetActions.DeviceMute, state);
|
||||||
|
return new Promise<typeof data>((resolve) => {
|
||||||
|
if (this.mediaStatePromiseResolver) {
|
||||||
|
this.mediaStatePromiseResolver();
|
||||||
|
}
|
||||||
|
this.mediaStatePromiseResolver = () => resolve(data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setSound(sound: boolean): void {
|
private setSound(sound: boolean): void {
|
||||||
@@ -186,9 +233,14 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
if (this.microphone && !this.sound) {
|
if (this.microphone && !this.sound) {
|
||||||
this.toggleSound();
|
this.toggleSound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mediaStatePromiseResolver) {
|
||||||
|
this.mediaStatePromiseResolver();
|
||||||
|
this.mediaStatePromiseResolver = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onControlMutation() {
|
private onControlMutation() {
|
||||||
const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
|
const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
|
||||||
const spotlight: boolean = this.spotlightButton?.checked ?? false;
|
const spotlight: boolean = this.spotlightButton?.checked ?? false;
|
||||||
|
|
||||||
@@ -321,6 +373,7 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
|
this.bodyMutationObserver.disconnect();
|
||||||
this.controlMutationObserver.disconnect();
|
this.controlMutationObserver.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,10 @@ export class CallEmbed {
|
|||||||
const controlState = initialControlState ?? new CallControlState(true, false, true);
|
const controlState = initialControlState ?? new CallControlState(true, false, true);
|
||||||
this.control = new CallControl(controlState, call, iframe);
|
this.control = new CallControl(controlState, call, iframe);
|
||||||
this.initialState = controlState;
|
this.initialState = controlState;
|
||||||
|
this.control.startObserving();
|
||||||
|
iframe.onload = () => {
|
||||||
|
this.control.startObserving();
|
||||||
|
};
|
||||||
|
|
||||||
let initialMediaEvent = true;
|
let initialMediaEvent = true;
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
|
|||||||
Reference in New Issue
Block a user