feat/user-api-stubs

Summary

Replaced all stub implementations in UserApiService, EventApiService, and related screens with real Supabase operations. Also fixed a critical navigation bug in the public profile screen.

Changes

user_api_service.dart — Follow / Unfollow / Block / Report wired to Supabase

All social graph operations were previously no-ops returning hardcoded true.

Follow / Unfollow:

  • followUserUPSERT user_follows (follower_id, following_id) with onConflict: 'follower_id,following_id'
  • unfollowUserDELETE user_follows WHERE follower_id AND following_id
  • isFollowingUserSELECT follower_id LIMIT 1, returns rows.isNotEmpty
  • getUserFollowersuser_follows JOIN profiles!user_follows_follower_id_fkey paginated
  • getUserFollowinguser_follows JOIN profiles!user_follows_following_id_fkey paginated

Block / Unblock:

  • blockUserUPSERT user_blocks (blocker_id, blocked_id) with onConflict: 'blocker_id,blocked_id'
  • unblockUserDELETE user_blocks WHERE blocker_id AND blocked_id
  • getBlockedUsersuser_blocks JOIN profiles!user_blocks_blocked_id_fkey paginated

Report:

  • reportUserINSERT user_reports (reporter_id, reported_id, reason, description)

Required DB tables (migrations 20260318000005, 20260318000006):

  • user_blocks (blocker_id, blocked_id) with PRIMARY KEY, no_self_block CHECK, RLS (self-manage only)
  • user_reports (reporter_id, reported_id, reason, description, status) with no_self_report CHECK, valid_status CHECK, RLS (insert/read own)

event_api_service.dart — updateEvent and deleteEvent

  • updateEvent — real UPDATE events SET ... WHERE id AND organizer_id = currentUser, returns updated Event. Owner-only via Supabase .eq('organizer_id', userId).
  • deleteEvent — soft-delete: UPDATE events SET status='cancelled' (owner-only). Preserves data, RSVP history, and attendee records.

event_details_screen.dart — Organizer follow wired to UserApiService

Previously _toggleFollowOrganizer only toggled local state with no API call.

  • Added UserApiService _userApiService field
  • initState calls _loadFollowState()isFollowingUser(organizerId) to initialize _isFollowingOrganizer
  • _toggleFollowOrganizer now async: optimistic local update → followUser/unfollowUser API call → revert on failure with SnackBar feedback

public_profile_screen.dart — Chat button navigates to real DM

Bug: context.push('/social/chat/${user.id}') passed a profile UUID as the chat route param. The chat screen calls fetchChat(chatId) which found no chats row with that UUID, causing an empty/error state.

Fix: Replace direct push with:

onTap: () async {
  final chat = await MessagingApiService().createDirectChat(user.id);
  if (chat != null && context.mounted) {
    context.push('/social/chat/${chat.id}');
  }
},

createDirectChat finds the existing DM room or creates a new one, returning a valid Chat with a real chats.id. The context.mounted guard prevents navigation on a disposed widget.