diff --git a/mobile/app/(app)/messages/[conversationId].tsx b/mobile/app/(app)/messages/[conversationId].tsx index 6eb6fec..bf5e529 100644 --- a/mobile/app/(app)/messages/[conversationId].tsx +++ b/mobile/app/(app)/messages/[conversationId].tsx @@ -1,14 +1,16 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { View, Text, FlatList, KeyboardAvoidingView, Platform, StyleSheet } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useLocalSearchParams } from "expo-router"; import { useTranslation } from "react-i18next"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTRPC } from "../../../src/api/trpc"; import { useActivePet } from "../../../src/context/ActivePetContext"; +import { useTheme } from "../../../src/context/ThemeContext"; import { TextField } from "../../../src/components/TextField"; import { Button } from "../../../src/components/Button"; import { formatRelativeTime } from "../../../src/lib/format"; -import { colors, radii, typography } from "../../../src/theme/tokens"; +import { getTypography, radii, type ColorTokens, type Typography } from "../../../src/theme/tokens"; import type { ChatMessage } from "../../../src/api/types"; const POLL_INTERVAL_MS = 4000; @@ -24,6 +26,9 @@ export default function ConversationScreen() { const trpc = useTRPC(); const queryClient = useQueryClient(); const { activePetId } = useActivePet(); + const { colors } = useTheme(); + const insets = useSafeAreaInsets(); + const styles = useMemo(() => makeStyles(colors, insets.bottom), [colors, insets.bottom]); const listRef = useRef>(null); const [body, setBody] = useState(""); @@ -75,7 +80,7 @@ export default function ConversationScreen() { renderItem={({ item }) => } ListEmptyComponent={ - {t("messages.empty")} + {t("messages.empty")} } /> @@ -101,6 +106,8 @@ export default function ConversationScreen() { } function MessageBubble({ message, isOwn }: { message: ChatMessage; isOwn: boolean }) { + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors, 0), [colors]); return ( @@ -111,27 +118,37 @@ function MessageBubble({ message, isOwn }: { message: ChatMessage; isOwn: boolea ); } -const styles = StyleSheet.create({ - flex: { flex: 1, backgroundColor: colors.porchWhite }, - center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 }, - list: { padding: 16, flexGrow: 1, gap: 10 }, - bubbleRow: { alignItems: "flex-start", gap: 3, maxWidth: "80%" }, - bubbleRowOwn: { alignSelf: "flex-end", alignItems: "flex-end" }, - bubble: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: radii.xl }, - bubbleOther: { backgroundColor: colors.whisperGray, borderBottomLeftRadius: 4 }, - bubbleOwn: { backgroundColor: colors.marigoldOrange, borderBottomRightRadius: 4 }, - bubbleText: { ...typography.body }, - bubbleTextOwn: { color: colors.pureWhite }, - timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0, fontSize: 10 }, - inputRow: { - flexDirection: "row", - alignItems: "center", - gap: 8, - padding: 12, - borderTopWidth: 1, - borderTopColor: colors.hairlineGray, - backgroundColor: colors.pureWhite, - }, - input: { flex: 1 }, - sendButton: { width: 88 }, -}); +function makeStyles(colors: ColorTokens, insetsBottom: number) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + flex: { flex: 1, backgroundColor: colors.porchWhite }, + center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 }, + emptyText: { ...typography.body }, + list: { padding: 16, flexGrow: 1, gap: 10 }, + bubbleRow: { alignItems: "flex-start", gap: 3, maxWidth: "80%" }, + bubbleRowOwn: { alignSelf: "flex-end", alignItems: "flex-end" }, + bubble: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: radii.xl }, + bubbleOther: { backgroundColor: colors.whisperGray, borderBottomLeftRadius: 4 }, + bubbleOwn: { backgroundColor: colors.marigoldOrange, borderBottomRightRadius: 4 }, + bubbleText: { ...typography.body }, + bubbleTextOwn: { color: colors.pureWhite }, + timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0, fontSize: 10 }, + inputRow: { + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingHorizontal: 12, + paddingTop: 12, + // Extra breathing room above Android's gesture-nav strip — without it + // the send button sat right at the screen edge, right where the + // system back/home gesture also listens, making it error-prone to + // tap (found.md: "in einem Schutzbereich sehr schwer anklickbar"). + paddingBottom: Math.max(12, insetsBottom), + borderTopWidth: 1, + borderTopColor: colors.hairlineGray, + backgroundColor: colors.pureWhite, + }, + input: { flex: 1 }, + sendButton: { width: 88 }, + }); +} diff --git a/mobile/app/(app)/messages/index.tsx b/mobile/app/(app)/messages/index.tsx index 7a6c579..dd0fb82 100644 --- a/mobile/app/(app)/messages/index.tsx +++ b/mobile/app/(app)/messages/index.tsx @@ -1,12 +1,14 @@ +import { useMemo } from "react"; import { View, Text, FlatList, Pressable, ActivityIndicator, StyleSheet } from "react-native"; import { useQuery } from "@tanstack/react-query"; import { useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; import { useTRPC } from "../../../src/api/trpc"; import { useActivePet } from "../../../src/context/ActivePetContext"; +import { useTheme } from "../../../src/context/ThemeContext"; import { PetAvatar } from "../../../src/components/PetAvatar"; import { formatPetName, formatRelativeTime } from "../../../src/lib/format"; -import { colors, typography } from "../../../src/theme/tokens"; +import { getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens"; import type { Conversation } from "../../../src/api/types"; /** Conversations list (conversations.list) — taps a row to open the chat detail/compose screen. */ @@ -15,6 +17,8 @@ export default function MessagesScreen() { const trpc = useTRPC(); const router = useRouter(); const { activePetId } = useActivePet(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const { data, isLoading, error } = useQuery({ ...trpc.conversations.list.queryOptions({ petId: activePetId ?? "" }), @@ -48,7 +52,7 @@ export default function MessagesScreen() { )} ListEmptyComponent={ - {t("messages.noConversations")} + {t("messages.noConversations")} } /> @@ -56,6 +60,8 @@ export default function MessagesScreen() { } function ConversationRow({ conversation, onPress }: { conversation: Conversation; onPress: () => void }) { + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); return ( @@ -79,25 +85,29 @@ function ConversationRow({ conversation, onPress }: { conversation: Conversation ); } -const styles = StyleSheet.create({ - list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, - separator: { height: 12 }, - center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, - errorText: { ...typography.body, color: colors.alertRed }, - row: { flexDirection: "row", alignItems: "center", gap: 12 }, - rowText: { flex: 1, gap: 2 }, - nameRow: { flexDirection: "row", alignItems: "center", gap: 6 }, - name: { ...typography.body, fontFamily: typography.title.fontFamily }, - preview: { ...typography.body, color: colors.quietGray }, - timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0 }, - unreadDot: { - backgroundColor: colors.marigoldOrange, - borderRadius: 8, - minWidth: 16, - height: 16, - alignItems: "center", - justifyContent: "center", - paddingHorizontal: 3, - }, - unreadCount: { color: colors.pureWhite, fontSize: 10 }, -}); +function makeStyles(colors: ColorTokens) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite }, + separator: { height: 12 }, + center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, + errorText: { ...typography.body, color: colors.alertRed }, + emptyText: { ...typography.body }, + row: { flexDirection: "row", alignItems: "center", gap: 12 }, + rowText: { flex: 1, gap: 2 }, + nameRow: { flexDirection: "row", alignItems: "center", gap: 6 }, + name: { ...typography.body, fontFamily: typography.title.fontFamily }, + preview: { ...typography.body, color: colors.quietGray }, + timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0 }, + unreadDot: { + backgroundColor: colors.marigoldOrange, + borderRadius: 8, + minWidth: 16, + height: 16, + alignItems: "center", + justifyContent: "center", + paddingHorizontal: 3, + }, + unreadCount: { color: colors.pureWhite, fontSize: 10 }, + }); +} diff --git a/mobile/app/(app)/posts/[postId]/comments.tsx b/mobile/app/(app)/posts/[postId]/comments.tsx index 311ae3f..72efe11 100644 --- a/mobile/app/(app)/posts/[postId]/comments.tsx +++ b/mobile/app/(app)/posts/[postId]/comments.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo, useRef, useState } from "react"; import { View, Text, FlatList, ActivityIndicator, Pressable, KeyboardAvoidingView, Platform, StyleSheet } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -29,7 +30,8 @@ export default function CommentsScreen() { const queryClient = useQueryClient(); const { activePetId } = useActivePet(); const { colors } = useTheme(); - const styles = useMemo(() => makeStyles(colors), [colors]); + const insets = useSafeAreaInsets(); + const styles = useMemo(() => makeStyles(colors, insets.bottom), [colors, insets.bottom]); const listRef = useRef>(null); const [body, setBody] = useState(""); @@ -148,7 +150,7 @@ type CommentRowProps = { function CommentRow({ comment, isOwn, onDelete, onPressPet }: CommentRowProps) { const { colors } = useTheme(); - const styles = useMemo(() => makeStyles(colors), [colors]); + const styles = useMemo(() => makeStyles(colors, 0), [colors]); return ( @@ -172,7 +174,7 @@ function CommentRow({ comment, isOwn, onDelete, onPressPet }: CommentRowProps) { ); } -function makeStyles(colors: ColorTokens) { +function makeStyles(colors: ColorTokens, insetsBottom: number) { const typography: Typography = getTypography(colors); return StyleSheet.create({ flex: { flex: 1, backgroundColor: colors.porchWhite }, @@ -192,7 +194,12 @@ function makeStyles(colors: ColorTokens) { flexDirection: "row", alignItems: "center", gap: 8, - padding: 12, + paddingHorizontal: 12, + paddingTop: 12, + // See messages/[conversationId].tsx's identical fix — extra breathing + // room above Android's gesture-nav strip so the send button isn't + // sitting right where the system back/home gesture also listens. + paddingBottom: Math.max(12, insetsBottom), borderTopWidth: 1, borderTopColor: colors.hairlineGray, backgroundColor: colors.pureWhite,