From 3d196b15287f53b6e2dbb921ce9948b96dc9defc Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 6 Sep 2026 19:56:48 +0200 Subject: [PATCH] fix(mobile): notification post link and actor-name link, comments dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notification taps for post-related types (REACTION/COMMENT/MENTION/ PAW_BACK) landed on a bare "no comments yet" screen with no sign of the actual post. The comments screen now fetches the post via the existing posts.getById and shows it (image/caption/header) via the shared PostCard above the comment list. The actor's name in each notification row is now its own tappable text run that links to their profile, separate from tapping the rest of the row (which still opens the notification's target) — ported from web's notifications page, which does the same split between a Link and plain text. Also fixed: the comments screen used the static (light-only) color tokens throughout, so embedding the theme-aware PostCard produced near-invisible light text on its own light background in dark mode. Converted the screen to useTheme() like the rest of the dark-mode- converted screens. --- mobile/app/(app)/(tabs)/notifications.tsx | 18 ++++- mobile/app/(app)/posts/[postId]/comments.tsx | 85 ++++++++++++-------- mobile/src/api/types.ts | 1 + mobile/src/i18n/locales/de.json | 12 +-- mobile/src/i18n/locales/en.json | 12 +-- 5 files changed, 80 insertions(+), 48 deletions(-) diff --git a/mobile/app/(app)/(tabs)/notifications.tsx b/mobile/app/(app)/(tabs)/notifications.tsx index d0eb218..37ec830 100644 --- a/mobile/app/(app)/(tabs)/notifications.tsx +++ b/mobile/app/(app)/(tabs)/notifications.tsx @@ -29,11 +29,15 @@ const SELF_MESSAGE_KEYS: Partial> = { ADOPTION_DAY: "notifications.adoptionDay", }; -function getNotificationText(item: NotificationItem, t: TFunction): string { +// Suffix only — the actor's name is rendered separately as its own tappable +// Text run (see the row renderer) so it can link to the pet's profile +// independently of the row's own tap target, ported from web's +// notifications/page.tsx (name as a , rest as plain text). +function getNotificationSuffix(item: NotificationItem, t: TFunction): string { const selfKey = SELF_MESSAGE_KEYS[item.type]; if (selfKey) return t(selfKey); const actorKey = ACTOR_MESSAGE_KEYS[item.type]; - if (actorKey && item.actorPet) return t(actorKey, { actor: formatPetName(item.actorPet) }); + if (actorKey && item.actorPet) return t(actorKey); return item.type; } @@ -153,7 +157,14 @@ export default function NotificationsScreen() { )} - {getNotificationText(item, t)} + + {item.actorPet && ( + router.push(`/pets/${item.actorPet!.id}`)}> + {formatPetName(item.actorPet)}{" "} + + )} + {getNotificationSuffix(item, t)} + {formatRelativeTime(item.createdAt)} @@ -179,6 +190,7 @@ function makeStyles(colors: ColorTokens) { center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite }, errorText: { ...typography.body, color: colors.alertRed }, bodyText: { ...typography.body }, + actorName: { ...typography.body, fontFamily: typography.title.fontFamily }, row: { flexDirection: "row", gap: 12, padding: 12, borderRadius: 12 }, rowUnread: { backgroundColor: colors.marigoldTint }, rowText: { flex: 1, gap: 2 }, diff --git a/mobile/app/(app)/posts/[postId]/comments.tsx b/mobile/app/(app)/posts/[postId]/comments.tsx index 3d1ccd4..311ae3f 100644 --- a/mobile/app/(app)/posts/[postId]/comments.tsx +++ b/mobile/app/(app)/posts/[postId]/comments.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { View, Text, FlatList, ActivityIndicator, Pressable, KeyboardAvoidingView, Platform, StyleSheet } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; @@ -6,18 +6,20 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Trash2 } from "lucide-react-native"; 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 { PostCard } from "../../../../src/components/PostCard"; import { TextField } from "../../../../src/components/TextField"; import { Button } from "../../../../src/components/Button"; 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 { PostComment } from "../../../../src/api/types"; /** - * Read + write comments for a post, ported from web's PostDetailDialog - * comments panel (src/components/feed/PostDetailDialog.tsx) but scoped to - * just that: this is a dedicated screen, not a shared image+comments dialog - * (the image already has its own full-view route, image-viewer.tsx). + * Post + comments, ported from web's PostDetailDialog + * (src/components/feed/PostDetailDialog.tsx): notification taps and other + * deep links land here, so the post itself (image/caption/header) is shown + * via the shared PostCard, not just a bare comment list. */ export default function CommentsScreen() { const { t } = useTranslation(); @@ -26,11 +28,14 @@ export default function CommentsScreen() { const trpc = useTRPC(); const queryClient = useQueryClient(); const { activePetId } = useActivePet(); + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); const listRef = useRef>(null); const [body, setBody] = useState(""); const [pendingBody, setPendingBody] = useState(null); + const { data: post } = useQuery(trpc.posts.getById.queryOptions({ postId })); const { data, isLoading } = useQuery(trpc.comments.listByPost.queryOptions({ postId, limit: 50 })); const comments = data?.comments ?? []; @@ -79,9 +84,16 @@ export default function CommentsScreen() { data={comments} keyExtractor={(item) => item.id} contentContainerStyle={styles.list} + ListHeaderComponent={ + post && activePetId ? ( + + + + ) : null + } ListEmptyComponent={ - {t("comments.empty")} + {t("comments.empty")} } renderItem={({ item }) => ( @@ -98,7 +110,7 @@ export default function CommentsScreen() { {t("comments.sending")} - {pendingBody} + {pendingBody} ) : null @@ -135,6 +147,8 @@ type CommentRowProps = { }; function CommentRow({ comment, isOwn, onDelete, onPressPet }: CommentRowProps) { + const { colors } = useTheme(); + const styles = useMemo(() => makeStyles(colors), [colors]); return ( @@ -147,7 +161,7 @@ function CommentRow({ comment, isOwn, onDelete, onPressPet }: CommentRowProps) { {formatRelativeTime(comment.createdAt)} - {comment.body} + {comment.body} {isOwn && ( @@ -158,27 +172,32 @@ function CommentRow({ comment, isOwn, onDelete, onPressPet }: CommentRowProps) { ); } -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: 14 }, - row: { flexDirection: "row", gap: 10 }, - pending: { opacity: 0.6, marginTop: 14 }, - rowBody: { flex: 1, gap: 2 }, - rowHeader: { flexDirection: "row", alignItems: "baseline", gap: 8 }, - petName: { ...typography.body, fontFamily: typography.title.fontFamily, fontSize: 13 }, - timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0, fontSize: 11 }, - pendingLabel: { ...typography.label, textTransform: "none", letterSpacing: 0, fontSize: 11 }, - deleteButton: { alignSelf: "flex-start", padding: 4 }, - 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) { + const typography: Typography = getTypography(colors); + return StyleSheet.create({ + flex: { flex: 1, backgroundColor: colors.porchWhite }, + center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 }, + list: { padding: 16, flexGrow: 1, gap: 14 }, + postHeader: { marginBottom: 8, paddingBottom: 14, borderBottomWidth: 1, borderBottomColor: colors.hairlineGray }, + row: { flexDirection: "row", gap: 10 }, + pending: { opacity: 0.6, marginTop: 14 }, + rowBody: { flex: 1, gap: 2 }, + rowHeader: { flexDirection: "row", alignItems: "baseline", gap: 8 }, + bodyText: { ...typography.body }, + petName: { ...typography.body, fontFamily: typography.title.fontFamily, fontSize: 13 }, + timestamp: { ...typography.label, textTransform: "none", letterSpacing: 0, fontSize: 11 }, + pendingLabel: { ...typography.label, textTransform: "none", letterSpacing: 0, fontSize: 11 }, + deleteButton: { alignSelf: "flex-start", padding: 4 }, + inputRow: { + flexDirection: "row", + alignItems: "center", + gap: 8, + padding: 12, + borderTopWidth: 1, + borderTopColor: colors.hairlineGray, + backgroundColor: colors.pureWhite, + }, + input: { flex: 1 }, + sendButton: { width: 88 }, + }); +} diff --git a/mobile/src/api/types.ts b/mobile/src/api/types.ts index 0056304..fc62bfa 100644 --- a/mobile/src/api/types.ts +++ b/mobile/src/api/types.ts @@ -12,3 +12,4 @@ export type PostComment = RouterOutputs["comments"]["listByPost"]["comments"][nu export type ChatMessage = RouterOutputs["messages"]["list"]["messages"][number]; export type MilestonePost = RouterOutputs["milestones"]["listByPet"][number]; export type TrendingPost = RouterOutputs["explore"]["getTrending"]["posts"][number]; +export type PostDetail = RouterOutputs["posts"]["getById"]; diff --git a/mobile/src/i18n/locales/de.json b/mobile/src/i18n/locales/de.json index 2cc6962..c14491a 100644 --- a/mobile/src/i18n/locales/de.json +++ b/mobile/src/i18n/locales/de.json @@ -177,12 +177,12 @@ "noFollowing": "Folgt noch niemandem." }, "notifications": { - "follow": "{{actor}} folgt dir jetzt.", - "reaction": "{{actor}} hat deinen Beitrag mit einer Pfote versehen.", - "comment": "{{actor}} hat deinen Beitrag kommentiert.", - "message": "{{actor}} hat dir eine Nachricht geschickt.", - "mention": "{{actor}} hat dich erwähnt.", - "pawBack": "{{actor}} hat zurückgepfotet.", + "follow": "folgt dir jetzt.", + "reaction": "hat deinen Beitrag mit einer Pfote versehen.", + "comment": "hat deinen Beitrag kommentiert.", + "message": "hat dir eine Nachricht geschickt.", + "mention": "hat dich erwähnt.", + "pawBack": "hat zurückgepfotet.", "birthday": "🎂 Alles Gute zum Geburtstag!", "adoptionDay": "🏠 Heute ist der Adoptionstag!", "empty": "Keine Benachrichtigungen." diff --git a/mobile/src/i18n/locales/en.json b/mobile/src/i18n/locales/en.json index ec41db5..2c58bb7 100644 --- a/mobile/src/i18n/locales/en.json +++ b/mobile/src/i18n/locales/en.json @@ -177,12 +177,12 @@ "noFollowing": "Not following anyone yet." }, "notifications": { - "follow": "{{actor}} is now following you.", - "reaction": "{{actor}} pawed your post.", - "comment": "{{actor}} commented on your post.", - "message": "{{actor}} sent you a message.", - "mention": "{{actor}} mentioned you.", - "pawBack": "{{actor}} paw-backed your post.", + "follow": "is now following you.", + "reaction": "pawed your post.", + "comment": "commented on your post.", + "message": "sent you a message.", + "mention": "mentioned you.", + "pawBack": "paw-backed your post.", "birthday": "🎂 Happy birthday!", "adoptionDay": "🏠 Today is adoption day!", "empty": "No notifications."