fix(mobile): notification post link and actor-name link, comments dark mode

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.
This commit is contained in:
admin committed 2026-09-06 19:56:48 +02:00
1 parent 73ea97ac16
commit 3d196b1528
5 files changed
+80 -48

No files matched your search

+15 -3
View File
@@ -29,11 +29,15 @@ const SELF_MESSAGE_KEYS: Partial<Record<NotificationItem["type"], string>> = {
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 <Link>, 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() {
</View>
)}
<View style={styles.rowText}>
<Text style={styles.bodyText}>{getNotificationText(item, t)}</Text>
<Text style={styles.bodyText}>
{item.actorPet && (
<Text style={styles.actorName} onPress={() => router.push(`/pets/${item.actorPet!.id}`)}>
{formatPetName(item.actorPet)}{" "}
</Text>
)}
{getNotificationSuffix(item, t)}
</Text>
<Text style={styles.timestamp}>{formatRelativeTime(item.createdAt)}</Text>
</View>
</Pressable>
@@ -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 },
+52 -33
View File
@@ -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<FlatList<PostComment>>(null);
const [body, setBody] = useState("");
const [pendingBody, setPendingBody] = useState<string | null>(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 ? (
<View style={styles.postHeader}>
<PostCard post={post} actingPetId={activePetId} />
</View>
) : null
}
ListEmptyComponent={
<View style={styles.center}>
<Text style={typography.body}>{t("comments.empty")}</Text>
<Text style={styles.bodyText}>{t("comments.empty")}</Text>
</View>
}
renderItem={({ item }) => (
@@ -98,7 +110,7 @@ export default function CommentsScreen() {
<PetAvatar name="…" size="sm" />
<View style={styles.rowBody}>
<Text style={styles.pendingLabel}>{t("comments.sending")}</Text>
<Text style={typography.body}>{pendingBody}</Text>
<Text style={styles.bodyText}>{pendingBody}</Text>
</View>
</View>
) : null
@@ -135,6 +147,8 @@ type CommentRowProps = {
};
function CommentRow({ comment, isOwn, onDelete, onPressPet }: CommentRowProps) {
const { colors } = useTheme();
const styles = useMemo(() => makeStyles(colors), [colors]);
return (
<View style={styles.row}>
<Pressable onPress={onPressPet}>
@@ -147,7 +161,7 @@ function CommentRow({ comment, isOwn, onDelete, onPressPet }: CommentRowProps) {
</Pressable>
<Text style={styles.timestamp}>{formatRelativeTime(comment.createdAt)}</Text>
</View>
<Text style={typography.body}>{comment.body}</Text>
<Text style={styles.bodyText}>{comment.body}</Text>
</View>
{isOwn && (
<Pressable onPress={onDelete} hitSlop={8} style={styles.deleteButton}>
@@ -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 },
});
}
+1
View File
@@ -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"];
+6 -6
View File
@@ -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."
+6 -6
View File
@@ -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."