From 474bbb6829a7f947e149ed321edddae096b2fe7f Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sun, 4 Jan 2026 19:55:32 +0000 Subject: [PATCH] Collapse match card header on keyboard open --- .../ios/Sauci.xcodeproj/project.pbxproj | 10 +- .../features/chat/components/ChatHeader.tsx | 230 ++++++++++-------- 2 files changed, 141 insertions(+), 99 deletions(-) diff --git a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj index 4ee1cf1..e449118 100644 --- a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj @@ -505,7 +505,10 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -560,7 +563,10 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/apps/mobile/src/features/chat/components/ChatHeader.tsx b/apps/mobile/src/features/chat/components/ChatHeader.tsx index 1920411..41ca702 100644 --- a/apps/mobile/src/features/chat/components/ChatHeader.tsx +++ b/apps/mobile/src/features/chat/components/ChatHeader.tsx @@ -3,7 +3,14 @@ import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { Image } from 'expo-image'; import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; -import Animated, { FadeIn, FadeInDown } from 'react-native-reanimated'; +import Animated, { + FadeIn, + FadeInDown, + useAnimatedKeyboard, + useAnimatedStyle, + interpolate, + Extrapolation +} from 'react-native-reanimated'; import { EdgeInsets } from 'react-native-safe-area-context'; import { DecorativeSeparator } from '../../../components/ui'; @@ -30,6 +37,29 @@ export const ChatHeader: React.FC = ({ onBack, }) => { const isYesYes = match?.match_type === 'yes_yes'; + + // Modern reanimated keyboard hook + const keyboard = useAnimatedKeyboard(); + + const containerAnimatedStyle = useAnimatedStyle(() => { + // Collapse when keyboard height exceeds 50px + const animationValue = interpolate( + keyboard.height.value, + [0, 60], + [0, 1], + Extrapolation.CLAMP + ); + + return { + opacity: interpolate(animationValue, [0, 1], [1, 0]), + maxHeight: interpolate(animationValue, [0, 1], [500, 0]), // Assuming card max height + marginTop: interpolate(animationValue, [0, 1], [spacing.sm, 0]), + transform: [ + { scale: interpolate(animationValue, [0, 1], [1, 0.95]) } + ], + overflow: 'hidden', + }; + }); return ( <> @@ -102,106 +132,108 @@ export const ChatHeader: React.FC = ({ {match?.question && ( - {/* Card gradient background */} - - - {/* Top glow for YES+YES */} - {isYesYes && ( + + {/* Card gradient background */} - )} - {/* Match Label */} - - {isYesYes ? "PERFECT MATCH" : "SOFT MATCH"} - + {/* Top glow for YES+YES */} + {isYesYes && ( + + )} - {/* Decorative Separator */} - - - {/* Question and Responses - Show who answered what */} - {match.responses && match.responses.length > 0 ? ( - - {(() => { - // Sort responses by created_at to determine who answered first - const sortedResponses = [...match.responses].sort( - (a: any, b: any) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() - ); - const isTwoPart = !!match.question.partner_text; - - return sortedResponses.map((response: any, index: number) => { - const isUser = response.user_id === user?.id; - const isYes = response.answer === 'yes'; - const name = isUser ? 'You' : (response.profiles?.name?.split(' ')[0] || 'Partner'); - - // First responder sees main text, second sees partner_text - const questionText = isTwoPart && index === 1 - ? match.question.partner_text - : match.question.text; - - return ( - - {/* Name and Answer Badge */} - - {name} - {isYes ? ( - - - YES - - ) : ( - - - MAYBE - - )} - - {/* Question they saw */} - - "{questionText}" - - - ); - }); - })()} - - ) : ( - - "{match.question.text}" + {/* Match Label */} + + {isYesYes ? "PERFECT MATCH" : "SOFT MATCH"} - )} - {/* Premium border */} - + {/* Decorative Separator */} + + + {/* Question and Responses - Show who answered what */} + {match.responses && match.responses.length > 0 ? ( + + {(() => { + // Sort responses by created_at to determine who answered first + const sortedResponses = [...match.responses].sort( + (a: any, b: any) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + ); + const isTwoPart = !!match.question.partner_text; + + return sortedResponses.map((response: any, index: number) => { + const isUser = response.user_id === user?.id; + const isYes = response.answer === 'yes'; + const name = isUser ? 'You' : (response.profiles?.name?.split(' ')[0] || 'Partner'); + + // First responder sees main text, second sees partner_text + const questionText = isTwoPart && index === 1 + ? match.question.partner_text + : match.question.text; + + return ( + + {/* Name and Answer Badge */} + + {name} + {isYes ? ( + + + YES + + ) : ( + + + MAYBE + + )} + + {/* Question they saw */} + + "{questionText}" + + + ); + }); + })()} + + ) : ( + + "{match.question.text}" + + )} + + {/* Premium border */} + + )} @@ -301,10 +333,14 @@ const styles = StyleSheet.create({ height: 1, backgroundColor: `${ACCENT_RGBA}0.15)`, }, - matchCard: { + matchCardContainer: { marginHorizontal: spacing.md, - marginTop: spacing.sm, - marginBottom: spacing.xs, + // Vertical margins are handled by animation + borderRadius: radius.lg, + overflow: 'hidden', + }, + matchCard: { + // Margins moved to container borderRadius: radius.lg, paddingVertical: spacing.md, paddingHorizontal: spacing.md,