Skip to content

feat: Social feed with photo sharing, likes, and comments#1882

Merged
andrew-bierman merged 5 commits into
developmentfrom
copilot/feature-photo-sharing-social-interactions
Apr 11, 2026
Merged

feat: Social feed with photo sharing, likes, and comments#1882
andrew-bierman merged 5 commits into
developmentfrom
copilot/feature-photo-sharing-social-interactions

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 9, 2026

The app lacked any social sharing capabilities. This adds a full social feed feature: photo posts, likes, comments (with replies), and the ability to delete your own content.

API (packages/api/)

DB Schema — 4 new tables:

  • posts — caption + jsonb array of R2 object keys
  • post_likes, post_comments (supports parent_comment_id for replies), comment_likes

Routespackages/api/src/routes/feed/

Method Path Description
GET/POST /api/feed Paginated feed; create post
GET/DELETE /api/feed/:postId Fetch or delete own post
POST /api/feed/:postId/like Toggle like
GET/POST /api/feed/:postId/comments List / add comment
DELETE /api/feed/:postId/comments/:commentId Delete own comment
POST /api/feed/:postId/comments/:commentId/like Toggle comment like

Feed list query batches like/comment counts in parallel to avoid N+1.

Expo App (apps/expo/)

Feature modulefeatures/feed/ follows the existing pattern (hooks, components, screens, utils):

  • 8 React Query hooks covering all CRUD + toggle operations
  • PostCard — images (horizontal scroll for multi-photo), like/comment counts, owner delete
  • CommentItem — supports nested reply indentation, per-comment likes
  • FeedScreen, CreatePostScreen (multi-select gallery + camera, parallel R2 uploads via Promise.all), PostDetailScreen (inline comment input with keyboard-avoiding view)
  • Shared utils (buildPostImageUrl, formatAuthorName, formatRelativeDate) to avoid duplication across components

Navigation

  • New feed tab in bottom nav, gated by featureFlags.enableFeed = true
  • FeedTile added to the home dashboard
  • Routes: (tabs)/feed/index, feed/[id], feed/create

i18n — all feed strings added to en.json; navigation.feed key added.

Original prompt

This section details on the original issue you should resolve

<issue_title>Feature Request: Implement photo sharing with social interactions (tag people, comment, share to social media)</issue_title>
<issue_description>Currently, the app lacks social sharing capabilities. Users need the ability to share photos, tag other users, interact through comments, and share content to external social media platforms.

Proposed features:

  1. Photo sharing
  • Upload photos from the camera or the gallery
  • Add captions/descriptions
  • Multiple photo uploads per post
  • Photo editing tools (filters, crop, rotate) before posting.
  1. Tagging People
  • Tag other app users in photos
  • Notification for tagged users
  • @mention functionality in captions/comments
  • Privacy settings for tagged content
  1. Social Feed
  • Dedicated feed showing shared photos
  • Chronological or algorithmic sorting
  • Like/react buttons
  • Save/bookmark posts
  1. Comments System
  • Comment on photos
  • Reply to specific comments
  • Like comments
  • Delete/edit own comments
  • Report inappropriate comments
  1. Social Media Sharing
  • Share photos to external platforms:
  • Instagram
  • Meta (Facebook)
  • Twitter/X
  • WhatsApp
  • Telegram
  • Copy Link

User Flow:

  1. User taps "+" or "Share" button
  2. Select photos from the gallery
  3. Add caption and tag people
  4. Post to app feed
  5. Option to share to external social media
  6. Other users can view, like, and comment

Technical Considerations:

  • Cloud storage for photos
  • Real-time updates for comments
  • Push notifications
  • Privacy and data protection
  • Moderation and reporting system

Acceptance Criteria:

  • Users can successfully upload and share photos
  • Tagged users receive notifications
  • Comments appear in real-time
  • External sharing works on all supported platforms
  • Performance is optimized for image loading

What we can also add is to share with the app watermark/branding</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Add photo sharing with social interactions features feat: Social feed with photo sharing, likes, and comments Mar 9, 2026
@andrew-bierman
Copy link
Copy Markdown
Collaborator

@coderabbitai Please review this PR for code quality, best practices, and potential issues.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

Rate Limit Exceeded

@andrew-bierman have exceeded the limit for the number of chat messages per hour. Please wait 0 minutes and 43 seconds before sending another message.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 092ef170-738d-48ac-b06c-66edf0241c1d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/feature-photo-sharing-social-interactions

Comment @coderabbitai help to get the list of available commands and usage tips.

@andrew-bierman
Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@andrew-bierman andrew-bierman marked this pull request as ready for review March 10, 2026 02:26
Copilot AI review requested due to automatic review settings March 10, 2026 02:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “social feed” feature across the API and Expo app, enabling users to create photo posts and interact via likes and comments (including replies), with navigation entry points in the app UI.

Changes:

  • API: introduced feed schemas, DB tables/relations, and protected /api/feed routes for posts, likes, and comments.
  • Expo: added features/feed module (screens/components/hooks/utils) and new feed navigation routes.
  • UI/i18n: added feed strings and exposed the feed via a new tab + dashboard tile behind enableFeed.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
packages/api/src/schemas/feed.ts New OpenAPI/Zod schemas for posts/comments/feed responses.
packages/api/src/routes/index.ts Mounts new protected /feed routes.
packages/api/src/routes/feed/posts.ts Implements post CRUD + post like toggle + feed pagination.
packages/api/src/routes/feed/index.ts Aggregates feed post + comment subroutes.
packages/api/src/routes/feed/comments.ts Implements comment list/create/delete + comment like toggle.
packages/api/src/db/schema.ts Adds posts, post_likes, post_comments, comment_likes tables + relations/types.
apps/expo/lib/i18n/locales/en.json Adds navigation/feed strings for the new feature.
apps/expo/features/feed/utils/index.ts Adds feed helpers (image URL, author name, relative time).
apps/expo/features/feed/types.ts Adds TS types for post/comment payloads.
apps/expo/features/feed/screens/index.ts Re-exports feed screens.
apps/expo/features/feed/screens/PostDetailScreen.tsx Post detail UI with paginated comments + inline composer.
apps/expo/features/feed/screens/FeedScreen.tsx Feed list UI with infinite scroll + empty state + create CTA.
apps/expo/features/feed/screens/CreatePostScreen.tsx Create flow: pick/take photos, upload to R2, create post.
apps/expo/features/feed/index.ts Barrel export for feed feature module.
apps/expo/features/feed/hooks/useTogglePostLike.ts React Query mutation for post like toggle.
apps/expo/features/feed/hooks/useToggleCommentLike.ts React Query mutation for comment like toggle.
apps/expo/features/feed/hooks/usePostComments.ts Infinite query for comments pagination.
apps/expo/features/feed/hooks/useFeed.ts Infinite query for feed pagination.
apps/expo/features/feed/hooks/useDeletePost.ts Mutation for deleting a post.
apps/expo/features/feed/hooks/useDeleteComment.ts Mutation for deleting a comment.
apps/expo/features/feed/hooks/useCreatePost.ts Mutation for creating a post.
apps/expo/features/feed/hooks/useAddComment.ts Mutation for adding a comment/reply.
apps/expo/features/feed/hooks/index.ts Re-exports feed hooks.
apps/expo/features/feed/components/index.ts Re-exports feed components.
apps/expo/features/feed/components/PostCard.tsx Post card UI (images carousel, like/comment actions, owner delete).
apps/expo/features/feed/components/FeedTile.tsx Dashboard tile entrypoint into the feed.
apps/expo/features/feed/components/CommentItem.tsx Comment row UI with indentation + like/delete actions.
apps/expo/config.ts Enables featureFlags.enableFeed.
apps/expo/app/(app)/feed/create.tsx Expo Router entry for creating a post.
apps/expo/app/(app)/feed/[id].tsx Expo Router entry for post detail (fetch post by id).
apps/expo/app/(app)/(tabs)/feed/index.tsx Feed tab route.
apps/expo/app/(app)/(tabs)/feed/_layout.tsx Feed tab stack layout.
apps/expo/app/(app)/(tabs)/_layout.tsx Adds feed tab + icon mapping with feature flag gating.
apps/expo/app/(app)/(tabs)/(home)/index.tsx Adds FeedTile to dashboard layout and searchable tiles.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +226 to +244
commentsRoutes.openapi(deleteCommentRoute, async (c) => {
const auth = c.get('user');
const { commentId } = c.req.valid('param');
const db = createDb(c);

const comment = await db.query.postComments.findFirst({
where: eq(postComments.id, commentId),
});

if (!comment) {
return c.json({ error: 'Comment not found' }, 404);
}

if (comment.userId !== auth.userId) {
return c.json({ error: 'Forbidden' }, 403);
}

await db.delete(postComments).where(eq(postComments.id, commentId));

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteCommentRoute ignores the postId path param and deletes solely by commentId. This allows deleting a comment even if the URL’s postId doesn’t match the comment’s postId, which is surprising for consumers and can mask client bugs. Validate that the comment belongs to the postId in the route params (or remove postId from the path).

Copilot uses AI. Check for mistakes.
Comment on lines +248 to +285
// POST /feed/:postId/comments/:commentId/like - toggle like on comment
const toggleCommentLikeRoute = createRoute({
method: 'post',
path: '/:postId/comments/:commentId/like',
tags: ['Feed'],
summary: 'Toggle like on a comment',
security: [{ bearerAuth: [] }],
request: {
params: z.object({
postId: z.coerce.number().int(),
commentId: z.coerce.number().int(),
}),
},
responses: {
200: {
description: 'Like toggled',
content: { 'application/json': { schema: LikeToggleResponseSchema } },
},
404: {
description: 'Comment not found',
content: { 'application/json': { schema: ErrorResponseSchema } },
},
},
});

commentsRoutes.openapi(toggleCommentLikeRoute, async (c) => {
const auth = c.get('user');
const { commentId } = c.req.valid('param');
const db = createDb(c);

const comment = await db.query.postComments.findFirst({
where: eq(postComments.id, commentId),
});

if (!comment) {
return c.json({ error: 'Comment not found' }, 404);
}

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleCommentLikeRoute also ignores the postId param and doesn’t verify the comment belongs to that post. For consistency with the REST path and to prevent mismatched URLs from succeeding, check comment.postId === postId (and ideally return 404 when it doesn’t match).

Copilot uses AI. Check for mistakes.
Comment on lines +268 to +285
postsRoutes.openapi(deletePostRoute, async (c) => {
const auth = c.get('user');
const { postId } = c.req.valid('param');
const db = createDb(c);

const post = await db.query.posts.findFirst({ where: eq(posts.id, postId) });

if (!post) {
return c.json({ error: 'Post not found' }, 404);
}

if (post.userId !== auth.userId) {
return c.json({ error: 'Forbidden' }, 403);
}

await db.delete(posts).where(eq(posts.id, postId));

return c.json({ success: true }, 200);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When deleting a post, the DB rows are removed but the image objects in R2 are not. This can leave orphaned files and increase storage costs over time. Consider deleting post.images keys from the R2 bucket (similar to pack item image deletion) before/after the DB delete, while keeping failures non-blocking.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +16
return useMutation({
mutationFn: async (postId: number) => {
const response = await axiosInstance.post<LikeToggleResponse>(`/api/feed/${postId}/like`);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['feed'] });
},
});
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useTogglePostLike only invalidates the feed list query (['feed']). The post detail route uses a separate query key (['feed', postId]), so liking a post from PostDetailScreen won’t refresh the post’s likeCount/likedByMe and the UI can stay stale. Also invalidate the per-post query key (or update the cache optimistically).

Copilot uses AI. Check for mistakes.
Comment thread packages/api/src/db/schema.ts Outdated
Comment on lines +525 to +535
// Post likes table
export const postLikes = pgTable('post_likes', {
id: serial('id').primaryKey(),
postId: integer('post_id')
.references(() => posts.id, { onDelete: 'cascade' })
.notNull(),
userId: integer('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

post_likes is missing a uniqueness constraint on (postId, userId). With the current read-then-insert toggle, concurrent requests can insert duplicate likes, inflating likeCount and breaking toggling. Add a composite unique constraint/index and handle insert via conflict-safe semantics (and delete by the same pair).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
import { ActivityIndicator, Button, Text } from '@packrat/ui/nativewindui';
import { Icon } from '@roninoss/icons';
import { userStore } from 'expo-app/features/auth/store';
import { uploadImage } from 'expo-app/features/packs/utils/uploadImage';
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userStore is imported but never used in this screen. Please remove the unused import to keep the file lint-clean.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +35
if (!post) {
return (
<View className="flex-1 items-center justify-center bg-background">
<Text>Post not found</Text>
</View>
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The post detail route renders a hardcoded "Post not found" string instead of using i18n like the rest of the feed UI. Consider adding a feed.postNotFound (or similar) key and using t(...) here for localization consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +84
{post.images.length > 1 && (
<View className="absolute bottom-2 right-3 bg-black/50 rounded-full px-2 py-0.5">
<Text className="text-white text-xs">{post.images.length} photos</Text>
</View>
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "{post.images.length} photos" overlay is hardcoded English. Since feed strings are otherwise localized, consider moving this to i18n (including pluralization) so it translates correctly.

Copilot uses AI. Check for mistakes.
Comment thread packages/api/src/db/schema.ts Outdated
Comment on lines +552 to +562
// Comment likes table
export const commentLikes = pgTable('comment_likes', {
id: serial('id').primaryKey(),
commentId: integer('comment_id')
.references(() => postComments.id, { onDelete: 'cascade' })
.notNull(),
userId: integer('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment_likes is missing a uniqueness constraint on (commentId, userId). As written, concurrent toggles can create duplicate rows and cause incorrect likeCount results. Add a composite unique constraint/index and make the toggle insert conflict-safe.

Copilot uses AI. Check for mistakes.
Comment on lines +538 to +550
export const postComments = pgTable('post_comments', {
id: serial('id').primaryKey(),
postId: integer('post_id')
.references(() => posts.id, { onDelete: 'cascade' })
.notNull(),
userId: integer('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
content: text('content').notNull(),
parentCommentId: integer('parent_comment_id'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

post_comments.parentCommentId is just a plain integer; there’s no foreign key back to post_comments.id, so replies can reference non-existent comments and won’t cascade on delete. Consider making it a self-referential FK (and ideally add an index) to enforce reply integrity.

Copilot uses AI. Check for mistakes.
@andrew-bierman andrew-bierman changed the base branch from main to development March 12, 2026 05:23
@cloudflare-workers-and-pages
Copy link
Copy Markdown
Contributor

cloudflare-workers-and-pages Bot commented Mar 18, 2026

Deploying packrat-landing with  Cloudflare Pages  Cloudflare Pages

Latest commit: e4b1f63
Status:🚫  Build failed.

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown
Contributor

Deploying packrat-guides with  Cloudflare Pages  Cloudflare Pages

Latest commit: e4b1f63
Status:🚫  Build failed.

View logs

Copilot AI and others added 5 commits April 10, 2026 23:34
Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
…s, fix type inconsistencies

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
…eanup

- Validate postId param in deleteCommentRoute and toggleCommentLikeRoute
  to prevent cross-post comment deletion/liking (security fix)
- Add composite unique constraints on (postId, userId) for post_likes
  and (commentId, userId) for comment_likes to prevent duplicate likes
- Replace duplicated formatRelativeDate with shared getRelativeTime utility
- Remove unused userStore import from CreatePostScreen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Set enableFeed to false in apps/expo/config.ts (feature not yet QA'd)
- Add migration 0033_social_feed_tables.sql for posts, postLikes, postComments, commentLikes tables
- Add self-referential FK on postComments.parentCommentId with ON DELETE cascade
- Fix comment count display in PostDetailScreen to use post.commentCount instead of comments.length (which only reflects the current page)
@andrew-bierman andrew-bierman force-pushed the copilot/feature-photo-sharing-social-interactions branch from d31f885 to e5c3950 Compare April 11, 2026 05:35
@github-actions
Copy link
Copy Markdown
Contributor

Coverage Report for Expo Unit Tests Coverage (./apps/expo)

Status Category Percentage Covered / Total
🔵 Lines 74.32% 495 / 666
🔵 Statements 74.32% (🎯 75%) 495 / 666
🔵 Functions 91.07% 51 / 56
🔵 Branches 91.38% 191 / 209
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/expo/features/feed/utils/index.ts 0% 0% 0% 0% 1-21
Generated in workflow #58 for commit e5c3950 by the Vitest Coverage Report Action

@github-actions
Copy link
Copy Markdown
Contributor

Coverage Report for API Unit Tests Coverage (./packages/api)

Status Category Percentage Covered / Total
🔵 Lines 96.57% 902 / 934
🔵 Statements 96.57% (🎯 80%) 902 / 934
🔵 Functions 100% 48 / 48
🔵 Branches 90.03% 280 / 311
File CoverageNo changed files found.
Generated in workflow #58 for commit e5c3950 by the Vitest Coverage Report Action

@andrew-bierman andrew-bierman merged commit dc4e8c9 into development Apr 11, 2026
5 of 10 checks passed
@andrew-bierman andrew-bierman deleted the copilot/feature-photo-sharing-social-interactions branch April 11, 2026 05:49
andrew-bierman added a commit that referenced this pull request Apr 11, 2026
The Expo Unit Tests CI job on development was failing because the v8
statement-coverage threshold (75%) dropped to 74.32% after recent feature
merges. PR #1882 (social feed) and the pack-template weight helpers
landed with no unit tests, and vitest.config.ts includes every .ts under
`features/**/utils/**` in the coverage report.

This adds pure-logic unit tests for two easy-to-cover modules:

- features/feed/utils/index.ts: buildPostImageUrl, formatAuthorName,
  and formatRelativeDate. clientEnvs and getRelativeTime are mocked.
- features/pack-templates/utils/computePacktemplateWeight.ts: covers
  empty templates, base gear, consumables, worn items, quantity, and
  unit conversion. The expo-app/features/packs/utils barrel is mocked to
  the two pure conversion helpers so the test does not pull in
  uploadImage.ts (which imports expo-file-system -> expo-sqlite).

Coverage goes from 74.32% -> 79.87% statements; 330/330 tests pass.
No production code changes.
andrew-bierman added a commit that referenced this pull request Apr 11, 2026
…nents

Replace invalid icon names (like 'delete') that weren't in the nwui Icon
type allowlist. These were causing check-types failures on development
after #1882, #1885, #1906 merged.
andrew-bierman added a commit that referenced this pull request May 14, 2026
…g-social-interactions

feat: Social feed with photo sharing, likes, and comments
andrew-bierman added a commit that referenced this pull request May 14, 2026
…nents

Replace invalid icon names (like 'delete') that weren't in the nwui Icon
type allowlist. These were causing check-types failures on development
after #1882, #1885, #1906 merged.
andrew-bierman added a commit that referenced this pull request May 14, 2026
The Expo Unit Tests CI job on development was failing because the v8
statement-coverage threshold (75%) dropped to 74.32% after recent feature
merges. PR #1882 (social feed) and the pack-template weight helpers
landed with no unit tests, and vitest.config.ts includes every .ts under
`features/**/utils/**` in the coverage report.

This adds pure-logic unit tests for two easy-to-cover modules:

- features/feed/utils/index.ts: buildPostImageUrl, formatAuthorName,
  and formatRelativeDate. clientEnvs and getRelativeTime are mocked.
- features/pack-templates/utils/computePacktemplateWeight.ts: covers
  empty templates, base gear, consumables, worn items, quantity, and
  unit conversion. The expo-app/features/packs/utils barrel is mocked to
  the two pure conversion helpers so the test does not pull in
  uploadImage.ts (which imports expo-file-system -> expo-sqlite).

Coverage goes from 74.32% -> 79.87% statements; 330/330 tests pass.
No production code changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Implement photo sharing with social interactions (tag people, comment, share to social media)

4 participants