diff --git a/mobile/README.md b/mobile/README.md index a8d7094..9c6362f 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -13,8 +13,10 @@ 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, stories, pet profile detail, video playback, search, health, and -message sending are still not built — see "What's not built yet" below. +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). @@ -153,21 +155,25 @@ mobile/ (auth)/ sign-in.tsx # email/password + Clerk Device Trust email-code step (app)/ - _layout.tsx # Stack hosting the (tabs) group + the messages screen + _layout.tsx # Stack hosting the (tabs) group + the messages/search/story/comment screens (tabs)/ - _layout.tsx # bottom Tabs: Feed/Explore/Notifications/Pets + header Messages button - index.tsx # Feed + _layout.tsx # bottom Tabs: Feed/Explore/Notifications/Pets + header Search/Messages buttons + index.tsx # Feed (StoryTray header + infinite post list) explore.tsx # pet discovery grid notifications.tsx # notifications list pets.tsx # owner's pets + active-pet switcher messages/index.tsx # conversations list + 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) posts/[postId]/comments.tsx # read/write/delete comments for a post image-viewer.tsx # full-screen tapped-image lightbox src/ api/trpc.ts # tRPC client, AppRouter type import, query client factory api/types.ts # inferRouterOutputs shortcuts (Pet, FeedPost, ...) context/ActivePetContext.tsx # MMKV-backed active-pet state + hooks/useDebounce.ts # generic debounce hook (search input) 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 @@ -176,7 +182,8 @@ mobile/ lib/sponsor.ts # active-sponsor check lib/upload.ts # direct PUT of a picked image to a presigned Supabase URL theme/tokens.ts # design tokens mirroring the web app's DESIGN.md - components/ # Button, TextField, Card, PostCard, PawButton, PetAvatar, PetIdentityBadges + components/ # Button, TextField, Card, PostCard, PawButton, PetAvatar, + # PetIdentityBadges, FollowButton, StoryTray, PawRing, VideoPlayer ``` ## Android-first, then iOS @@ -199,14 +206,14 @@ separately. "Starting with Android" means: Phase 1 (Feed/Explore/Notifications/Pets tabs, reactions, active-pet switching, persisted cache for fast reopen) plus a full pet profile -(view/follow/edit/followers-following), post creation/upload, comments, and -repost ("Paw-Back") creation 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: +(view/follow/edit/followers-following), post creation/upload, comments, +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 +emulator. Still missing, roughly in the order they'd unblock the most: -- Stories (story ring, viewer, creation) -- Video playback (currently a static placeholder for `type === "VIDEO"` posts) -- Search (`search.pets`/`byHashtag`/`byOwner`) — separate from the Explore tab - Health tracking, milestones - Sending messages (conversations list works; no chat detail/compose screen) - Push notifications diff --git a/mobile/app.json b/mobile/app.json index 8edc4cf..96cfa86 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -38,7 +38,8 @@ "photosPermission": "Erlaube PawFeed den Zugriff auf deine Fotos, um sie in einem Beitrag zu teilen.", "cameraPermission": "Erlaube PawFeed den Zugriff auf die Kamera, um ein Foto für deinen Beitrag aufzunehmen." } - ] + ], + "expo-video" ] } } diff --git a/mobile/app/(app)/(tabs)/_layout.tsx b/mobile/app/(app)/(tabs)/_layout.tsx index a6410a6..fa7461d 100644 --- a/mobile/app/(app)/(tabs)/_layout.tsx +++ b/mobile/app/(app)/(tabs)/_layout.tsx @@ -1,7 +1,7 @@ import { View, Text, Pressable, StyleSheet } from "react-native"; import { Tabs, useRouter } from "expo-router"; import { useQuery } from "@tanstack/react-query"; -import { Home, Compass, Bell, Heart, MessageCircle, PawPrint, Plus } from "lucide-react-native"; +import { Home, Compass, Bell, Heart, MessageCircle, PawPrint, Plus, Search } from "lucide-react-native"; import { useTRPC } from "../../../src/api/trpc"; import { useActivePet } from "../../../src/context/ActivePetContext"; import { colors, fontFamily } from "../../../src/theme/tokens"; @@ -10,8 +10,10 @@ import { colors, fontFamily } from "../../../src/theme/tokens"; * Bottom tab bar mirroring the product's real mobile IA (web's * src/components/layout/MobileNav.tsx): Feed, Explore, Notifications, Pets. * Messages is a header button, not a tab, matching web (there it's a fixed - * floating corner button). The center "+" post button from web has no - * equivalent here yet — post creation is a later phase. + * floating corner button). "+" (new post, Feed tab) and the search icon + * (Explore tab) have no web mobile-nav equivalent at all — web only exposes + * search via the desktop Sidebar — so these are this app's own entry points + * for actions that need one. */ export default function TabsLayout() { return ( @@ -43,7 +45,16 @@ export default function TabsLayout() { /> }} + options={{ + title: "Explore", + headerRight: () => ( + + + + + ), + tabBarIcon: ({ color, size }) => , + }} /> router.push("/search")} hitSlop={12}> + + + ); +} + function MessagesHeaderButton() { const router = useRouter(); return ( diff --git a/mobile/app/(app)/(tabs)/index.tsx b/mobile/app/(app)/(tabs)/index.tsx index 127d308..46db47e 100644 --- a/mobile/app/(app)/(tabs)/index.tsx +++ b/mobile/app/(app)/(tabs)/index.tsx @@ -3,12 +3,14 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import { useTRPC } from "../../../src/api/trpc"; import { useActivePet } from "../../../src/context/ActivePetContext"; import { PostCard } from "../../../src/components/PostCard"; +import { StoryTray } from "../../../src/components/StoryTray"; import { colors, typography } from "../../../src/theme/tokens"; import type { FeedPost } from "../../../src/api/types"; /** - * Port of web's src/components/feed/FeedList.tsx, scoped to Phase 1: real - * infinite-scroll feed data, no ads/story tray/feed-mode toggle yet. + * Port of web's src/components/feed/FeedList.tsx: infinite-scroll feed data + * with the StoryTray as a list header. No ads or feed-mode ("algo" vs + * "chrono") toggle yet. */ export default function FeedScreen() { const trpc = useTRPC(); @@ -47,6 +49,7 @@ export default function FeedScreen() { keyExtractor={(post) => post.id} contentContainerStyle={styles.list} renderItem={({ item }) => } + ListHeaderComponent={} ItemSeparatorComponent={() => ( diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx index aca14e7..62d892a 100644 --- a/mobile/app/(app)/_layout.tsx +++ b/mobile/app/(app)/_layout.tsx @@ -28,6 +28,7 @@ export default function AppLayout() { + , }} /> + , + }} + /> + ); } diff --git a/mobile/app/(app)/pets/[petId]/index.tsx b/mobile/app/(app)/pets/[petId]/index.tsx index 6d5ded2..f7e9316 100644 --- a/mobile/app/(app)/pets/[petId]/index.tsx +++ b/mobile/app/(app)/pets/[petId]/index.tsx @@ -1,13 +1,13 @@ -import { useState } from "react"; import { View, Text, FlatList, Pressable, ActivityIndicator, StyleSheet } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { useTRPC } from "../../../../src/api/trpc"; import { useActivePet } from "../../../../src/context/ActivePetContext"; import { PetAvatar } from "../../../../src/components/PetAvatar"; import { PetIdentityBadges } from "../../../../src/components/PetIdentityBadges"; import { PostCard } from "../../../../src/components/PostCard"; import { Button } from "../../../../src/components/Button"; +import { FollowButton } from "../../../../src/components/FollowButton"; import { formatPetName } from "../../../../src/lib/format"; import { colors, typography } from "../../../../src/theme/tokens"; @@ -23,7 +23,6 @@ export default function PetProfileScreen() { const { petId } = useLocalSearchParams<{ petId: string }>(); const router = useRouter(); const trpc = useTRPC(); - const queryClient = useQueryClient(); const { activePetId } = useActivePet(); const { data: pet, isLoading, error } = useQuery(trpc.pets.getProfile.queryOptions({ petId })); @@ -33,27 +32,6 @@ export default function PetProfileScreen() { const isOwnPet = activePetId === petId; - const { data: isFollowing } = useQuery({ - ...trpc.follows.isFollowing.queryOptions({ followerPetId: activePetId ?? "", followeePetId: petId }), - enabled: !isOwnPet && !!activePetId, - }); - - const [followPending, setFollowPending] = useState(false); - const followMutation = useMutation( - trpc.follows.create.mutationOptions({ - onMutate: () => setFollowPending(true), - onSuccess: () => queryClient.invalidateQueries(trpc.follows.pathFilter()), - onSettled: () => setFollowPending(false), - }) - ); - const unfollowMutation = useMutation( - trpc.follows.delete.mutationOptions({ - onMutate: () => setFollowPending(true), - onSuccess: () => queryClient.invalidateQueries(trpc.follows.pathFilter()), - onSettled: () => setFollowPending(false), - }) - ); - if (isLoading) { return ( @@ -106,17 +84,7 @@ export default function PetProfileScreen() { {isOwnPet ? (