diff --git a/mobile/app/(app)/(tabs)/notifications.tsx b/mobile/app/(app)/(tabs)/notifications.tsx
index 74ddefc..d0eb218 100644
--- a/mobile/app/(app)/(tabs)/notifications.tsx
+++ b/mobile/app/(app)/(tabs)/notifications.tsx
@@ -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={() => }
+ ItemSeparatorComponent={() => }
onEndReachedThreshold={0.5}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
@@ -111,7 +139,11 @@ export default function NotificationsScreen() {
renderItem={({ item }) => (
!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 ? (
@@ -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 },
diff --git a/mobile/found.md b/mobile/found.md
index f076382..2afa69d 100644
--- a/mobile/found.md
+++ b/mobile/found.md
@@ -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.
+-
\ No newline at end of file
diff --git a/mobile/src/components/PostCard.tsx b/mobile/src/components/PostCard.tsx
index 0473997..87a5b70 100644
--- a/mobile/src/components/PostCard.tsx
+++ b/mobile/src/components/PostCard.tsx
@@ -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 (
@@ -114,16 +140,18 @@ export function PostCard({ post, actingPetId, isTrending }: Props) {
{formatRelativeTime(post.createdAt)}
- setReportOpen(true)} hitSlop={8}>
+
- setReportOpen(false)}
- reporterPetId={actingPetId}
- targetPostId={post.id}
- />
+ {!isOwnPost && (
+ setReportOpen(false)}
+ reporterPetId={actingPetId}
+ targetPostId={post.id}
+ />
+ )}
{post.type === "VIDEO" && post.videoPost ? (