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:
1 parent
04a193b8dd
commit
768d0dda23
3 files changed
+89
-55
No files matched your search
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user