first commit
This commit is contained in:
137
lib/features/feed/data/services/feed_service.dart
Normal file
137
lib/features/feed/data/services/feed_service.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../../domain/models/feed_post_model.dart';
|
||||
|
||||
class FeedService {
|
||||
const FeedService(this._client);
|
||||
|
||||
final SupabaseClient _client;
|
||||
|
||||
User get _currentUser {
|
||||
final user = _client.auth.currentUser;
|
||||
if (user == null) {
|
||||
throw StateError('No authenticated user');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<List<FeedPostModel>> fetchFeed() async {
|
||||
final user = _currentUser;
|
||||
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<Map<String, dynamic>>.from(posts);
|
||||
final userIds = postRows
|
||||
.map((e) => e['user_id'] as String?)
|
||||
.whereType<String>()
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
final profiles = userIds.isEmpty
|
||||
? <Map<String, dynamic>>[]
|
||||
: List<Map<String, dynamic>>.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,
|
||||
};
|
||||
|
||||
final postIds = postRows.map((e) => e['id'] as String).toList();
|
||||
final myLikes = postIds.isEmpty
|
||||
? <Map<String, dynamic>>[]
|
||||
: List<Map<String, dynamic>>.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();
|
||||
}
|
||||
|
||||
Future<void> createPost({
|
||||
required String caption,
|
||||
required Uint8List imageBytes,
|
||||
required String extension,
|
||||
}) async {
|
||||
final user = _currentUser;
|
||||
final ts = DateTime.now().millisecondsSinceEpoch;
|
||||
final random = Random().nextInt(99999);
|
||||
final path = 'posts/${user.id}/$ts-$random.$extension';
|
||||
|
||||
await _client.storage.from('post-images').uploadBinary(
|
||||
path,
|
||||
imageBytes,
|
||||
fileOptions: const FileOptions(upsert: false),
|
||||
);
|
||||
|
||||
final imageUrl = _client.storage.from('post-images').getPublicUrl(path);
|
||||
|
||||
await _client.from('posts').insert({
|
||||
'user_id': user.id,
|
||||
'caption': caption.trim(),
|
||||
'image_url': imageUrl,
|
||||
'likes_count': 0,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> toggleLike({
|
||||
required String postId,
|
||||
required bool currentlyLiked,
|
||||
required int currentLikesCount,
|
||||
}) async {
|
||||
final user = _currentUser;
|
||||
if (currentlyLiked) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Future<void> deleteOwnPost(String postId) async {
|
||||
final user = _currentUser;
|
||||
await _client
|
||||
.from('posts')
|
||||
.delete()
|
||||
.eq('id', postId)
|
||||
.eq('user_id', user.id);
|
||||
}
|
||||
}
|
||||
152
lib/features/feed/data/services/post_service.dart
Normal file
152
lib/features/feed/data/services/post_service.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
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<List<FeedPostModel>> 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<Map<String, dynamic>>.from(posts);
|
||||
final userIds = postRows
|
||||
.map((e) => e['user_id'] as String?)
|
||||
.whereType<String>()
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
// Fetch profiles for the users in the feed
|
||||
final profiles = userIds.isEmpty
|
||||
? <Map<String, dynamic>>[]
|
||||
: List<Map<String, dynamic>>.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
|
||||
? <Map<String, dynamic>>[]
|
||||
: List<Map<String, dynamic>>.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<void> 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<void> 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<void> deletePost(String postId) async {
|
||||
final user = _currentUser;
|
||||
await _client
|
||||
.from('posts')
|
||||
.delete()
|
||||
.eq('id', postId)
|
||||
.eq('user_id', user.id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user