import 'dart:math'; import 'dart:typed_data'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../../domain/models/feed_post_model.dart'; class PostService { const PostService(this._client); final SupabaseClient _client; User get _currentUser { final user = _client.auth.currentUser; if (user == null) { throw StateError('No authenticated user'); } return user; } /// Fetches the global feed in chronological order. Future> fetchFeed() async { final user = _currentUser; // Fetch posts with user profile data using joins if possible, // but sticking to the established pattern for consistency and safety. final posts = await _client .from('posts') .select('id, user_id, caption, image_url, likes_count, created_at') .order('created_at', ascending: false); if (posts.isEmpty) return const []; final postRows = List>.from(posts); final userIds = postRows .map((e) => e['user_id'] as String?) .whereType() .toSet() .toList(); // Fetch profiles for the users in the feed final profiles = userIds.isEmpty ? >[] : List>.from(await _client .from('profiles') .select('user_id, username, avatar_url') .inFilter('user_id', userIds)); final profileByUserId = { for (final profile in profiles) (profile['user_id'] as String): profile, }; // Fetch the current user's likes for these posts to set isLiked final postIds = postRows.map((e) => e['id'] as String).toList(); final myLikes = postIds.isEmpty ? >[] : List>.from(await _client .from('post_likes') .select('post_id') .eq('user_id', user.id) .inFilter('post_id', postIds)); final likedPostIds = myLikes.map((e) => e['post_id'] as String).toSet(); return postRows.map((row) { final profile = profileByUserId[row['user_id']] ?? const {}; return FeedPostModel( id: row['id'] as String, userId: row['user_id'] as String, caption: (row['caption'] as String?) ?? '', imageUrl: (row['image_url'] as String?) ?? '', createdAt: DateTime.parse(row['created_at'] as String), likesCount: (row['likes_count'] as int?) ?? 0, isLiked: likedPostIds.contains(row['id']), username: (profile['username'] as String?) ?? 'riot_user', avatarUrl: (profile['avatar_url'] as String?) ?? '', ); }).toList(); } /// Uploads a new post with an image and optional caption. Future uploadPost({ required String caption, required Uint8List imageBytes, required String extension, }) async { final user = _currentUser; final ts = DateTime.now().millisecondsSinceEpoch; final random = Random().nextInt(99999); final fileName = '$ts-$random.$extension'; final path = 'posts/${user.id}/$fileName'; // Upload image to Supabase Storage await _client.storage.from('post-images').uploadBinary( path, imageBytes, fileOptions: const FileOptions(upsert: false), ); final imageUrl = _client.storage.from('post-images').getPublicUrl(path); // Insert post record into PostgreSQL await _client.from('posts').insert({ 'user_id': user.id, 'caption': caption.trim(), 'image_url': imageUrl, 'likes_count': 0, }); } /// Toggles a like on a post. Future toggleLike({ required String postId, required bool currentlyLiked, required int currentLikesCount, }) async { final user = _currentUser; if (currentlyLiked) { // Unlike await _client .from('post_likes') .delete() .eq('post_id', postId) .eq('user_id', user.id); await _client.from('posts').update({ 'likes_count': max(0, currentLikesCount - 1), }).eq('id', postId); } else { // Like await _client.from('post_likes').upsert({ 'post_id': postId, 'user_id': user.id, }); await _client.from('posts').update({ 'likes_count': currentLikesCount + 1, }).eq('id', postId); } } /// Deletes a post belonging to the current user. Future deletePost(String postId) async { final user = _currentUser; await _client .from('posts') .delete() .eq('id', postId) .eq('user_id', user.id); } }