Skip to content

Comments

Fixed Streaks, Unautenticated Users and Blocked Friends#10

Merged
fortune710 merged 9 commits intomainfrom
dev
Jan 3, 2026
Merged

Fixed Streaks, Unautenticated Users and Blocked Friends#10
fortune710 merged 9 commits intomainfrom
dev

Conversation

@fortune710
Copy link
Owner

@fortune710 fortune710 commented Dec 24, 2025

Summary by CodeRabbit

  • New Features

    • Music preview player integrated into the vault for in-place audio previews
    • Legal Documents hub added with list and detail views (Terms, EULA, Privacy)
  • Bug Fixes

    • Improved avatar fallback and initials when images are unavailable
    • Onboarding, capture redirects and suggested-friends prefetch now respect authentication state to avoid incorrect redirects or prefetches
    • Audio preview popover exit animation and interaction behavior improved
  • Chores

    • Removed "Storage & Data" from settings and consolidated legal access into a single menu item

✏️ Tip: You can customize this high-level summary in your review settings.

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link

vercel bot commented Dec 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
keepsafe Ready Ready Preview, Comment Jan 3, 2026 0:32am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 24, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Prefetch/onboarding guards tightened; music preview added with onMusicPress propagation from vault to popover; FriendService and blocked_by support added with DB migration and types; new Legal screen and settings tweaks; streak timezone removed; deep-linking logging/fallbacks adjusted.

Changes

Cohort / File(s) Summary
Music playback integration
frontend/app/vault.tsx, frontend/components/entries/vault-entry-card.tsx, frontend/components/capture/canvas/vault-canvas.tsx, frontend/components/capture/canvas/music-canvas-item.tsx, frontend/components/capture/music/audio-preview-popover.tsx
Threaded an onMusicPress callback from vault down to music items; Vault manages selectedMusic and renders AudioPreviewPopover; MusicCanvasItem uses TouchableOpacity; popover visibility/exit-animation rendering adjusted; minor UI style tweaks.
Friend service, types & UI
frontend/services/friend-service.ts, frontend/hooks/use-friends.ts, frontend/types/friends.ts, frontend/app/settings/blocked-users.tsx
Added FriendService methods (getFriends, sendFriendRequest, removeFriend, getSuggestedFriendsFromContacts), introduced FriendWithProfile and aliases, integrated blocked_by handling in hooks and blocked-users UI (fallback avatars, blocked_by propagation).
Database migration & generated types
frontend/supabase/migrations/20251220000000_add_blocked_by.sql, frontend/types/database.ts
Added blocked_by UUID column to friendships, index, updated RLS policy; extended generated Database types to include blocked_by in Row/Insert/Update.
Legal documents screen
frontend/app/settings/legal.tsx
New LegalScreen component with list and detail views for Terms, EULA, Privacy, selection state, and styled content rendering.
Settings adjustments
frontend/app/settings/index.tsx, frontend/app/settings/about.tsx
Removed Storage & Data settings item; moved SafeAreaView import to safe-area-context; replaced direct external-link handling with navigation to /settings/legal; updated header padding scaling and icons.
Streak tracking simplification
frontend/services/streak-service.ts, frontend/hooks/use-streak-tracking.ts
Removed timezone/userTimeZone from streak state and logic; simplified streak increment/reset semantics to day-gap based checks and added logging.
App startup & deep-linking
frontend/app/index.tsx, frontend/hooks/use-deep-linking.ts
PrefetchSuggestedFriends and onboarding/capture redirects now require both user and session presence/absence; deep-linking removed catch-all navigation fallback and added console logging.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Item as MusicCanvasItem
    participant Canvas as VaultCanvas
    participant Card as VaultEntryCard
    participant Vault as Vault Screen
    participant Popover as AudioPreviewPopover

    User->>Item: taps music item
    Item->>Canvas: onPress -> propagate music tag
    Canvas->>Card: invoke onMusicPress with MusicTag
    Card->>Vault: call onMusicPress handler
    Vault->>Vault: set selectedMusic, isMusicPlayerVisible = true
    Vault->>Popover: render isVisible=true with selectedMusic
    Popover->>User: display preview
    User->>Popover: tap backdrop / close
    Popover->>Vault: onClose
    Vault->>Vault: run exit animation, clear selectedMusic after delay
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped into the vault to hear a tune,
Friends found and blocked beneath the moon,
Legal pages lined up neat and bright,
Streaks simplified by morning light,
A joyful hop — code changes take flight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly maps to the three main categories of changes: streak logic fixes (use-streak-tracking.ts, streak-service.ts), unauthenticated user handling (app/index.tsx), and blocked friends feature (migration, types, friend-service.ts, blocked-users.tsx).

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28e30e8 and dd16bcb.

📒 Files selected for processing (4)
  • frontend/app/index.tsx
  • frontend/app/settings/legal.tsx
  • frontend/app/vault.tsx
  • frontend/hooks/use-friends.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/app/index.tsx (1)

10-11: Remove debug console logs before production.

These debug logs should be removed or replaced with a proper logging solution for production code.

🔎 Proposed fix
-  console.log("User: %s", user);
-  console.log("Session: %s", session);
-
🧹 Nitpick comments (7)
frontend/services/streak-service.ts (2)

52-60: Inconsistent logging: use logger instead of console.log.

Line 56 uses console.log, but line 169 uses logger.info. Consider using logger.debug here for consistency and better log management in production.

🔎 Suggested fix
   static async saveStreakData(userId: string, data: StreakData): Promise<void> {
     try {
       await deviceStorage.setItem(`streak_${userId}`, data);
-      console.log('Saved streak data:', data);
+      logger.debug('Saved streak data:', data);
     } catch (error) {
       console.error('Failed to save streak data:', error);
     }
   }

78-122: Misleading variable name and excessive debug logging.

  1. todayStr (line 80) is derived from entryDate, not necessarily "today". Consider renaming to entryDateStr for clarity.

  2. Multiple console.log calls should use logger.debug for consistency and to allow proper log level control in production.

🔎 Suggested improvements
   static async updateStreak(userId: string, entryDate: Date, currentStreakData: StreakData): Promise<StreakData> {
     const now = new Date();
-    const todayStr = format(startOfDay(entryDate), 'yyyy-MM-dd');
+    const entryDateStr = format(startOfDay(entryDate), 'yyyy-MM-dd');
     
-    console.log('Updating streak - Today:', todayStr, 'Last entry:', currentStreakData.lastEntryDate);
+    logger.debug('Updating streak - Entry date:', entryDateStr, 'Last entry:', currentStreakData.lastEntryDate);
     
     let newStreakData = { ...currentStreakData };

     // First entry ever - start the streak!
     if (!newStreakData.lastEntryDate) {
       newStreakData.currentStreak = 1;
       newStreakData.maxStreak = 1;
-      newStreakData.lastEntryDate = todayStr;
+      newStreakData.lastEntryDate = entryDateStr;
       newStreakData.lastAccessTime = now.toISOString();
-      console.log('First entry ever - streak set to 1');
+      logger.debug('First entry ever - streak set to 1');
     } else {
       // ... rest of the method - similar console.log → logger.debug changes
     }
frontend/app/settings/blocked-users.tsx (1)

85-88: Consider adding error handling for avatar image loading.

The Image component lacks an onError prop. If the avatar URL fails to load (e.g., network issues, API downtime), users will see a broken image. Consider adding graceful error handling.

🔎 Suggested enhancement
+              const [imageError, setImageError] = React.useState(false);
+              
               return (
                 <View key={friend.id} style={styles.userItem}>
                   <View style={styles.avatarContainer}>
-                    {avatarUrl ? (
+                    {avatarUrl && !imageError ? (
                       <Image
                         source={{ uri: avatarUrl }}
                         style={styles.avatar}
+                        onError={() => setImageError(true)}
                       />
                     ) : (
                       <View style={[styles.avatar, styles.avatarPlaceholder]}>
                         <Text style={styles.avatarText}>
                           {(friend.friend_profile?.full_name?.[0] || friend.friend_profile?.username?.[0] || '?').toUpperCase()}
                         </Text>
                       </View>
                     )}

Note: This suggestion assumes the unreachable code issue (previous comment) is addressed first.

frontend/app/settings/legal.tsx (2)

34-198: Consider externalizing legal document content.

The legal content is currently hardcoded within the component with inline text spanning 160+ lines. This approach has several drawbacks:

  1. Maintenance burden: Updates require code changes and redeployment
  2. Stale dates: The "Last Updated: January 15, 2025" is hardcoded and won't update automatically
  3. No versioning: Cannot track content changes or show version history
  4. Testability: Difficult to test different content scenarios
💡 Recommended approaches

Option 1: Separate content files
Move content to JSON or TypeScript constant files:

// constants/legal-content.ts
export const legalContent = {
  terms: {
    lastUpdated: '2025-01-15',
    sections: [
      { title: '1. Acceptance of Terms', text: '...' },
      // ...
    ]
  },
  // ...
};

Option 2: Remote content management
Fetch content from a backend API or CMS, enabling:

  • Real-time updates without deployment
  • Version tracking and audit trails
  • A/B testing different legal language
  • Automatic "last updated" timestamps

This would also allow non-technical team members to update legal documents.


267-267: Inconsistent scaling applied to title.

The title uses scale(16) but this results in a font size that may be smaller than intended. The other screens use fixed font sizes (e.g., line 92 in about.tsx uses fontSize: 20). Consider whether responsive scaling is necessary here or if a fixed size would be more consistent.

🔎 Suggested fix for consistency
  title: {
-    fontSize: scale(16),
+    fontSize: 20,
    fontWeight: '600',
    color: '#1E293B',
  },
frontend/components/capture/canvas/music-canvas-item.tsx (1)

12-15: Consider using Pressable instead of TouchableOpacity.

While TouchableOpacity works well, the React Native community now recommends Pressable for new code as it offers better performance, more flexibility (custom feedback, hitSlop), and is the modern standard.

🔎 Proposed refactor to Pressable
-import { StyleSheet, Text, TouchableOpacity } from "react-native";
+import { StyleSheet, Text, Pressable } from "react-native";
-    <TouchableOpacity style={styles.musicContainer} onPress={onPress}>
+    <Pressable style={styles.musicContainer} onPress={onPress}>
         <Image source={{ uri: music.cover }} style={styles.musicImage} />
         <Text style={styles.textStyle}>{music.title} - {music.artist}</Text>
-    </TouchableOpacity>
+    </Pressable>
frontend/hooks/use-friends.ts (1)

70-80: Avoid any type and as never cast for type safety.

Using any and casting to never bypasses TypeScript's type checking. Consider using the proper Database types for the update payload.

🔎 Proposed fix using proper types
-      const updateData: any = { status };
-      if (blocked_by !== undefined) {
-        updateData.blocked_by = blocked_by;
-      }
+      const updateData: Database['public']['Tables']['friendships']['Update'] = { 
+        status,
+        ...(blocked_by !== undefined && { blocked_by })
+      };

       const { error } = await supabase
         .from(TABLES.FRIENDSHIPS)
-        .update(updateData as never)
+        .update(updateData)
         .eq('id', id);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6859a9f and 3267e2c.

📒 Files selected for processing (18)
  • frontend/app/index.tsx
  • frontend/app/settings/about.tsx
  • frontend/app/settings/blocked-users.tsx
  • frontend/app/settings/index.tsx
  • frontend/app/settings/legal.tsx
  • frontend/app/vault.tsx
  • frontend/components/capture/canvas/music-canvas-item.tsx
  • frontend/components/capture/canvas/vault-canvas.tsx
  • frontend/components/capture/music/audio-preview-popover.tsx
  • frontend/components/entries/vault-entry-card.tsx
  • frontend/hooks/use-deep-linking.ts
  • frontend/hooks/use-friends.ts
  • frontend/hooks/use-streak-tracking.ts
  • frontend/services/friend-service.ts
  • frontend/services/streak-service.ts
  • frontend/supabase/migrations/20251220000000_add_blocked_by.sql
  • frontend/types/database.ts
  • frontend/types/friends.ts
💤 Files with no reviewable changes (1)
  • frontend/hooks/use-streak-tracking.ts
🧰 Additional context used
🧬 Code graph analysis (5)
frontend/app/settings/blocked-users.tsx (1)
frontend/lib/utils.ts (1)
  • getDefaultAvatarUrl (6-8)
frontend/hooks/use-friends.ts (3)
frontend/constants/supabase.ts (2)
  • FRIENDSHIP_STATUS (31-36)
  • TABLES (4-15)
frontend/lib/supabase.ts (1)
  • supabase (14-30)
frontend/lib/logger.ts (1)
  • error (32-35)
frontend/app/vault.tsx (1)
frontend/components/capture/music/audio-preview-popover.tsx (1)
  • AudioPreviewPopover (25-77)
frontend/types/friends.ts (1)
frontend/types/database.ts (1)
  • Database (11-334)
frontend/components/capture/canvas/vault-canvas.tsx (1)
frontend/components/capture/canvas/music-canvas-item.tsx (1)
  • MusicCanvasItem (10-17)
🔇 Additional comments (22)
frontend/services/streak-service.ts (4)

1-29: LGTM! Well-documented service with clear business logic.

The documentation clearly explains the streak logic, and the simplified StreakData interface (after removing timezone handling) is clean and appropriate for local device storage.


31-50: LGTM!

The method correctly returns cached data when available or sensible defaults otherwise.


144-180: LGTM! Logic correctly handles streak validation on app access.

The streak check logic is sound:

  • daysSinceLastEntry = 1 maintains the streak (user has until end of day to create an entry)
  • daysSinceLastEntry > 1 resets the streak to 0

Good addition of updating lastAccessTime in lines 172-179 even when streak is still valid.


182-193: LGTM!

Correctly resets the current streak while preserving maxStreak for historical tracking.

frontend/app/settings/blocked-users.tsx (2)

10-10: LGTM!

The import is appropriate for generating fallback avatar URLs using the DiceBear API.


206-215: LGTM! Styles are well-defined.

The avatar placeholder styles are appropriate. However, note that these styles are currently unused due to the unreachable code issue identified in lines 79-95. Once the logic is corrected, these styles will function as intended.

frontend/app/settings/index.tsx (1)

48-55: LGTM - Clean removal with preserved reference.

The Storage & Data settings item has been cleanly removed from the navigation flow while preserving the original code as a commented reference. This aligns with the PR's broader settings restructuring.

frontend/app/index.tsx (1)

33-33: LGTM - Improved authentication gating.

The updated condition (!user && !session) correctly ensures that users are redirected to onboarding only when both authentication indicators are absent, preventing premature redirects during initialization.

frontend/app/settings/about.tsx (1)

2-6: LGTM - Clean refactor to internal navigation.

The changes successfully transition from external URL linking to internal routing for legal documents, and introduce responsive scaling for better cross-device support. The consolidated imports and updated UI elements align well with the new legal documents screen.

Also applies to: 43-49, 85-86

frontend/app/settings/legal.tsx (1)

10-32: LGTM - Well-structured legal documents screen.

The two-panel navigation pattern is intuitive, with clean separation between the document list and detail views. The conditional back button behavior appropriately handles both navigation contexts. The component structure is clear and maintainable despite the inline content.

Also applies to: 200-249

frontend/components/capture/music/audio-preview-popover.tsx (2)

32-33: LGTM: Exit animation logic is well-implemented.

The conditional return allows the exit animation to complete before unmounting. The logic correctly keeps the component mounted when music exists but isVisible is false, enabling the SlideOutDown animation to play. This coordinates well with the closeMusicPlayer setTimeout in vault.tsx.


38-44: LGTM: Pointer events and backdrop interaction are correctly configured.

The pointerEvents toggle prevents interaction with the hidden overlay, while the backdrop remains accessible to trigger onClose. The activeOpacity={1} prevents visual feedback on the backdrop, which is appropriate for a dismissal area.

frontend/components/entries/vault-entry-card.tsx (1)

16-16: LGTM: Clean callback propagation with proper typing.

The onMusicPress callback is correctly typed with MusicTag, properly propagated through the component tree, and only passed to VaultCanvas for photo-type entries. The implementation is type-safe and follows good practices.

Also applies to: 37-37, 49-49, 209-211

frontend/app/vault.tsx (1)

58-61: LGTM: Clean music playback integration at screen level.

The music player state management and callback flow are well-structured. Disabling scroll while the music player is visible (line 117) improves the user experience, and the onMusicPress callback is properly threaded through to VaultEntryCard. The AudioPreviewPopover conditional rendering is clean.

Also applies to: 117-117, 134-134, 181-187

frontend/components/capture/canvas/vault-canvas.tsx (2)

5-5: LGTM: Type-safe callback propagation through the canvas component tree.

The onMusicPress callback is correctly:

  • Typed with MusicTag for type safety
  • Propagated from VaultCanvasVaultCanvasItemMusicCanvasItem
  • Made optional at each level with proper optional chaining
  • Threaded through the component hierarchy cleanly

Also applies to: 15-16, 19-19, 54-55, 65-65, 68-68


94-97: LGTM: Proper null checking in music press handler.

The onPress handler correctly guards against null/undefined values by checking both item.music_tag existence and using optional chaining on onMusicPress. This prevents runtime errors if either the music tag or callback is missing.

frontend/types/friends.ts (1)

36-41: LGTM! Clean type definitions.

The type aliases and FriendWithProfile interface properly leverage the Database types, ensuring type safety and alignment with the schema changes (including the new blocked_by field).

frontend/types/database.ts (1)

103-130: LGTM! Type definitions align with migration.

The blocked_by field is correctly added to Row, Insert, and Update types with appropriate nullability, matching the database migration.

frontend/hooks/use-friends.ts (2)

149-163: LGTM! Block action correctly sets blocked_by.

The blockFriend function properly sets blocked_by: userId to track who initiated the block, and the dependency array correctly includes userId.


165-179: LGTM! Unblock correctly clears blocked_by.

Setting blocked_by: null when unblocking and reverting status to ACCEPTED is the correct behavior.

frontend/services/friend-service.ts (2)

158-184: LGTM! Clean friend request implementation.

The validation, logging, and error handling are well-structured. The use of FRIENDSHIP_STATUS.PENDING constant ensures consistency.


198-215: LGTM! Clean friend removal implementation.

Good use of logger and proper error propagation with throw error.

Comment on lines 79 to 95
const avatarUrl = friend.friend_profile?.avatar_url ?? getDefaultAvatarUrl(friend.friend_profile?.full_name ?? "");

return (
<View key={friend.id} style={styles.userItem}>
<View style={styles.avatarContainer}>
<Image
source={{ uri: friend.friend_profile?.avatar_url || undefined }}
style={styles.avatar}
/>
{avatarUrl ? (
<Image
source={{ uri: avatarUrl }}
style={styles.avatar}
/>
) : (
<View style={[styles.avatar, styles.avatarPlaceholder]}>
<Text style={styles.avatarText}>
{(friend.friend_profile?.full_name?.[0] || friend.friend_profile?.username?.[0] || '?').toUpperCase()}
</Text>
</View>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unreachable fallback placeholder code.

The placeholder rendering logic (lines 90-94) is unreachable because avatarUrl is always assigned a value on line 79. The nullish coalescing operator ensures avatarUrl is either friend.friend_profile?.avatar_url or a generated DiceBear URL, so the conditional check on line 84 will always evaluate to true.

🔎 Recommended fix: Choose one fallback strategy

Option 1: Use only the DiceBear fallback (simplest)

Remove the conditional logic since avatarUrl always has a value:

-              const avatarUrl = friend.friend_profile?.avatar_url ?? getDefaultAvatarUrl(friend.friend_profile?.full_name ?? "");
+              const avatarUrl = friend.friend_profile?.avatar_url ?? getDefaultAvatarUrl(friend.friend_profile?.full_name ?? friend.friend_profile?.username ?? "");
               
               return (
                 <View key={friend.id} style={styles.userItem}>
                   <View style={styles.avatarContainer}>
-                    {avatarUrl ? (
-                      <Image
-                        source={{ uri: avatarUrl }}
-                        style={styles.avatar}
-                      />
-                    ) : (
-                      <View style={[styles.avatar, styles.avatarPlaceholder]}>
-                        <Text style={styles.avatarText}>
-                          {(friend.friend_profile?.full_name?.[0] || friend.friend_profile?.username?.[0] || '?').toUpperCase()}
-                        </Text>
-                      </View>
-                    )}
+                    <Image
+                      source={{ uri: avatarUrl }}
+                      style={styles.avatar}
+                    />
                   </View>

Option 2: Use placeholder only when no avatar_url exists

Reserve the colored placeholder for when there's no actual avatar:

-              const avatarUrl = friend.friend_profile?.avatar_url ?? getDefaultAvatarUrl(friend.friend_profile?.full_name ?? "");
+              const avatarUrl = friend.friend_profile?.avatar_url;
               
               return (
                 <View key={friend.id} style={styles.userItem}>
                   <View style={styles.avatarContainer}>
                     {avatarUrl ? (
                       <Image
                         source={{ uri: avatarUrl }}
                         style={styles.avatar}
                       />
                     ) : (
                       <View style={[styles.avatar, styles.avatarPlaceholder]}>
                         <Text style={styles.avatarText}>
                           {(friend.friend_profile?.full_name?.[0] || friend.friend_profile?.username?.[0] || '?').toUpperCase()}
                         </Text>
                       </View>
                     )}
                   </View>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const avatarUrl = friend.friend_profile?.avatar_url ?? getDefaultAvatarUrl(friend.friend_profile?.full_name ?? "");
return (
<View key={friend.id} style={styles.userItem}>
<View style={styles.avatarContainer}>
<Image
source={{ uri: friend.friend_profile?.avatar_url || undefined }}
style={styles.avatar}
/>
{avatarUrl ? (
<Image
source={{ uri: avatarUrl }}
style={styles.avatar}
/>
) : (
<View style={[styles.avatar, styles.avatarPlaceholder]}>
<Text style={styles.avatarText}>
{(friend.friend_profile?.full_name?.[0] || friend.friend_profile?.username?.[0] || '?').toUpperCase()}
</Text>
</View>
)}
const avatarUrl = friend.friend_profile?.avatar_url;
return (
<View key={friend.id} style={styles.userItem}>
<View style={styles.avatarContainer}>
{avatarUrl ? (
<Image
source={{ uri: avatarUrl }}
style={styles.avatar}
/>
) : (
<View style={[styles.avatar, styles.avatarPlaceholder]}>
<Text style={styles.avatarText}>
{(friend.friend_profile?.full_name?.[0] || friend.friend_profile?.username?.[0] || '?').toUpperCase()}
</Text>
</View>
)}
</View>
🤖 Prompt for AI Agents
In frontend/app/settings/blocked-users.tsx around lines 79 to 95, the
placeholder branch is unreachable because avatarUrl is always set using the
nullish coalescing to a generated URL; either (A) always render the <Image> by
removing the conditional and keep the DiceBear fallback (delete the placeholder
branch), or (B) change the avatarUrl assignment to only use
friend.friend_profile?.avatar_url (remove the DiceBear default) so the existing
conditional can render the colored placeholder when avatarUrl is null; update
the avatarUrl computation and remove or preserve branches accordingly to match
the chosen strategy.

Comment on lines +91 to +95
static async getFriends(userId: string): Promise<FriendWithProfile[]> {
if (!userId) {
logger.error('User ID is required to retrive friends');
throw new Error('User ID is required');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "retrive" should be "retrieve".

🔎 Proposed fix
-      logger.error('User ID is required to retrive friends');
+      logger.error('User ID is required to retrieve friends');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static async getFriends(userId: string): Promise<FriendWithProfile[]> {
if (!userId) {
logger.error('User ID is required to retrive friends');
throw new Error('User ID is required');
}
static async getFriends(userId: string): Promise<FriendWithProfile[]> {
if (!userId) {
logger.error('User ID is required to retrieve friends');
throw new Error('User ID is required');
}
🤖 Prompt for AI Agents
In frontend/services/friend-service.ts around lines 91 to 95, the logger message
contains a typo "retrive" — update the string passed to logger.error to "User ID
is required to retrieve friends" (correcting "retrive" to "retrieve"); leave the
thrown Error text as-is unless you also want to make it consistent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
frontend/services/friend-service.ts (2)

123-127: Typo: "retrive" should be "retrieve".

🔎 Proposed fix
-      logger.error('User ID is required to retrive friends');
+      logger.error('User ID is required to retrieve friends');

156-177: Critical: Error check occurs after data processing — potential null dereference.

data?.map() on line 156 processes before checking error (line 175) or handling !data (line 173). If the query fails, data could be null/undefined. While optional chaining prevents immediate crash on .map(), the friends variable would be undefined, and the logic flow is confusing.

🔎 Proposed fix: Check error before processing data
       .or(`user_id.eq.${userId},friend_id.eq.${userId}`)
       .order('created_at', { ascending: false }) as {
         data: any[],
         error: any,
       };

+      if (error) {
+        throw new Error(error.message);
+      }
+
+      if (!data) {
+        return [];
+      }
+
       const friends: FriendWithProfile[] = data.map(friend => {
         const { 
           user_profile: user, 
           friend_profile: friend_, 
           ...friend_record 
         } = friend;

         const profile = friend.friend_id === userId ? 
         user : friend_;

         return {
           ...friend_record,
           friend_profile: profile
         }

       })

-      if (!data) return [];
-
-      if (error) {
-        throw new Error(error.message);
-      }
-
       // Cache the friends data
-      if (data) {
-        await deviceStorage.setFriends(userId, friends);
-      }
+      await deviceStorage.setFriends(userId, friends);

       return friends as FriendWithProfile[];
🧹 Nitpick comments (3)
frontend/hooks/use-friends.ts (2)

69-81: Avoid using any type and as never type assertions.

Using any for updateData and as never for the update call bypasses TypeScript's type safety. Consider defining a proper type for the update payload.

🔎 Proposed fix
-    mutationFn: async ({ id, status, blocked_by }: { id: string; status: typeof FRIENDSHIP_STATUS.ACCEPTED | typeof FRIENDSHIP_STATUS.DECLINED | typeof FRIENDSHIP_STATUS.BLOCKED; blocked_by?: string | null }) => {
+    mutationFn: async ({ id, status, blocked_by }: { 
+      id: string; 
+      status: typeof FRIENDSHIP_STATUS.ACCEPTED | typeof FRIENDSHIP_STATUS.DECLINED | typeof FRIENDSHIP_STATUS.BLOCKED; 
+      blocked_by?: string | null 
+    }) => {
       if (__DEV__) console.log('Updating friendship:', { id, status, blocked_by });

-      const updateData: any = { status };
+      const updateData: { status: string; blocked_by?: string | null } = { status };
       if (blocked_by !== undefined) {
         updateData.blocked_by = blocked_by;
       }

       const { error } = await supabase
         .from(TABLES.FRIENDSHIPS)
-        .update(updateData as never)
+        .update(updateData)
         .eq('id', id);

197-201: Remove debug console.log from production code.

The console.log({ contacts }) on line 199 will output to production logs. Consider wrapping it in __DEV__ or removing it.

🔎 Proposed fix
         const contacts = await FriendService.getSuggestedFriendsFromContacts();
-          console.log({ contacts });
+          if (__DEV__) console.log({ contacts });
           return contacts.filter(contact => contact.id !== profile?.id);
frontend/services/friend-service.ts (1)

200-207: Avoid as never type assertion.

The as never cast on line 206 bypasses type checking. Consider using proper typing for the insert payload.

🔎 Proposed fix
       const { error } = await supabase
         .from(TABLES.FRIENDSHIPS)
         .insert({
           user_id: userId,
           friend_id: friendId,
           status: FRIENDSHIP_STATUS.PENDING,
-        } as never)
+        })
         .select()
         .single();

If TypeScript complains, ensure the Database types in @/types/database.ts are properly generated from your Supabase schema.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3267e2c and 28e30e8.

📒 Files selected for processing (5)
  • frontend/app/index.tsx
  • frontend/app/settings/blocked-users.tsx
  • frontend/hooks/use-friends.ts
  • frontend/services/friend-service.ts
  • frontend/supabase/migrations/20251220000000_add_blocked_by.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/settings/blocked-users.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/hooks/use-friends.ts (1)
frontend/constants/supabase.ts (2)
  • FRIENDSHIP_STATUS (31-36)
  • TABLES (4-15)
frontend/services/friend-service.ts (3)
frontend/types/friends.ts (2)
  • Profile (37-37)
  • FriendWithProfile (39-41)
frontend/lib/logger.ts (2)
  • logger (18-36)
  • error (32-35)
frontend/constants/supabase.ts (2)
  • TABLES (4-15)
  • FRIENDSHIP_STATUS (31-36)
🔇 Additional comments (8)
frontend/app/index.tsx (1)

16-20: No action neededprefetchSuggestedFriends is properly memoized.

The function is wrapped in useCallback within the useFriends hook (with dependencies [queryClient, profile?.id]), making it stable across renders. The dependency array now correctly includes it, resolving the earlier exhaustive-deps warning.

Likely an incorrect or invalid review comment.

frontend/supabase/migrations/20251220000000_add_blocked_by.sql (2)

13-21: Good implementation of the WITH CHECK clause.

The policy now correctly restricts blocked_by manipulation: users can only set it to NULL or their own auth.uid(), preventing unauthorized attribution of blocks. This addresses the previous review feedback.


4-8: LGTM: Schema changes are well-structured.

The blocked_by column with ON DELETE SET NULL maintains referential integrity gracefully, and the index will optimize queries filtering by blocker.

frontend/hooks/use-friends.ts (2)

40-46: Good refactor: Delegating to FriendService.

The query now cleanly delegates to FriendService.getFriends(), improving separation of concerns. The enabled: !!userId guard ensures the non-null assertion on line 43 is safe.


52-54: Good implementation of blocked friends filtering.

The filter correctly identifies friends blocked by the current user using blocked_by === userId, aligning with the new schema.

frontend/services/friend-service.ts (3)

17-47: Good addition of validation helpers.

The isValidEmail, isValidPhoneNumber, and isValidUUID validators address the previous review concerns about untrusted input being interpolated into queries. The implementations are practical and cover common edge cases.


268-283: Good input validation before query construction.

The contact data is now properly validated and filtered using the new helper methods before being used in the query, addressing the previous security concerns about untrusted device contact data.


294-310: Good defensive query building with validated data.

The query construction now only adds .or() and .not() clauses when valid data exists, preventing malformed queries and potential injection issues.

Comment on lines 149 to 163
const blockFriend = useCallback(async (friendshipId: string) => {
try {
await updateFriendshipMutation.mutateAsync({ id: friendshipId, status: FRIENDSHIP_STATUS.BLOCKED });
await updateFriendshipMutation.mutateAsync({
id: friendshipId,
status: FRIENDSHIP_STATUS.BLOCKED,
blocked_by: userId!
});
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to block friend',
};
}
}, [updateFriendshipMutation]);
}, [updateFriendshipMutation, userId]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-null assertion on userId could cause runtime error.

blockFriend uses userId! but this callback can be invoked by consumers even when userId is undefined. Consider adding a guard or returning an error early.

🔎 Proposed fix
   const blockFriend = useCallback(async (friendshipId: string) => {
+    if (!userId) {
+      return { success: false, error: 'User ID is required to block a friend' };
+    }
     try {
       await updateFriendshipMutation.mutateAsync({ 
         id: friendshipId, 
         status: FRIENDSHIP_STATUS.BLOCKED,
-        blocked_by: userId!
+        blocked_by: userId
       });
       return { success: true };
🤖 Prompt for AI Agents
In frontend/hooks/use-friends.ts around lines 149 to 163, the callback uses a
non-null assertion userId! which can crash if userId is undefined; add an
explicit guard at the start of blockFriend to check for a valid userId and
immediately return a failure result (or throw a clear error) when missing,
remove the non-null assertion from the mutate payload, and keep userId in the
dependency array so the callback updates when auth state changes.

Comment on lines +285 to +286
logger.info(`Getting retriving saved friends for ${session.user.id}`);
const friends = await deviceStorage.getFriends(session.user.id);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "retriving" should be "retrieving".

🔎 Proposed fix
-      logger.info(`Getting retriving saved friends for ${session.user.id}`);
+      logger.info(`Retrieving saved friends for ${session.user.id}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.info(`Getting retriving saved friends for ${session.user.id}`);
const friends = await deviceStorage.getFriends(session.user.id);
logger.info(`Retrieving saved friends for ${session.user.id}`);
const friends = await deviceStorage.getFriends(session.user.id);
🤖 Prompt for AI Agents
In frontend/services/friend-service.ts around lines 285 to 286, there's a typo
in the log message: "Getting retriving saved friends for ..." should read
"Getting retrieving saved friends for ...". Update the logger.info string to
correct "retriving" to "retrieving" (keep the rest of the message as-is) so logs
are spelled correctly.

@fortune710 fortune710 merged commit ef073fa into main Jan 3, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant