From 58250c31c9f5cc9548abe1f3fb4c920ff29a080c Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:20:54 -0400 Subject: [PATCH 01/13] fix: Massively improve voice call PIP drag performance & support touchscreens Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 373 ++++++++---------- .../voice/callCard/VoiceCallCardPiP.tsx | 2 +- 2 files changed, 170 insertions(+), 205 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index 3036add02..d203a3dcf 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -1,13 +1,9 @@ import { JSX, - Match, Show, - Switch, - batch, createContext, createEffect, createSignal, - on, onCleanup, onMount, useContext, @@ -16,7 +12,6 @@ import { Portal } from "solid-js/web"; import { AutoSizer } from "@dschz/solid-auto-sizer"; import { Channel } from "stoat.js"; -import { css } from "styled-system/css"; import { styled } from "styled-system/jsx"; import { InRoom, useVoice } from "@revolt/rtc"; @@ -25,239 +20,213 @@ import { VoiceCallCardActiveRoom } from "./VoiceCallCardActiveRoom"; import { VoiceCallCardPiP } from "./VoiceCallCardPiP"; import { VoiceCallCardPreview } from "./VoiceCallCardPreview"; +type FloatMode = "tl" | "tr" | "bl" | "br"; + type State = | { - type: "floating"; - corner: "top-left" | "top-right" | "bottom-left" | "bottom-right"; + mode: "floating"; + float: FloatMode; } | { - type: "fixed"; - x: number; - y: number; - width: number; - channel: Channel; + mode?: "fixed" | "moving"; }; -type NewState = { channel: Channel; x: number; y: number; width: number }; +type Info = { + pos: DOMRect; + channel: Channel; +}; -const callCardContext = createContext<(state?: NewState) => void>(null!); +const PAD = 16, + PAD_X = `${PAD}px`, + PAD_Y = `${PAD + 56}px`; -/** - * Voice call card context - */ +const callCardContext = createContext<(info?: Info) => void>(); + +function getTouch(id: number, tl: TouchList) { + for (const t of tl) if (t.identifier === id) return t; +} + +/** Voice call card context */ export function VoiceCallCardContext(props: { children: JSX.Element }) { - const [state, setState] = createSignal({ - type: "floating", - corner: "bottom-right", - }); + const voice = useVoice(); - const [moving, setMoving] = createSignal(); - const [offset, setOffset] = createSignal({ x: 0, y: 0 }); - - function position() { - const position = state(); - - switch (position.type) { - case "fixed": - return { - transform: `translate(${position.x}px, ${position.y}px)`, - // top: position.y + "px", - // left: position.x + "px", - width: position.width + "px", - height: "40vh", - }; - case "floating": - return { - "--width": "280px", - "--height": "158px", - "--padding-x": "32px", - "--padding-y": "96px", - transform: `translate(${ - position.corner === "top-left" || position.corner === "bottom-left" - ? "calc(var(--padding-x) + var(--offset-x))" - : "calc(100vw - var(--padding-x) - var(--width) + var(--offset-x))" - }, ${ - position.corner === "top-left" || position.corner === "top-right" - ? "calc(var(--padding-y) + var(--offset-y))" - : "calc(100vh - var(--padding-y) - var(--height) + var(--offset-y))" - })`, - width: "var(--width)", - height: "var(--height)", - }; + const [state, setState] = createSignal({}); + let ref: HTMLDivElement, channel: Channel | null; + + let events: AbortController | null, + tid = 0, + ofsX = 0, + ofsY = 0; + + function touchToMouse(e: MouseEvent | TouchEvent, down = false) { + if (e instanceof TouchEvent) { + const t = down ? e.touches[0] : getTouch(tid, e.changedTouches); + if (down) tid = t!.identifier; + else if (!t) return false; + //@ts-expect-error prop + e.clientX = t.clientX; + //@ts-expect-error prop + e.clientY = t.clientY; } + return true; } - createEffect( - on(moving, (moving) => { - if (moving) { - const controller = new AbortController(); - - document.addEventListener( - "mousemove", - (event) => { - const position = state(); - if (position.type !== "floating") return controller.abort(); - - setOffset((pos) => ({ - x: pos.x + event.movementX, - y: pos.y + event.movementY, - })); - }, - { signal: controller.signal }, - ); - - document.addEventListener( - "mouseup", - (event) => { - batch(() => { - setMoving(false); - - const left = event.clientX < window.outerWidth / 2; - const top = event.clientY < window.outerHeight / 2; - - setState({ - type: "floating", - corner: left - ? top - ? "top-left" - : "bottom-left" - : top - ? "top-right" - : "bottom-right", - }); - }); - }, - { signal: controller.signal }, - ); - - onCleanup(() => controller.abort()); - } - }), - ); - - function updateState(state?: NewState) { - if (state) { - setState({ - type: "fixed", - width: state.width, - x: state.x, - y: state.y, - channel: state.channel, - }); - } else { - setState({ - type: "floating", - corner: "bottom-right", - }); + function mouseDown(e: MouseEvent | TouchEvent) { + touchToMouse(e, true); + if (state().mode === "floating") { + const pos = ref!.getBoundingClientRect(); + ofsX = (e as MouseEvent).clientX - pos.x; + ofsY = (e as MouseEvent).clientY - pos.y; + setState({ mode: "moving" }); + addEvents(); } } - function updateStateWithTransition(state?: NewState) { - // no clue if this works + function mouseMove(e: MouseEvent | TouchEvent) { + if (!touchToMouse(e)) return; + e.preventDefault(); + ref!.style.left = `${(e as MouseEvent).clientX - ofsX}px`; + ref!.style.top = `${(e as MouseEvent).clientY - ofsY}px`; + } + + function mouseUp(e: MouseEvent | TouchEvent) { + if (!touchToMouse(e)) return; + const sty = ref!.style, + left = (e as MouseEvent).clientX < outerWidth / 2, + top = (e as MouseEvent).clientY < outerHeight / 2; + + sty.transition = "all .2s cubic-bezier(0, 1.67, 0.85, 0.8)"; + setFloat(left ? (top ? "tl" : "bl") : top ? "tr" : "br"); + setTimeout(() => (sty.transition = ""), 1); + resetEvents(); + } + + function addEvents() { + if (events) return; + events = new AbortController(); + const sig = { passive: false, signal: events.signal }; + document.addEventListener("mousemove", mouseMove, sig); + document.addEventListener("mouseup", mouseUp, sig); + document.addEventListener("touchmove", mouseMove, sig); + document.addEventListener("touchend", mouseUp, sig); + } + + function resetEvents() { + events?.abort(); + events = null; + } - if (!document.startViewTransition) { - updateState(state); - return; + function setInfo(info?: Info) { + if (ref!) { + if (info) { + channel = info.channel; + const sty = ref.style; + sty.left = `${info.pos.x}px`; + sty.top = `${info.pos.y}px`; + sty.width = `${info.pos.width}px`; + setState({ mode: "fixed" }); + } else { + channel = null; + setFloat("tr"); + } } + resetEvents(); + } - document.startViewTransition(() => updateState(state)); + function setFloat(float: FloatMode) { + const sty = ref!.style; + sty.left = + float[1] === "l" ? PAD_X : `calc(100vw - var(--width) - ${PAD_X})`; + sty.top = + float[0] === "t" ? PAD_Y : `calc(100vh - var(--height) - ${PAD_Y})`; + sty.width = ""; + setState({ mode: "floating", float }); } + onCleanup(resetEvents); + return ( - + {props.children} - -
{ - if (state().type === "floating") { - batch(() => { - setMoving(true); - setOffset({ x: 0, y: 0 }); - }); - } - }} - // dragging logic for touch input - // todo - > - - - - - - - - - - -
+ + + + + + } + > + + + +
); } -/** - * 'Marker' to send position information for mounting the floating call card - */ +const Float = styled("div", { + base: { + position: "fixed", + zIndex: 10, + pointerEvents: "none", + transition: "all .3s cubic-bezier(1, 0, 0, 1)", + height: "40vh", + }, + + variants: { + mode: { + floating: { cursor: "grab" }, + moving: { + cursor: "grabbing", + transition: "none", + }, + fixed: {}, + }, + }, + compoundVariants: [ + { + mode: ["floating", "moving"], + css: { + "--width": "300px", + "--height": "170px", + width: "var(--width)", + height: "var(--height)", + }, + }, + ], +}); + +/** 'Marker' to send position information for mounting the floating call card */ export function VoiceChannelCallCardMount(props: { channel: Channel }) { const voice = useVoice(); const [width, setWidth] = createSignal(0); - const [ref, setRef] = createSignal(); - const updateSize = useContext(callCardContext)!; - - const ongoingCallElsewhere = () => - voice.channel() && voice.channel()?.id !== props.channel.id; + const setInfo = useContext(callCardContext)!; createEffect(() => { - const rect = ref()?.getBoundingClientRect(); - const w = width(); - - const activeChannel = voice.channel(); - const canUpdate = !activeChannel || activeChannel.id === props.channel.id; - - if (rect?.left && w) { - if (canUpdate) { - updateSize({ - x: rect.left, - y: rect.top, - width: w, - channel: props.channel, - }); - } else { - updateSize(); - } - } + width(); + const active = voice.channel(); + const isActive = !active || active.id === props.channel.id; + const pos = ref()?.getBoundingClientRect(); + if (pos) setInfo(isActive ? { pos, channel: props.channel } : undefined); }); - onCleanup(() => updateSize()); + onCleanup(setInfo); + + //TODO React to pos change and not only width change return ( -
-
+ +
{({ width }) => { setWidth(width); @@ -265,11 +234,7 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { }}
- - - - -
+ ); } diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx index e4f261c3f..f79837233 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx @@ -90,7 +90,7 @@ const MiniCard = styled("div", { display: "flex", alignItems: "center", flexDirection: "column", - justifyContent: "center", + justifyContent: "end", gap: "var(--gap-md)", padding: "var(--gap-md)", From fd0190965228231ef0aed1b15f27382c10a2612c Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:49:32 -0400 Subject: [PATCH 02/13] fix: Cleaner code for previous fix, avoiding unnecessary redraws - Also un-borked the VoiceCallCardPreview which it borked Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 151 +++++++++--------- 1 file changed, 74 insertions(+), 77 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index d203a3dcf..bbf6bd8f0 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -1,8 +1,11 @@ import { JSX, + Match, Show, + Switch, createContext, createEffect, + createMemo, createSignal, onCleanup, onMount, @@ -14,26 +17,18 @@ import { AutoSizer } from "@dschz/solid-auto-sizer"; import { Channel } from "stoat.js"; import { styled } from "styled-system/jsx"; -import { InRoom, useVoice } from "@revolt/rtc"; +import { useVoice } from "@revolt/rtc"; import { VoiceCallCardActiveRoom } from "./VoiceCallCardActiveRoom"; import { VoiceCallCardPiP } from "./VoiceCallCardPiP"; import { VoiceCallCardPreview } from "./VoiceCallCardPreview"; -type FloatMode = "tl" | "tr" | "bl" | "br"; - -type State = - | { - mode: "floating"; - float: FloatMode; - } - | { - mode?: "fixed" | "moving"; - }; +type Mode = "floating" | "moving"; +type FloatType = "tl" | "tr" | "bl" | "br"; type Info = { - pos: DOMRect; channel: Channel; + pos?: DOMRect; }; const PAD = 16, @@ -50,8 +45,9 @@ function getTouch(id: number, tl: TouchList) { export function VoiceCallCardContext(props: { children: JSX.Element }) { const voice = useVoice(); - const [state, setState] = createSignal({}); - let ref: HTMLDivElement, channel: Channel | null; + const [mode, setMode] = createSignal(); + const [info, setInfo] = createSignal(); + let ref: HTMLDivElement; let events: AbortController | null, tid = 0, @@ -73,11 +69,11 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { function mouseDown(e: MouseEvent | TouchEvent) { touchToMouse(e, true); - if (state().mode === "floating") { + if (mode() === "floating") { const pos = ref!.getBoundingClientRect(); ofsX = (e as MouseEvent).clientX - pos.x; ofsY = (e as MouseEvent).clientY - pos.y; - setState({ mode: "moving" }); + setMode("moving"); addEvents(); } } @@ -92,8 +88,9 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { function mouseUp(e: MouseEvent | TouchEvent) { if (!touchToMouse(e)) return; const sty = ref!.style, - left = (e as MouseEvent).clientX < outerWidth / 2, - top = (e as MouseEvent).clientY < outerHeight / 2; + pos = ref!.getBoundingClientRect(), + left = (e as MouseEvent).clientX - ofsX + pos.width / 2 < outerWidth / 2, + top = (e as MouseEvent).clientY - ofsY + pos.height / 2 < outerHeight / 2; sty.transition = "all .2s cubic-bezier(0, 1.67, 0.85, 0.8)"; setFloat(left ? (top ? "tl" : "bl") : top ? "tr" : "br"); @@ -104,11 +101,11 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { function addEvents() { if (events) return; events = new AbortController(); - const sig = { passive: false, signal: events.signal }; - document.addEventListener("mousemove", mouseMove, sig); - document.addEventListener("mouseup", mouseUp, sig); - document.addEventListener("touchmove", mouseMove, sig); - document.addEventListener("touchend", mouseUp, sig); + const opt = { passive: false, signal: events.signal }; + document.addEventListener("mousemove", mouseMove, opt); + document.addEventListener("mouseup", mouseUp, opt); + document.addEventListener("touchmove", mouseMove, opt); + document.addEventListener("touchend", mouseUp, opt); } function resetEvents() { @@ -116,31 +113,37 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { events = null; } - function setInfo(info?: Info) { - if (ref!) { - if (info) { - channel = info.channel; - const sty = ref.style; - sty.left = `${info.pos.x}px`; - sty.top = `${info.pos.y}px`; - sty.width = `${info.pos.width}px`; - setState({ mode: "fixed" }); - } else { - channel = null; - setFloat("tr"); - } - } + const channel = createMemo(() => { + const inf = info(); + + console.log("SET INFO", inf); + + if (!ref!) return; + const sty = ref.style; + //TODO for PR #835 to adapt VoiceCallCard to mobile UI + //const drawer = state.appDrawer(); + + //Set mode based on state + if (inf?.pos /*&& (!drawer || drawer.state === SlideState.SHOWN)*/) { + sty.left = `${inf.pos.x}px`; + sty.top = `${inf.pos.y}px`; + sty.width = `${inf.pos.width}px`; + setMode(); + } else if (!voice.channel()) setMode(); + else if (!mode()) setFloat("tr"); + resetEvents(); - } + return inf?.channel; + }); - function setFloat(float: FloatMode) { + function setFloat(float: FloatType) { const sty = ref!.style; sty.left = float[1] === "l" ? PAD_X : `calc(100vw - var(--width) - ${PAD_X})`; sty.top = float[0] === "t" ? PAD_Y : `calc(100vh - var(--height) - ${PAD_Y})`; sty.width = ""; - setState({ mode: "floating", float }); + setMode("floating"); } onCleanup(resetEvents); @@ -149,25 +152,21 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { {props.children} - - - - - - } - > - - - - + + + + + + + + + + ); @@ -189,7 +188,6 @@ const Float = styled("div", { cursor: "grabbing", transition: "none", }, - fixed: {}, }, }, compoundVariants: [ @@ -214,27 +212,26 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { createEffect(() => { width(); - const active = voice.channel(); - const isActive = !active || active.id === props.channel.id; - const pos = ref()?.getBoundingClientRect(); - if (pos) setInfo(isActive ? { pos, channel: props.channel } : undefined); + const active = voice.channel(), + canUpdate = !active || active.id === props.channel.id; + if (canUpdate) + setInfo({ + channel: props.channel, + pos: ref()?.getBoundingClientRect(), + }); }); onCleanup(setInfo); - //TODO React to pos change and not only width change - return ( - -
- - {({ width }) => { - setWidth(width); - return null; - }} - -
-
+
+ + {({ width }) => { + setWidth(width); + return null; + }} + +
); } @@ -243,8 +240,8 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { */ function VoiceCallCard(props: { channel: Channel }) { const voice = useVoice(); - const inCall = () => voice.channel()?.id === props.channel.id; + const inCall = () => !!voice.channel(); let viewRef: HTMLDivElement | undefined; onMount(() => { From df918bcba8ca4891bccb80a9d6c220e1d38e69cf Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:51:13 -0400 Subject: [PATCH 03/13] refactor: VoiceCallCardActions - Add missing tooltips to VoiceCallCard buttons - Add missing translatable strings to VoiceCallCardActions Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 4 +- .../voice/callCard/VoiceCallCardActions.tsx | 93 ++++++++++++------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index bbf6bd8f0..ee42aa64a 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -116,8 +116,6 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { const channel = createMemo(() => { const inf = info(); - console.log("SET INFO", inf); - if (!ref!) return; const sty = ref.style; //TODO for PR #835 to adapt VoiceCallCard to mobile UI @@ -240,8 +238,8 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { */ function VoiceCallCard(props: { channel: Channel }) { const voice = useVoice(); - const inCall = () => !!voice.channel(); + let viewRef: HTMLDivElement | undefined; onMount(() => { diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx index 3484c7d81..7b50c2f8f 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx @@ -1,3 +1,4 @@ +import { useNavigate } from "@solidjs/router"; import { Show } from "solid-js"; import { useLingui } from "@lingui-solid/solid/macro"; @@ -10,32 +11,46 @@ import { Symbol } from "@revolt/ui/components/utils/Symbol"; export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { const voice = useVoice(); + const navigate = useNavigate(); const { t } = useLingui(); - function isVideoEnabled() { - return CONFIGURATION.ENABLE_VIDEO; - } + //TODO For changes in PR #999 + const enableVideo = CONFIGURATION.ENABLE_VIDEO; return ( - - - arrow_top_left - - + { + navigate(voice.channel()?.path ?? ""); + //TODO For change in PR #835 + //state.appDrawer()?.setShown(true); + }} + use:floating={{ + tooltip: { + placement: "top", + content: t`Return to voice channel`, + }, + }} + > + arrow_top_left + voice.toggleMute()} use:floating={{ - tooltip: voice.speakingPermission - ? undefined - : { - placement: "top", - content: t`Missing permission`, - }, + tooltip: { + placement: "top", + content: voice.speakingPermission + ? voice.microphone() + ? t`Unmute` + : t`Mute` + : t`Missing permission`, + }, }} isDisabled={!voice.speakingPermission} > @@ -48,12 +63,14 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { variant={voice.deafen() || !voice.listenPermission ? "tonal" : "filled"} onPress={() => voice.toggleDeafen()} use:floating={{ - tooltip: voice.listenPermission - ? undefined - : { - placement: "top", - content: t`Missing permission`, - }, + tooltip: { + placement: "top", + content: voice.listenPermission + ? voice.deafen() + ? t`Listen` + : t`Defean` + : t`Missing permission`, + }, }} isDisabled={!voice.listenPermission} > @@ -66,44 +83,44 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { { - if (isVideoEnabled()) voice.toggleCamera(); + if (enableVideo) voice.toggleCamera(); }} use:floating={{ tooltip: { placement: "top", - content: isVideoEnabled() + content: enableVideo ? voice.video() - ? "Stop Camera" - : "Start Camera" - : "Coming soon! 👀", + ? t`Stop camera` + : t`Start camera` + : t`Coming soon! 👀`, }, }} - isDisabled={!isVideoEnabled()} + isDisabled={!enableVideo} > camera_video { - if (isVideoEnabled()) voice.toggleScreenshare(); + if (enableVideo) voice.toggleScreenshare(); }} use:floating={{ tooltip: { placement: "top", - content: isVideoEnabled() + content: enableVideo ? voice.screenshare() - ? "Stop Sharing" - : "Share Screen" - : "Coming soon! 👀", + ? t`Stop sharing` + : t`Share screen` + : t`Coming soon! 👀`, }, }} - isDisabled={!isVideoEnabled()} + isDisabled={!enableVideo} > stop_screen_share} > screen_share @@ -113,6 +130,12 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { size={props.size} variant="_error" onPress={() => voice.disconnect()} + use:floating={{ + tooltip: { + placement: "top", + content: t`End call`, + }, + }} > call_end From 085ce1955838d5d82904f8739fd35fd99c32adc5 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:24:07 -0400 Subject: [PATCH 04/13] fix: Adjust to Voice state changes in PIP Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCardPiP.tsx | 9 +++++---- .../features/voice/callCard/VoiceCallCardStatus.tsx | 11 +++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx index f79837233..666fba64b 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx @@ -19,17 +19,17 @@ import { VoiceCallCardActions } from "./VoiceCallCardActions"; import { VoiceCallCardStatus } from "./VoiceCallCardStatus"; export function VoiceCallCardPiP() { - const tracks = useTracks( + const audTracks = useTracks( [{ source: Track.Source.Microphone, withPlaceholder: true }], { onlySubscribed: false }, ); return ( - - {() => } + + + {() => } - ); @@ -61,6 +61,7 @@ const UserIcon = styled("div", { display: "grid", width: "24px", height: "24px", + color: "#fffb", "& *": { gridArea: "1/1", diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx index f604be2ec..ff6a04300 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx @@ -4,7 +4,7 @@ import { styled } from "styled-system/jsx"; import { useVoice } from "@revolt/rtc"; import { Symbol } from "@revolt/ui/components/utils/Symbol"; -export function VoiceCallCardStatus() { +export function VoiceCallCardStatus(props: { pip?: boolean }) { const voice = useVoice(); const symbol = () => { @@ -38,7 +38,7 @@ export function VoiceCallCardStatus() { }; return ( - + {symbol()}{" "} {text()} @@ -94,5 +94,12 @@ const Status = styled("div", { color: "var(--md-sys-color-outline)", }, }, + pip: { + true: { + position: "absolute", + left: "var(--gap-md)", + top: "var(--gap-md)", + }, + }, }, }); From e0a9ff513205351991ec21a141cb6b6172db9000 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:47:37 -0400 Subject: [PATCH 05/13] fix: Show video in PIP player when focused - Don't force showing video bar every time focus mode is enabled Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../voice/callCard/VoiceCallCardActions.tsx | 1 + .../voice/callCard/VoiceCallCardPiP.tsx | 58 ++++++++++++++++++- .../voice/callCard/VoiceCallCardStatus.tsx | 1 + 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx index 7b50c2f8f..b33fd3399 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx @@ -148,6 +148,7 @@ const Actions = styled("div", { flexShrink: 0, gap: "var(--gap-md)", padding: "var(--gap-md)", + zIndex: 2, display: "flex", width: "fit-content", diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx index 666fba64b..c50cb019a 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx @@ -1,16 +1,20 @@ import { Show } from "solid-js"; import { TrackLoop, + TrackReference, useEnsureParticipant, useIsMuted, useIsSpeaking, + useTrackRefContext, useTracks, + VideoTrack, } from "solid-livekit-components"; import { Track } from "livekit-client"; import { styled } from "styled-system/jsx"; import { useUser } from "@revolt/markdown/users"; +import { useVoice } from "@revolt/rtc"; import { Avatar } from "@revolt/ui/components/design"; import { Row } from "@revolt/ui/components/layout"; import { Symbol } from "@revolt/ui/components/utils/Symbol"; @@ -19,17 +23,33 @@ import { VoiceCallCardActions } from "./VoiceCallCardActions"; import { VoiceCallCardStatus } from "./VoiceCallCardStatus"; export function VoiceCallCardPiP() { + const voice = useVoice(); const audTracks = useTracks( [{ source: Track.Source.Microphone, withPlaceholder: true }], { onlySubscribed: false }, ); + const hasFocusVideo = () => { + const track = voice.focusTrack(); + if (!track) return false; + + return ( + track.source === Track.Source.ScreenShare || + !useIsMuted({ + participant: track.participant, + source: Track.Source.Camera, + })() + ); + }; + return ( - - {() => } - + }> + + {() => } + + ); @@ -56,6 +76,37 @@ function ConnectedUser() { ); } +function MiniVideoTile() { + const voice = useVoice(); + + return ( + [voice.focusTrack()!]}> + {() => } + + ); +} + +function MiniVideo() { + const track = useTrackRefContext(); + + return ( + + ); +} + const UserIcon = styled("div", { base: { display: "grid", @@ -98,5 +149,6 @@ const MiniCard = styled("div", { borderRadius: "var(--borderRadius-lg)", background: "var(--md-sys-color-secondary-container)", + transform: "translateZ(0)", //Tells WebKit browsers to render on GPU }, }); diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx index ff6a04300..5dc07e3d5 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardStatus.tsx @@ -68,6 +68,7 @@ const Status = styled("div", { display: "flex", justifyContent: "center", + zIndex: 1, _hover: { "& div": { From b2489a65581b93cdb2fb2e53b32f9d233da19d90 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:53:46 -0400 Subject: [PATCH 06/13] fix: Improve mobile UI handling for 835 - Fix reversed mute/unmute text Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 33 ++++++++++--------- .../voice/callCard/VoiceCallCardActions.tsx | 4 +-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index ee42aa64a..b8eb2a487 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -29,6 +29,7 @@ type FloatType = "tl" | "tr" | "bl" | "br"; type Info = { channel: Channel; pos?: DOMRect; + //drawer?: SlideState; TODO PR #835 }; const PAD = 16, @@ -47,7 +48,7 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { const [mode, setMode] = createSignal(); const [info, setInfo] = createSignal(); - let ref: HTMLDivElement; + let ref: HTMLDivElement | undefined; let events: AbortController | null, tid = 0, @@ -116,19 +117,20 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { const channel = createMemo(() => { const inf = info(); - if (!ref!) return; + if (!ref) return; const sty = ref.style; - //TODO for PR #835 to adapt VoiceCallCard to mobile UI - //const drawer = state.appDrawer(); //Set mode based on state - if (inf?.pos /*&& (!drawer || drawer.state === SlideState.SHOWN)*/) { + //TODO for PR #835 to adapt VoiceCallCard to mobile UI + if (inf?.pos /*&& (!inf.drawer || inf.drawer === SlideState.SHOWN)*/) { sty.left = `${inf.pos.x}px`; sty.top = `${inf.pos.y}px`; sty.width = `${inf.pos.width}px`; setMode(); - } else if (!voice.channel()) setMode(); - else if (!mode()) setFloat("tr"); + } else if (!voice.channel()) { + sty.left = `${innerWidth + 50}px`; + setMode(); + } else if (!mode()) setFloat("tr"); resetEvents(); return inf?.channel; @@ -137,9 +139,9 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { function setFloat(float: FloatType) { const sty = ref!.style; sty.left = - float[1] === "l" ? PAD_X : `calc(100vw - var(--width) - ${PAD_X})`; + float[1] === "l" ? PAD_X : `calc(100vw - var(--flt-w) - ${PAD_X})`; sty.top = - float[0] === "t" ? PAD_Y : `calc(100vh - var(--height) - ${PAD_Y})`; + float[0] === "t" ? PAD_Y : `calc(100vh - var(--flt-h) - ${PAD_Y})`; sty.width = ""; setMode("floating"); } @@ -151,7 +153,7 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { {props.children} (); @@ -216,6 +218,7 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { setInfo({ channel: props.channel, pos: ref()?.getBoundingClientRect(), + //drawer: state.appDrawer()?.state, TODO PR #835 }); }); diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx index b33fd3399..43f01e4e1 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx @@ -47,8 +47,8 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { placement: "top", content: voice.speakingPermission ? voice.microphone() - ? t`Unmute` - : t`Mute` + ? t`Mute` + : t`Unmute` : t`Missing permission`, }, }} From 68665ec4c7a4e83ae7a6a4b188444c7595295a9a Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:41:37 -0400 Subject: [PATCH 07/13] fix: Use transform instead of top/left for PIP move to ensure GPU render Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 24 +++++++++---------- .../voice/callCard/VoiceCallCardPiP.tsx | 1 - 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index b8eb2a487..92be2fe66 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -82,8 +82,9 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { function mouseMove(e: MouseEvent | TouchEvent) { if (!touchToMouse(e)) return; e.preventDefault(); - ref!.style.left = `${(e as MouseEvent).clientX - ofsX}px`; - ref!.style.top = `${(e as MouseEvent).clientY - ofsY}px`; + const x = (e as MouseEvent).clientX - ofsX, + y = (e as MouseEvent).clientY - ofsY; + ref!.style.transform = `translate(${x}px, ${y}px)`; } function mouseUp(e: MouseEvent | TouchEvent) { @@ -123,12 +124,12 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { //Set mode based on state //TODO for PR #835 to adapt VoiceCallCard to mobile UI if (inf?.pos /*&& (!inf.drawer || inf.drawer === SlideState.SHOWN)*/) { - sty.left = `${inf.pos.x}px`; - sty.top = `${inf.pos.y}px`; + sty.transform = `translate(${inf.pos.x}px, ${inf.pos.y}px)`; sty.width = `${inf.pos.width}px`; setMode(); } else if (!voice.channel()) { - sty.left = `${innerWidth + 50}px`; + const y = inf?.pos?.y ?? ref.getBoundingClientRect().y; + sty.transform = `translate(${innerWidth + 50}px, ${y}px)`; setMode(); } else if (!mode()) setFloat("tr"); @@ -137,11 +138,10 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { }); function setFloat(float: FloatType) { - const sty = ref!.style; - sty.left = - float[1] === "l" ? PAD_X : `calc(100vw - var(--flt-w) - ${PAD_X})`; - sty.top = - float[0] === "t" ? PAD_Y : `calc(100vh - var(--flt-h) - ${PAD_Y})`; + const sty = ref!.style, + x = float[1] === "l" ? PAD_X : `calc(100vw - var(--flt-w) - ${PAD_X})`, + y = float[0] === "t" ? PAD_Y : `calc(100vh - var(--flt-h) - ${PAD_Y})`; + sty.transform = `translate(${x}, ${y})`; sty.width = ""; setMode("floating"); } @@ -282,8 +282,8 @@ function VoiceCallCard(props: { channel: Channel }) { const Base = styled("div", { base: { - // todo: temp for Mount - top: "var(--gap-md)", + top: 0, + left: 0, padding: "var(--gap-md)", width: "100%", diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx index c50cb019a..171059b0e 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx @@ -149,6 +149,5 @@ const MiniCard = styled("div", { borderRadius: "var(--borderRadius-lg)", background: "var(--md-sys-color-secondary-container)", - transform: "translateZ(0)", //Tells WebKit browsers to render on GPU }, }); From c32191fdca46dab15066a7f8185d40109c6b8b72 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:19:23 -0400 Subject: [PATCH 08/13] refactor: Replace AutoSizer with ResizeObserver Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index 92be2fe66..06120ecdf 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -13,7 +13,7 @@ import { } from "solid-js"; import { Portal } from "solid-js/web"; -import { AutoSizer } from "@dschz/solid-auto-sizer"; +import { createResizeObserver } from "@solid-primitives/resize-observer"; import { Channel } from "stoat.js"; import { styled } from "styled-system/jsx"; @@ -207,8 +207,12 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { //const state = useState(); const voice = useVoice(); const [width, setWidth] = createSignal(0); - const [ref, setRef] = createSignal(); const setInfo = useContext(callCardContext)!; + let ref: HTMLDivElement | undefined; + + onMount(() => { + createResizeObserver(ref, ({ width }) => setWidth(width)); + }); createEffect(() => { width(); @@ -217,23 +221,14 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { if (canUpdate) setInfo({ channel: props.channel, - pos: ref()?.getBoundingClientRect(), + pos: ref!.getBoundingClientRect(), //drawer: state.appDrawer()?.state, TODO PR #835 }); }); onCleanup(setInfo); - return ( -
- - {({ width }) => { - setWidth(width); - return null; - }} - -
- ); + return
; } /** From db4647d0b80a95990576cb8c1cf5cddaef347726 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:54:52 -0400 Subject: [PATCH 09/13] fix: Fix call card offset misalignment Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../ui/components/features/voice/callCard/VoiceCallCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index 06120ecdf..075e12286 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -277,8 +277,8 @@ function VoiceCallCard(props: { channel: Channel }) { const Base = styled("div", { base: { - top: 0, left: 0, + top: "var(--gap-md)", padding: "var(--gap-md)", width: "100%", From 3d947b4c2a206ea95a0f8d3355bf4323014442eb Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Thu, 7 May 2026 13:29:35 -0400 Subject: [PATCH 10/13] refactor: Remove PR TODOs Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../ui/components/features/voice/callCard/VoiceCallCard.tsx | 5 +---- .../features/voice/callCard/VoiceCallCardActions.tsx | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index 075e12286..c369d0856 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -29,7 +29,6 @@ type FloatType = "tl" | "tr" | "bl" | "br"; type Info = { channel: Channel; pos?: DOMRect; - //drawer?: SlideState; TODO PR #835 }; const PAD = 16, @@ -122,8 +121,7 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { const sty = ref.style; //Set mode based on state - //TODO for PR #835 to adapt VoiceCallCard to mobile UI - if (inf?.pos /*&& (!inf.drawer || inf.drawer === SlideState.SHOWN)*/) { + if (inf?.pos) { sty.transform = `translate(${inf.pos.x}px, ${inf.pos.y}px)`; sty.width = `${inf.pos.width}px`; setMode(); @@ -222,7 +220,6 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { setInfo({ channel: props.channel, pos: ref!.getBoundingClientRect(), - //drawer: state.appDrawer()?.state, TODO PR #835 }); }); diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx index 43f01e4e1..8c3cfa049 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx @@ -14,7 +14,6 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { const navigate = useNavigate(); const { t } = useLingui(); - //TODO For changes in PR #999 const enableVideo = CONFIGURATION.ENABLE_VIDEO; return ( @@ -25,8 +24,6 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { size={props.size} onPress={() => { navigate(voice.channel()?.path ?? ""); - //TODO For change in PR #835 - //state.appDrawer()?.setShown(true); }} use:floating={{ tooltip: { From 0664ac0be5ec6c5c92d71c6171d3537731077222 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Thu, 7 May 2026 14:05:27 -0400 Subject: [PATCH 11/13] fix: Change touchToMouse code to a more general solution (can be moved to utility class later) Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index c369d0856..b85575f2e 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -31,16 +31,19 @@ type Info = { pos?: DOMRect; }; +type PtrEvent = { + type: string; + tid: number; + clientX: number; + clientY: number; +}; + const PAD = 16, PAD_X = `${PAD}px`, PAD_Y = `${PAD + 56}px`; const callCardContext = createContext<(info?: Info) => void>(); -function getTouch(id: number, tl: TouchList) { - for (const t of tl) if (t.identifier === id) return t; -} - /** Voice call card context */ export function VoiceCallCardContext(props: { children: JSX.Element }) { const voice = useVoice(); @@ -54,44 +57,55 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { ofsX = 0, ofsY = 0; - function touchToMouse(e: MouseEvent | TouchEvent, down = false) { + function getTouch(id: number, tl: TouchList) { + for (const t of tl) if (t.identifier === id) return t; + } + + function getPointer( + e: MouseEvent | TouchEvent, + tid: number | null, + ): PtrEvent | undefined { + let t: MouseEvent | Touch | undefined; if (e instanceof TouchEvent) { - const t = down ? e.touches[0] : getTouch(tid, e.changedTouches); - if (down) tid = t!.identifier; - else if (!t) return false; - //@ts-expect-error prop - e.clientX = t.clientX; - //@ts-expect-error prop - e.clientY = t.clientY; - } - return true; + t = tid ? getTouch(tid, e.changedTouches) : e.touches[0]; + if (!t) return; + } else t = e; + return { + type: e.type, + tid: (t as Touch).identifier || 0, + clientX: t!.clientX, + clientY: t!.clientY, + }; } function mouseDown(e: MouseEvent | TouchEvent) { - touchToMouse(e, true); + const ptr = getPointer(e, null)!; + tid = ptr.tid; if (mode() === "floating") { const pos = ref!.getBoundingClientRect(); - ofsX = (e as MouseEvent).clientX - pos.x; - ofsY = (e as MouseEvent).clientY - pos.y; + ofsX = ptr.clientX - pos.x; + ofsY = ptr.clientY - pos.y; setMode("moving"); addEvents(); } } function mouseMove(e: MouseEvent | TouchEvent) { - if (!touchToMouse(e)) return; + const ptr = getPointer(e, tid); + if (!ptr) return; e.preventDefault(); - const x = (e as MouseEvent).clientX - ofsX, - y = (e as MouseEvent).clientY - ofsY; + const x = ptr.clientX - ofsX, + y = ptr.clientY - ofsY; ref!.style.transform = `translate(${x}px, ${y}px)`; } function mouseUp(e: MouseEvent | TouchEvent) { - if (!touchToMouse(e)) return; + const ptr = getPointer(e, tid); + if (!ptr) return; const sty = ref!.style, pos = ref!.getBoundingClientRect(), - left = (e as MouseEvent).clientX - ofsX + pos.width / 2 < outerWidth / 2, - top = (e as MouseEvent).clientY - ofsY + pos.height / 2 < outerHeight / 2; + left = ptr.clientX - ofsX + pos.width / 2 < outerWidth / 2, + top = ptr.clientY - ofsY + pos.height / 2 < outerHeight / 2; sty.transition = "all .2s cubic-bezier(0, 1.67, 0.85, 0.8)"; setFloat(left ? (top ? "tl" : "bl") : top ? "tr" : "br"); From 6df9fe6825b5622db9fffe9a8cd5a4de454015e8 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Thu, 7 May 2026 17:02:18 -0400 Subject: [PATCH 12/13] fix: Bug where PIP state didn't update when switching DM channels instead of server channels - Fix call card offset being off after initial page load by observing size of channel itself instead of an inner element Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../features/voice/callCard/VoiceCallCard.tsx | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index b85575f2e..34a861ceb 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -13,7 +13,7 @@ import { } from "solid-js"; import { Portal } from "solid-js/web"; -import { createResizeObserver } from "@solid-primitives/resize-observer"; +import { makeResizeObserver } from "@solid-primitives/resize-observer"; import { Channel } from "stoat.js"; import { styled } from "styled-system/jsx"; @@ -28,7 +28,7 @@ type FloatType = "tl" | "tr" | "bl" | "br"; type Info = { channel: Channel; - pos?: DOMRect; + pos: DOMRect; }; type PtrEvent = { @@ -50,9 +50,9 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { const [mode, setMode] = createSignal(); const [info, setInfo] = createSignal(); - let ref: HTMLDivElement | undefined; - let events: AbortController | null, + let ref: HTMLDivElement | undefined, + events: AbortController | null, tid = 0, ofsX = 0, ofsY = 0; @@ -61,13 +61,16 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { for (const t of tl) if (t.identifier === id) return t; } + /** + * @param tid Touch ID, or null to track new touch + */ function getPointer( e: MouseEvent | TouchEvent, tid: number | null, ): PtrEvent | undefined { let t: MouseEvent | Touch | undefined; if (e instanceof TouchEvent) { - t = tid ? getTouch(tid, e.changedTouches) : e.touches[0]; + t = tid != null ? getTouch(tid, e.changedTouches) : e.touches[0]; if (!t) return; } else t = e; return { @@ -109,6 +112,7 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { sty.transition = "all .2s cubic-bezier(0, 1.67, 0.85, 0.8)"; setFloat(left ? (top ? "tl" : "bl") : top ? "tr" : "br"); + //Reset CSS transition on next render pass setTimeout(() => (sty.transition = ""), 1); resetEvents(); } @@ -140,7 +144,7 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { sty.width = `${inf.pos.width}px`; setMode(); } else if (!voice.channel()) { - const y = inf?.pos?.y ?? ref.getBoundingClientRect().y; + const y = inf?.pos.y ?? ref.getBoundingClientRect().y; sty.transform = `translate(${innerWidth + 50}px, ${y}px)`; setMode(); } else if (!mode()) setFloat("tr"); @@ -174,7 +178,7 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { - + @@ -216,29 +220,35 @@ const Float = styled("div", { /** 'Marker' to send position information for mounting the floating call card */ export function VoiceChannelCallCardMount(props: { channel: Channel }) { - //const state = useState(); const voice = useVoice(); - const [width, setWidth] = createSignal(0); const setInfo = useContext(callCardContext)!; let ref: HTMLDivElement | undefined; + function updateInfo() { + const vc = voice.channel(); + setInfo( + !vc || vc.id === props.channel.id + ? { + channel: props.channel, + pos: ref!.getBoundingClientRect(), + } + : undefined, + ); + } + + createEffect(updateInfo); + + //Observe resize of parent + let obs: ReturnType; onMount(() => { - createResizeObserver(ref, ({ width }) => setWidth(width)); + obs = makeResizeObserver(updateInfo); + obs.observe(ref!.parentElement!); }); - - createEffect(() => { - width(); - const active = voice.channel(), - canUpdate = !active || active.id === props.channel.id; - if (canUpdate) - setInfo({ - channel: props.channel, - pos: ref!.getBoundingClientRect(), - }); + onCleanup(() => { + obs.unobserve(ref!.parentElement!); + setInfo(); }); - onCleanup(setInfo); - return
; } From ef5d4adef742f3d4fdaf3f905e0d9ad7fe75076f Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 8 May 2026 11:59:42 -0400 Subject: [PATCH 13/13] fix: Dim background behind muted icons Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../client/components/ui/components/design/Avatar.tsx | 3 ++- .../features/voice/callCard/VoiceCallCardPiP.tsx | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/client/components/ui/components/design/Avatar.tsx b/packages/client/components/ui/components/design/Avatar.tsx index b83cb2482..9fe5cd35a 100644 --- a/packages/client/components/ui/components/design/Avatar.tsx +++ b/packages/client/components/ui/components/design/Avatar.tsx @@ -16,7 +16,7 @@ export type Props = { /** * Avatar shape */ - shape?: "circle" | "rounded-square"; + shape?: "circle" | "square" | "rounded-square"; /** * Image source @@ -205,6 +205,7 @@ const Shape = styled("div", { circle: { borderRadius: "var(--borderRadius-circle)", }, + square: {}, "rounded-square": { borderRadius: "var(--borderRadius-md)", }, diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx index 171059b0e..94a73a3d4 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardPiP.tsx @@ -68,9 +68,14 @@ function ConnectedUser() { return ( - + - mic_off + mic_off ); @@ -113,6 +118,8 @@ const UserIcon = styled("div", { width: "24px", height: "24px", color: "#fffb", + overflow: "hidden", + borderRadius: "var(--borderRadius-circle)", "& *": { gridArea: "1/1",