(fix): joining a call while already being in a call wouldn't reset the call state

(qol): format
This commit is contained in:
Alois 2026-04-30 23:37:26 +02:00
commit 4dbf48b3f9
8 changed files with 85 additions and 50 deletions

View file

@ -1,10 +1,5 @@
import { useTTP } from "@tensamin/ttp"; import { useTTP } from "@tensamin/ttp";
import { import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
Button,
Popover,
PopoverContent,
PopoverTrigger,
} from "@tensamin/ui";
import Wrapper from "@tensamin/user/wrapper"; import Wrapper from "@tensamin/user/wrapper";
import { Mail } from "lucide-react"; import { Mail } from "lucide-react";
import { sendCallInvite } from "../../store"; import { sendCallInvite } from "../../store";

View file

@ -142,7 +142,8 @@ export default function Base({
{/* Detect video / user and place here */} {/* Detect video / user and place here */}
{type === "stream" && {type === "stream" &&
(screenSharePublication?.isSubscribed && screenSharePublication.track ? ( (screenSharePublication?.isSubscribed &&
screenSharePublication.track ? (
<VideoViewer <VideoViewer
flush={flush} flush={flush}
participantId={participant.identity} participantId={participant.identity}

View file

@ -7,12 +7,15 @@ export default function TopBar() {
const room = useCall((state) => state.room); const room = useCall((state) => state.room);
const view = useCall((state) => state.view); const view = useCall((state) => state.view);
const userIds = Array.from(room.remoteParticipants.values(), (participant) => { const userIds = Array.from(
const participantId = Number(participant.identity); room.remoteParticipants.values(),
return Number.isInteger(participantId) && participantId > 0 (participant) => {
? participantId const participantId = Number(participant.identity);
: null; return Number.isInteger(participantId) && participantId > 0
}).filter((participantId): participantId is number => participantId != null); ? participantId
: null;
},
).filter((participantId): participantId is number => participantId != null);
const userIdsKey = userIds.join(","); const userIdsKey = userIds.join(",");
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);

View file

@ -51,7 +51,13 @@ export function createScreenShareController({
await Promise.all( await Promise.all(
screenShareSession.tracks.map((track) => screenShareSession.tracks.map((track) =>
room.localParticipant.unpublishTrack(track, true).catch((error) => { room.localParticipant.unpublishTrack(track, true).catch((error) => {
log(1, "call", "red", "Failed to unpublish screen share track", error); log(
1,
"call",
"red",
"Failed to unpublish screen share track",
error,
);
}), }),
), ),
); );
@ -194,15 +200,12 @@ export function createScreenShareController({
}); });
}, 125); }, 125);
await publishScreenShareTracks( await publishScreenShareTracks([videoTrack], () => {
[videoTrack], stopped = true;
() => { window.clearInterval(interval);
stopped = true; stream.getTracks().forEach((track) => track.stop());
window.clearInterval(interval); canvas.remove();
stream.getTracks().forEach((track) => track.stop()); });
canvas.remove();
},
);
} }
async function stopScreenShare() { async function stopScreenShare() {

View file

@ -157,7 +157,8 @@ function getActiveScreenShareParticipantIds(): number[] {
.map((participant) => ({ .map((participant) => ({
participantId: getParticipantId(participant.identity), participantId: getParticipantId(participant.identity),
hasScreenShare: hasScreenShare:
participant.getTrackPublication(Track.Source.ScreenShare)?.track != null, participant.getTrackPublication(Track.Source.ScreenShare)?.track !=
null,
})) }))
.filter( .filter(
(entry): entry is { participantId: number; hasScreenShare: true } => (entry): entry is { participantId: number; hasScreenShare: true } =>
@ -290,18 +291,21 @@ export async function sendCallInvite(userId: number) {
const ownUserId = (await runtime.load("user_id")) as number; const ownUserId = (await runtime.load("user_id")) as number;
const privateKey = await runtime.load("private_key"); const privateKey = await runtime.load("private_key");
const ownPublicKey = await runtime.getUser(ownUserId).then( const ownPublicKey = await runtime
(data) => data.public_key, .getUser(ownUserId)
); .then((data) => data.public_key);
const remotePublicKey = await runtime.getUser(userId).then( const remotePublicKey = await runtime
(data) => data.public_key, .getUser(userId)
); .then((data) => data.public_key);
const sharedSecret = await runtime.getSharedSecret( const sharedSecret = await runtime.getSharedSecret(
privateKey, privateKey,
ownPublicKey, ownPublicKey,
remotePublicKey, remotePublicKey,
); );
const encryptedCallSecret = await runtime.encryptText(sharedSecret, callSecret); const encryptedCallSecret = await runtime.encryptText(
sharedSecret,
callSecret,
);
await runtime.send("call_invite", { await runtime.send("call_invite", {
receiver_id: userId, receiver_id: userId,
@ -373,8 +377,9 @@ export function stopWatchingFocusedStream() {
stopWatchingStream(focusedParticipantId); stopWatchingStream(focusedParticipantId);
} }
let screenShareController: ReturnType<typeof createScreenShareController> | null = let screenShareController: ReturnType<
null; typeof createScreenShareController
> | null = null;
function getScreenShareController() { function getScreenShareController() {
if (!screenShareController) { if (!screenShareController) {
@ -390,7 +395,8 @@ function getScreenShareController() {
: updater, : updater,
); );
}, },
getLocalParticipantId: () => getParticipantId(room.localParticipant.identity), getLocalParticipantId: () =>
getParticipantId(room.localParticipant.identity),
startWatching: startWatchingStream, startWatching: startWatchingStream,
stopWatching: stopWatchingStream, stopWatching: stopWatchingStream,
syncParticipantState, syncParticipantState,
@ -439,7 +445,13 @@ export async function disconnect() {
try { try {
await getScreenShareController().clearPublishedScreenShare(); await getScreenShareController().clearPublishedScreenShare();
} catch (error) { } catch (error) {
log(1, "call", "red", "Failed to clear screen share during disconnect", error); log(
1,
"call",
"red",
"Failed to clear screen share during disconnect",
error,
);
} }
useCall.setState({ useCall.setState({
@ -463,7 +475,7 @@ export async function disconnect() {
}); });
try { try {
room.disconnect(); await room.disconnect();
} catch (error) { } catch (error) {
log(1, "call", "red", "Failed to disconnect from room", error); log(1, "call", "red", "Failed to disconnect from room", error);
} finally { } finally {
@ -480,6 +492,11 @@ export async function joinCall(
sendInvite = true, sendInvite = true,
) { ) {
const runtime = requireRuntime(useCall.getState().runtime); const runtime = requireRuntime(useCall.getState().runtime);
const state = useCall.getState().state;
if (state !== "closed") {
await disconnect();
}
log(2, "call", "purple", "Call creation initialised"); log(2, "call", "purple", "Call creation initialised");
useCall.setState({ useCall.setState({
@ -754,11 +771,10 @@ export function useInitializeCall() {
if (invitedUserId != null) { if (invitedUserId != null) {
setTimeout(async () => { setTimeout(async () => {
void sendCallInvite(invitedUserId) void sendCallInvite(invitedUserId).catch((error) => {
.catch((error) => { toast("error", "Failed to send call invite.");
toast("error", "Failed to send call invite."); log(1, "call", "red", "Failed to send call invite", error);
log(1, "call", "red", "Failed to send call invite", error); });
});
}, 1000); }, 1000);
} }

View file

@ -15,7 +15,10 @@ export default function View() {
(state) => state.activeScreenShareParticipantIds, (state) => state.activeScreenShareParticipantIds,
); );
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [focusedTileSize, setFocusedTileSize] = useState({ width: 0, height: 0 }); const [focusedTileSize, setFocusedTileSize] = useState({
width: 0,
height: 0,
});
const [isFocusedTileFlush, setIsFocusedTileFlush] = useState(false); const [isFocusedTileFlush, setIsFocusedTileFlush] = useState(false);
const [participantVersion, setParticipantVersion] = useState(0); const [participantVersion, setParticipantVersion] = useState(0);
@ -42,7 +45,7 @@ export default function View() {
const participantId = Number(participant.identity); const participantId = Number(participant.identity);
return Number.isInteger(participantId) && participantId > 0; return Number.isInteger(participantId) && participantId > 0;
}); });
// eslint-disable-next-line // eslint-disable-next-line
}, [participantVersion, room]); }, [participantVersion, room]);
const userIds = useMemo( const userIds = useMemo(
@ -141,7 +144,10 @@ export default function View() {
activeScreenShareParticipantIdSet.has(focusedParticipantId); activeScreenShareParticipantIdSet.has(focusedParticipantId);
return ( return (
<div ref={containerRef} className="flex h-full w-full items-center justify-center overflow-hidden"> <div
ref={containerRef}
className="flex h-full w-full items-center justify-center overflow-hidden"
>
<div <div
className="flex max-h-full w-full flex-col items-center overflow-hidden" className="flex max-h-full w-full flex-col items-center overflow-hidden"
style={{ gap: tiles.length > 0 ? STACK_GAP_PX : 0 }} style={{ gap: tiles.length > 0 ? STACK_GAP_PX : 0 }}

View file

@ -129,13 +129,16 @@ export default function View() {
}, [room]); }, [room]);
const users = useMemo(() => { const users = useMemo(() => {
const participants = [...room.remoteParticipants.values(), room.localParticipant]; const participants = [
...room.remoteParticipants.values(),
room.localParticipant,
];
return participants.filter((participant) => { return participants.filter((participant) => {
const participantId = Number(participant.identity); const participantId = Number(participant.identity);
return Number.isInteger(participantId) && participantId > 0; return Number.isInteger(participantId) && participantId > 0;
}); });
// eslint-disable-next-line // eslint-disable-next-line
}, [participantVersion, room]); }, [participantVersion, room]);
const userIds = useMemo( const userIds = useMemo(
@ -195,9 +198,13 @@ export default function View() {
} }
return ( return (
<div ref={containerRef} style={{ <div
height: "calc(100% - 2rem)" ref={containerRef}
}} className="w-full overflow-hidden p-3"> style={{
height: "calc(100% - 2rem)",
}}
className="w-full overflow-hidden p-3"
>
<div className="flex h-full w-full flex-col items-center justify-center gap-3"> <div className="flex h-full w-full flex-col items-center justify-center gap-3">
{rows.map((row) => ( {rows.map((row) => (
<div key={row.rowIndex} className="flex justify-center gap-3"> <div key={row.rowIndex} className="flex justify-center gap-3">
@ -217,7 +224,10 @@ export default function View() {
height: layout.tileHeight, height: layout.tileHeight,
}} }}
> >
<Base type={tile.kind} participant={getParticipantById(tile.participantId)} /> <Base
type={tile.kind}
participant={getParticipantById(tile.participantId)}
/>
</div> </div>
); );
})} })}

View file

@ -7,3 +7,4 @@
- Buttons - Buttons
- Preview image - Preview image
- Mobile - Mobile
- Deaf status is not synced