fix(mobile): notification separators/navigation, own-post delete option
Notifications: add a visible hairline separator between rows and make each row tappable — it marks read and navigates to the relevant post (comments screen) or pet profile, porting web's getHref()/getPostId() routing logic. Feed: the "..." menu on a post now opens a delete confirmation instead of the report sheet when the post belongs to the currently active pet; report sheet stays unchanged for posts from other pets. Reuses the pre-existing posts.delete mutation, no backend changes needed.
This commit is contained in:
1 parent
6b01046ded
commit
73ea97ac16
5 files changed
+92
-15
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { View, Text, FlatList, Pressable, ActivityIndicator, StyleSheet } from "react-native";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useFocusEffect, useRouter } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -37,10 +37,38 @@ function getNotificationText(item: NotificationItem, t: TFunction): string {
|
||||
return item.type;
|
||||
}
|
||||
|
||||
/** Port of web's notifications list — mark-read on tap, no deep-linking to the target post/comment yet. */
|
||||
/**
|
||||
* Port of web's notifications/page.tsx getHref()/getPostId(): FOLLOW goes to
|
||||
* the actor's profile, BIRTHDAY/ADOPTION_DAY to this pet's own health page
|
||||
* (the anniversary is about *this* pet, there's no other actor to visit),
|
||||
* and REACTION/COMMENT/MENTION/PAW_BACK go to the post. Web opens the post
|
||||
* in an inline dialog; mobile has no such dialog, so this goes to the
|
||||
* comments screen instead — the closest existing route to "the post".
|
||||
* MESSAGE has no target here either, matching web (it doesn't handle it
|
||||
* there so this doesn't try to be smarter about it).
|
||||
*/
|
||||
function getNotificationRoute(item: NotificationItem): string | null {
|
||||
switch (item.type) {
|
||||
case "FOLLOW":
|
||||
return item.actorPet ? `/pets/${item.actorPet.id}` : null;
|
||||
case "BIRTHDAY":
|
||||
case "ADOPTION_DAY":
|
||||
return item.recipientPetId ? `/pets/${item.recipientPetId}/health` : null;
|
||||
case "REACTION":
|
||||
case "COMMENT":
|
||||
case "MENTION":
|
||||
case "PAW_BACK":
|
||||
return item.post?.id ? `/posts/${item.post.id}/comments` : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Port of web's notifications list — tapping a row marks it read and navigates to its target (post/pet). */
|
||||
export default function NotificationsScreen() {
|
||||
const { t } = useTranslation();
|
||||
const trpc = useTRPC();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { activePetId } = useActivePet();
|
||||
const { colors } = useTheme();
|
||||
@@ -102,7 +130,7 @@ export default function NotificationsScreen() {
|
||||
data={notifications}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.list}
|
||||
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||
ItemSeparatorComponent={() => <View style={styles.separatorLine} />}
|
||||
onEndReachedThreshold={0.5}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
|
||||
@@ -111,7 +139,11 @@ export default function NotificationsScreen() {
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
style={[styles.row, !item.read && styles.rowUnread]}
|
||||
onPress={() => !item.read && markRead.mutate({ notificationId: item.id })}
|
||||
onPress={() => {
|
||||
if (!item.read) markRead.mutate({ notificationId: item.id });
|
||||
const route = getNotificationRoute(item);
|
||||
if (route) router.push(route);
|
||||
}}
|
||||
>
|
||||
{item.actorPet ? (
|
||||
<PetAvatar name={item.actorPet.name} avatarKey={item.actorPet.avatarKey} />
|
||||
@@ -139,7 +171,11 @@ function makeStyles(colors: ColorTokens) {
|
||||
const typography: Typography = getTypography(colors);
|
||||
return StyleSheet.create({
|
||||
list: { padding: 16, flexGrow: 1, backgroundColor: colors.porchWhite },
|
||||
separator: { height: 8 },
|
||||
// A visible hairline plus vertical breathing room — found.md: the old
|
||||
// 8px gap alone didn't read as separate rows, especially between two
|
||||
// unread (tinted-background) notifications sitting right on top of
|
||||
// each other.
|
||||
separatorLine: { height: 1, backgroundColor: colors.hairlineGray, marginVertical: 6 },
|
||||
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, backgroundColor: colors.porchWhite },
|
||||
errorText: { ...typography.body, color: colors.alertRed },
|
||||
bodyText: { ...typography.body },
|
||||
|
||||
@@ -30,3 +30,10 @@ TODO Neu2:
|
||||
→ Links + "Version 1.0.0" unterhalb der Sprachauswahl ergänzt, live getestet (Impressum öffnet im Browser).
|
||||
- [x] Symbole für Deutsch Englisch und Automatisch im Meine tiere menü ändern. auf flaggen z.b.
|
||||
→ 🇩🇪 / 🇬🇧 / 🌐 statt Icon, live bestätigt.
|
||||
- [x] Benachrichtigungen trennen zwischen den einzelnen Benachrichtigungen für mehr optik
|
||||
→ Sichtbare Trennlinie zwischen den Einträgen ergänzt, live bestätigt.
|
||||
- [x] Benachrichtigungen sind nicht anklickbar, sollten aber zu dem jeweiligen Post / Pet gehen.
|
||||
→ Tap markiert als gelesen und navigiert zum Ziel (Post-Kommentare bzw. Tierprofil je nach Typ), live bestätigt.
|
||||
- [x] Drei Punkte, menü im Feed, falls es der eigene Beitrag ist, lösch funktion hinzufügen, statt Report.
|
||||
→ Bei eigenem Beitrag öffnet "⋮" jetzt einen Lösch-Bestätigungsdialog statt der Report-Ansicht, bei fremden Beiträgen bleibt Report wie gewohnt; beides live bestätigt.
|
||||
-
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { View, Text, Pressable, StyleSheet, ScrollView, useWindowDimensions } from "react-native";
|
||||
import { View, Text, Pressable, Alert, StyleSheet, ScrollView, useWindowDimensions } from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { MessageCircle, MoreVertical, Flame } from "lucide-react-native";
|
||||
import { useTRPC } from "../api/trpc";
|
||||
import { PetAvatar } from "./PetAvatar";
|
||||
import { PetIdentityBadges } from "./PetIdentityBadges";
|
||||
import { PawButton } from "./PawButton";
|
||||
@@ -79,10 +81,23 @@ export function PostCard({ post, actingPetId, isTrending }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { width } = useWindowDimensions();
|
||||
const router = useRouter();
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
const { colors } = useTheme();
|
||||
const styles = useMemo(() => makeStyles(colors), [colors]);
|
||||
const cardWidth = width - 32; // matches the 16px screen padding used by the feed list
|
||||
const [reportOpen, setReportOpen] = useState(false);
|
||||
const isOwnPost = post.pet.id === actingPetId;
|
||||
|
||||
const deleteMutation = useMutation(
|
||||
trpc.posts.delete.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(trpc.feed.pathFilter());
|
||||
queryClient.invalidateQueries(trpc.posts.pathFilter());
|
||||
},
|
||||
onError: () => Alert.alert(t("post.deleteError")),
|
||||
})
|
||||
);
|
||||
|
||||
// Reposts ("Paw-Backs") delegate to RepostCard — a read-only wrapper
|
||||
// around the original post, no action row (matches web).
|
||||
@@ -95,6 +110,17 @@ export function PostCard({ post, actingPetId, isTrending }: Props) {
|
||||
router.push({ pathname: "/image-viewer", params: { images: JSON.stringify(uris), index: String(index) } });
|
||||
};
|
||||
|
||||
const handleMorePress = () => {
|
||||
if (isOwnPost) {
|
||||
Alert.alert(t("post.deleteConfirmTitle"), t("post.deleteConfirmMessage"), [
|
||||
{ text: t("common.cancel"), style: "cancel" },
|
||||
{ text: t("common.delete"), style: "destructive", onPress: () => deleteMutation.mutate({ postId: post.id }) },
|
||||
]);
|
||||
} else {
|
||||
setReportOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<View style={styles.headerRow}>
|
||||
@@ -114,16 +140,18 @@ export function PostCard({ post, actingPetId, isTrending }: Props) {
|
||||
<Text style={styles.timestamp}>{formatRelativeTime(post.createdAt)}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
<Pressable style={styles.moreButton} onPress={() => setReportOpen(true)} hitSlop={8}>
|
||||
<Pressable style={styles.moreButton} onPress={handleMorePress} hitSlop={8}>
|
||||
<MoreVertical size={18} color={colors.quietGray} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<ReportSheet
|
||||
visible={reportOpen}
|
||||
onClose={() => setReportOpen(false)}
|
||||
reporterPetId={actingPetId}
|
||||
targetPostId={post.id}
|
||||
/>
|
||||
{!isOwnPost && (
|
||||
<ReportSheet
|
||||
visible={reportOpen}
|
||||
onClose={() => setReportOpen(false)}
|
||||
reporterPetId={actingPetId}
|
||||
targetPostId={post.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{post.type === "VIDEO" && post.videoPost ? (
|
||||
<VideoPlayer
|
||||
|
||||
@@ -76,7 +76,10 @@
|
||||
"maxImages": "Maximal {{max}} Fotos pro Beitrag.",
|
||||
"thumbError": "Fehler",
|
||||
"captionPlaceholder": "Schreibe eine Bildunterschrift…",
|
||||
"shareButton": "Teilen"
|
||||
"shareButton": "Teilen",
|
||||
"deleteConfirmTitle": "Beitrag wirklich löschen?",
|
||||
"deleteConfirmMessage": "Dieser Beitrag wird unwiderruflich gelöscht.",
|
||||
"deleteError": "Beitrag konnte nicht gelöscht werden. Bitte versuch es erneut."
|
||||
},
|
||||
"story": {
|
||||
"changePhotoHint": "Antippen, um ein anderes Foto zu wählen",
|
||||
|
||||
@@ -76,7 +76,10 @@
|
||||
"maxImages": "Maximum {{max}} photos per post.",
|
||||
"thumbError": "Error",
|
||||
"captionPlaceholder": "Write a caption…",
|
||||
"shareButton": "Share"
|
||||
"shareButton": "Share",
|
||||
"deleteConfirmTitle": "Delete this post?",
|
||||
"deleteConfirmMessage": "This post will be permanently deleted.",
|
||||
"deleteError": "Couldn't delete this post. Please try again."
|
||||
},
|
||||
"story": {
|
||||
"changePhotoHint": "Tap to choose a different photo",
|
||||
|
||||
Reference in new issue
Block a user