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.");