diff --git a/mobile/README.md b/mobile/README.md index 9c6362f..1d80929 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -14,12 +14,13 @@ architecture-decision thread in this session's handoff. **Status:** Phase 1 — real Feed/Explore/Notifications/Pets tabs against the live backend, not just a connectivity scaffold anymore. Post creation, comments, repost, pet profile detail (view/follow/edit/followers-following), -stories (tray/creation/viewer), video playback, and search are done and -verified end-to-end against the live backend. Health tracking, milestones, -and message sending are still not built — see "What's not built yet" below. -Android is the priority platform; iOS builds need a Mac + Apple Developer -account and come later (same codebase, no separate source tree — see -"Android-first, then iOS" below). +stories (tray/creation/viewer), video playback, search, health tracking +(consent gate + weight/vet/vaccine logs), milestones, and message +sending/receiving are done and verified end-to-end against the live backend. +Push notifications are wired end-to-end but not yet deliverable — see "Push +notifications" below. Android is the priority platform; iOS builds need a +Mac + Apple Developer account and come later (same codebase, no separate +source tree — see "Android-first, then iOS" below). ## Architecture @@ -163,10 +164,14 @@ mobile/ notifications.tsx # notifications list pets.tsx # owner's pets + active-pet switcher messages/index.tsx # conversations list + messages/[conversationId].tsx # chat detail/compose, polls messages.list while focused (no realtime) search.tsx # pets/people/hashtags search, three tabs over one debounced query post-new.tsx # create a photo post (pick/upload → posts.create) story-new.tsx # create a photo story (pick/upload → stories.create) story-viewer.tsx # full-screen story playback (tap zones, progress bars, view tracking) + milestone-new.tsx # create a milestone post (type picker + optional photo → milestones.create) + pets/[petId]/health/index.tsx # weight/vet/vaccine logs, gated by HealthConsentGate + pets/[petId]/milestones.tsx # a pet's milestone posts (milestones.listByPet) posts/[postId]/comments.tsx # read/write/delete comments for a post image-viewer.tsx # full-screen tapped-image lightbox src/ @@ -174,6 +179,7 @@ mobile/ api/types.ts # inferRouterOutputs shortcuts (Pet, FeedPost, ...) context/ActivePetContext.tsx # MMKV-backed active-pet state hooks/useDebounce.ts # generic debounce hook (search input) + hooks/usePushNotifications.ts # permission + Expo push token registration (owner.registerPushToken) lib/env.ts # typed EXPO_PUBLIC_* access lib/tokenCache.ts # Clerk session persistence via expo-secure-store lib/storage.ts # MMKV instance + TanStack Query sync persister @@ -181,9 +187,11 @@ mobile/ lib/format.ts # pet display name + relative-time formatting lib/sponsor.ts # active-sponsor check lib/upload.ts # direct PUT of a picked image to a presigned Supabase URL + lib/milestone-meta.ts # MilestoneType → German label/icon map theme/tokens.ts # design tokens mirroring the web app's DESIGN.md components/ # Button, TextField, Card, PostCard, PawButton, PetAvatar, # PetIdentityBadges, FollowButton, StoryTray, PawRing, VideoPlayer + components/health/ # HealthConsentGate, WeightLogSection, VetVisitSection, VaccineSection ``` ## Android-first, then iOS @@ -210,11 +218,28 @@ switching, persisted cache for fast reopen) plus a full pet profile repost ("Paw-Back") creation, stories (tray/creation/viewer via `expo-video`-adjacent `expo-image`, plus `PawRing` seen/unseen indicator), video playback (`expo-video` against Mux HLS, polling while `PROCESSING`), -and search (pets/people/hashtags, `search.pets`/`byOwner`/`byHashtag`) are -done and verified end-to-end against the live backend on a real Android +search (pets/people/hashtags, `search.pets`/`byOwner`/`byHashtag`), health +tracking (consent gate + weight/vet visit/vaccine logs — feeding info, +emergency vet, and the shareable Health Card are web-only for now), +milestones (view + create, restricted to the 5 user-initiated types), and +messaging (conversation list + chat detail/compose, polling while the +screen is focused since messages.ts has no realtime mechanism) are done +and verified end-to-end against the live backend on a real Android emulator. Still missing, roughly in the order they'd unblock the most: -- Health tracking, milestones -- Sending messages (conversations list works; no chat detail/compose screen) -- Push notifications - No i18n — the whole app is hardcoded German text, matching the sign-in screen + +### Push notifications — wired but not yet deliverable + +`usePushNotifications` (in `app/_layout.tsx`) requests notification +permission and registers the resulting Expo push token with the backend +(`owner.registerPushToken`); `src/lib/notify.ts` on the web side sends a +best-effort push through Expo's push API alongside every in-app +Notification it creates (follow, reaction, comment, message, ...). Minting +a real token needs an **EAS project id** (`app.json`'s +`extra.eas.projectId`, set once via `eas init` — free, no EAS Build +required, but does need an Expo account login). Until that's done, the +permission prompt shows and is granted, but `getExpoPushTokenAsync` has +nothing to attach to and the hook no-ops with a console warning +(`[push] No EAS project id configured...`) — confirmed via `adb logcat` on +2026-09-06. No Owner row has an `expoPushToken` set until this is run. diff --git a/mobile/app.json b/mobile/app.json index 96cfa86..d25f4f6 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -39,7 +39,14 @@ "cameraPermission": "Erlaube PawFeed den Zugriff auf die Kamera, um ein Foto für deinen Beitrag aufzunehmen." } ], - "expo-video" + "expo-video", + [ + "expo-notifications", + { + "icon": "./assets/android-icon-monochrome.png", + "color": "#f97316" + } + ] ] } } diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index 62d892a..0e8623f 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -27,7 +27,10 @@ export default function AppLayout() { + + + , }} /> + , + }} + /> (); + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const { activePetId } = useActivePet(); + const listRef = useRef>(null); + + const [body, setBody] = useState(""); + + const { data, isLoading } = useQuery({ + ...trpc.messages.list.queryOptions({ conversationId, petId: activePetId ?? "" }), + enabled: !!activePetId, + refetchInterval: POLL_INTERVAL_MS, + }); + const messages = data?.messages ?? []; + + const invalidate = () => + queryClient.invalidateQueries({ + queryKey: trpc.messages.list.queryOptions({ conversationId, petId: activePetId ?? "" }).queryKey, + }); + + const sendMutation = useMutation( + trpc.messages.send.mutationOptions({ + onSuccess: async () => { + setBody(""); + await invalidate(); + await queryClient.invalidateQueries(trpc.conversations.pathFilter()); + setTimeout(() => listRef.current?.scrollToEnd({ animated: true }), 100); + }, + }) + ); + + // Mark received messages read as soon as the list loads (messages.list already + // does this server-side on read, but the inbox badge also needs a fresh count). + useEffect(() => { + if (activePetId) queryClient.invalidateQueries(trpc.conversations.pathFilter()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [messages.length]); + + const handleSubmit = useCallback(() => { + if (!body.trim() || !activePetId) return; + sendMutation.mutate({ conversationId, senderPetId: activePetId, body: body.trim() }); + }, [body, activePetId, conversationId, sendMutation]); + + return ( + + {!isLoading && ( + item.id} + contentContainerStyle={styles.list} + onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: false })} + renderItem={({ item }) => } + ListEmptyComponent={ + + Noch keine Nachrichten. Schreib die erste! + + } + /> + )} + + + +