diff --git a/app/(protected)/videocall/(pages)/CallScreen.tsx b/app/(protected)/videocall/(pages)/CallScreen.tsx index e850b6a..21651ea 100644 --- a/app/(protected)/videocall/(pages)/CallScreen.tsx +++ b/app/(protected)/videocall/(pages)/CallScreen.tsx @@ -8,7 +8,7 @@ import { FontAwesome6, } from "@expo/vector-icons"; import { useSafeAreaControl } from "@/components/SafeArea"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; const tCall = createT("videocall.call"); @@ -30,11 +30,15 @@ interface Props { icebreakerLoading: boolean; icebreakerIndex: number; icebreakerOpacity: Animated.Value; + isLiked: boolean; + receivedLike: boolean; + mutualLike: boolean; onToggleControls: () => void; onToggleMute: () => void; onFlipCamera: () => void; onStop: () => void; onLike: () => void; + onLikeBack: () => void; onNextUser: () => void; onReaction: () => void; onIcebreaker: () => void; @@ -54,15 +58,30 @@ export function CallScreen({ icebreakerLoading, icebreakerIndex, icebreakerOpacity, + isLiked, + receivedLike, + mutualLike, onToggleControls, onToggleMute, onFlipCamera, onStop, onLike, + onLikeBack, onNextUser, onReaction, onIcebreaker, }: Props) { + const likeNotifOpacity = useRef(new Animated.Value(0)).current; + const showBanner = receivedLike || mutualLike; + + useEffect(() => { + Animated.timing(likeNotifOpacity, { + toValue: showBanner ? 1 : 0, + duration: 300, + useNativeDriver: true, + }).start(); + }, [showBanner, likeNotifOpacity]); + const { setDisableSafeArea } = useSafeAreaControl(); useEffect(() => { @@ -73,6 +92,10 @@ export function CallScreen({ }; }); + const likeButtonVariant = mutualLike || receivedLike ? "liked" : "success"; + const likeButtonIcon = isLiked || mutualLike ? "heart" : "heart-outline"; + const likeButtonPress = mutualLike ? undefined : receivedLike ? onLikeBack : onLike; + return ( @@ -135,11 +158,11 @@ export function CallScreen({ } /> {})} + variant={likeButtonVariant} icon={ @@ -174,6 +197,23 @@ export function CallScreen({ + + + {mutualLike ? ( + {"It's a match! 💬"} + ) : ( + <> + You got liked! + + Like back + + + )} + + {localUrl && ( void; icon: React.ReactNode; - variant?: "default" | "success" | "danger"; + variant?: "default" | "success" | "danger" | "liked"; }) { return ( {icon} @@ -281,6 +322,32 @@ const s = StyleSheet.create({ }, controlButtonSuccess: { backgroundColor: "#45c466", borderColor: "#45c466" }, controlButtonDanger: { backgroundColor: "#df1d1d", borderColor: "#df1d1d" }, + controlButtonLiked: { backgroundColor: "#e91e8c", borderColor: "#e91e8c" }, + likeNotification: { + position: "absolute", + top: 120, + alignSelf: "center", + backgroundColor: "#e91e8c", + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 10, + flexDirection: "row", + alignItems: "center", + gap: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 6, + }, + likeNotificationText: { color: "#fff", fontSize: 14, fontWeight: "600" as const }, + likeBackButton: { + backgroundColor: "rgba(255,255,255,0.25)", + borderRadius: 12, + paddingHorizontal: 10, + paddingVertical: 4, + }, + likeBackText: { color: "#fff", fontSize: 13, fontWeight: "700" as const }, icebreakerBubble: { position: "absolute", left: 14, diff --git a/app/(protected)/videocall/index.tsx b/app/(protected)/videocall/index.tsx index 761a151..614f276 100644 --- a/app/(protected)/videocall/index.tsx +++ b/app/(protected)/videocall/index.tsx @@ -117,6 +117,23 @@ export default function VideoCall() { const [_matchedUserId, setMatchedUserId] = useState(null); const [gatewayRoomId, setGatewayRoomId] = useState(null); + const [isLiked, setIsLiked] = useState(false); + const [receivedLike, setReceivedLike] = useState(false); + const [mutualLike, setMutualLike] = useState(false); + const chatRoomIdRef = useRef(null); + + useEffect(() => { + if (!receivedLike) return; + const t = setTimeout(() => setReceivedLike(false), 5000); + return () => clearTimeout(t); + }, [receivedLike]); + + useEffect(() => { + if (!mutualLike) return; + const t = setTimeout(() => setMutualLike(false), 4000); + return () => clearTimeout(t); + }, [mutualLike]); + const matchmakingSocketRef = useRef(null); const stopTracksRef = useRef<() => void>(() => {}); const connectingIntentRef = useRef(false); @@ -462,6 +479,7 @@ export default function VideoCall() { await createRecvTransport(device); const socket: Socket = io(BASE_URL, { + auth: { token }, query: { peerId: peerIdRef.current, roomId: currentRoomId }, transports: ["websocket"], forceNew: true, @@ -484,6 +502,20 @@ export default function VideoCall() { }, ); + socket.on("receive_like", () => { + setReceivedLike(true); + }); + + socket.on("mutual_like", ({ chatRoomId: id }: { chatRoomId: string }) => { + setMutualLike(true); + setReceivedLike(false); + chatRoomIdRef.current = id; + }); + + socket.on("peer_left", () => { + handleNextUserRef.current(); + }); + await new Promise((resolve) => { if (socket.connected) resolve(); else socket.once("connect", () => resolve()); @@ -504,6 +536,7 @@ export default function VideoCall() { } }, [ screen, + token, api, consumeProducer, createRecvTransport, @@ -556,6 +589,12 @@ export default function VideoCall() { body: JSON.stringify({ peerId: peerIdRef.current }), }); } catch {} + try { + await api("/matchmaking/decline", { + method: "POST", + body: JSON.stringify({ roomId: currentRoomId }), + }); + } catch {} } try { @@ -584,6 +623,10 @@ export default function VideoCall() { setMatchedUserId(null); setMatchState("idle"); setMatchmakingReady(false); + setIsLiked(false); + setReceivedLike(false); + setMutualLike(false); + chatRoomIdRef.current = null; roomIdRef.current = null; matchmakingSocketRef.current?.disconnect(); @@ -611,6 +654,16 @@ export default function VideoCall() { [api, log], ); + const stopCallRef = useRef(stopCall); + useEffect(() => { + stopCallRef.current = stopCall; + }, [stopCall]); + + const handleNextUserRef = useRef<() => Promise>(async () => {}); + useEffect(() => { + handleNextUserRef.current = handleNextUser; + }); + const handleStartConnecting = useCallback( async (_camera: string, _mic: string, _interests: string[]) => { connectingIntentRef.current = true; @@ -702,10 +755,90 @@ export default function VideoCall() { } function handleLike() { - Alert.alert("Liked", "User wurde geliked."); + if (isLiked) return; + socketRef.current?.emit("send_like"); + setIsLiked(true); + } + + function handleLikeBack() { + console.log("[LikeBack] User liked back in room:", roomIdRef.current); + socketRef.current?.emit("send_like_back"); } - function handleNextUser() { - Alert.alert("Next user", "Hier kannst du den nächsten Match laden."); + + async function handleNextUser() { + const roomId = roomIdRef.current; + + // Decline current match so these two users won't match again + if (roomId) { + try { + await api("/matchmaking/decline", { + method: "POST", + body: JSON.stringify({ roomId }), + }); + } catch {} + } + + // Clean up video session without touching the matchmaking socket + localStreamRef.current?.getTracks()?.forEach((t: any) => t.stop()); + localStreamRef.current = null; + startingRef.current = false; + + if (icebreakerTimeoutRef.current) clearTimeout(icebreakerTimeoutRef.current); + setIcebreakerVisible(false); + icebreakerOpacity.setValue(0); + + socketRef.current?.disconnect(); + socketRef.current = null; + + if (roomId) { + try { + await api(`/video/room/${roomId}/leave`, { + method: "DELETE", + body: JSON.stringify({ peerId: peerIdRef.current }), + }); + } catch {} + } + + sendTransportRef.current?.close(); + recvTransportRef.current?.close(); + sendTransportRef.current = null; + recvTransportRef.current = null; + remoteStreamRef.current = new MediaStream(); + + consumersRef.current.forEach((c) => { + try { + c.close(); + } catch {} + }); + consumersRef.current.clear(); + consumedProducerIdsRef.current.clear(); + consumingProducerIdsRef.current.clear(); + remoteVideoStreamRef.current = null; + + iceRefreshTimerRef.current && clearTimeout(iceRefreshTimerRef.current); + iceRefreshTimerRef.current = null; + + // Generate a fresh peerId for the new call + peerIdRef.current = `peer-${Math.random().toString(36).slice(2, 10)}`; + + setLocalUrl(null); + setRemoteUrl(null); + setGatewayRoomId(null); + setMatchedUserId(null); + setMatchState("idle"); + setIsLiked(false); + setReceivedLike(false); + setMutualLike(false); + chatRoomIdRef.current = null; + roomIdRef.current = null; + + // Re-enter matchmaking — deactivate first to guarantee IDLE state + setScreen("connecting"); + await connectMatchmakingGateway(); + try { + await deactivateMatchmakingRef.current(); + } catch {} + await activateMatchmaking(); } function handleReaction() { Alert.alert("Reaction", "Emoji Picker oder Quick Reaction öffnen.");