fix(mobile): dark mode for Messages, safe-area padding for input bars

Messages (conversation list + chat detail) used the static (light-only)
color tokens instead of useTheme() — same bug class as the earlier
auth-screen/comments/RepostCard fixes this session.

Also fixed: the comment and chat input rows sat flush against the
bottom edge with a flat 12px padding, no bottom safe-area inset. On
devices with Android's gesture nav bar this put the send button right
where the system back/home gesture also listens, making it error-prone
to tap ("in einem Schutzbereich sehr schwer anklickbar"). Both input
rows now pad by max(12, insets.bottom) instead of a flat 12px.
This commit is contained in:
admin committed 2026-09-07 05:42:52 +02:00
1 parent 04a193b8dd
commit 768d0dda23
3 files changed
+89 -55

No files matched your search

+44 -27
View File
@@ -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<FlatList<ChatMessage>>(null);
const [body, setBody] = useState("");
@@ -75,7 +80,7 @@ export default function ConversationScreen() {
renderItem={({ item }) => <MessageBubble message={item} isOwn={item.senderPet.id === activePetId} />}
ListEmptyComponent={
<View style={styles.center}>
<Text style={typography.body}>{t("messages.empty")}</Text>
<Text style={styles.emptyText}>{t("messages.empty")}</Text>
</View>
}
/>
@@ -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 (
<View style={[styles.bubbleRow, isOwn && styles.bubbleRowOwn]}>
<View style={[styles.bubble, isOwn ? styles.bubbleOwn : styles.bubbleOther]}>
@@ -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 },
});
}
+34 -24
View File
@@ -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={
<View style={styles.center}>
<Text style={typography.body}>{t("messages.noConversations")}</Text>
<Text style={styles.emptyText}>{t("messages.noConversations")}</Text>
</View>
}
/>
@@ -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 (
<Pressable style={styles.row} onPress={onPress}>
<PetAvatar name={conversation.otherPet.name} avatarKey={conversation.otherPet.avatarKey} />
@@ -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 },
});
}
+11 -4
View File
@@ -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<FlatList<PostComment>>(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 (
<View style={styles.row}>
<Pressable onPress={onPressPet}>
@@ -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,