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:
1 parent
73ea97ac16
commit
3d196b1528
5 files changed
+80
-48
No files matched your search
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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"];
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in new issue
Block a user