sojorn/sojorn_app/lib/services/api_service.dart.backup
2026-02-15 00:33:24 -06:00

1134 lines
32 KiB
Plaintext

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:supabase_flutter/supabase_flutter.dart';
import '../config/supabase_config.dart';
import '../models/category.dart';
import '../models/profile.dart';
import '../models/profile_privacy_settings.dart';
import '../models/post.dart';
import '../models/comment.dart';
import '../models/tone_analysis.dart' hide ToneAnalysis;
import '../models/search_results.dart';
import '../models/notification.dart';
import '../models/beacon.dart';
/// API service for calling sojorn Edge Functions
class ApiService {
final SupabaseClient _supabase;
ApiService(this._supabase);
/// Helper to make Edge Function calls
/// Trust the Supabase SDK to handle auth, tokens, and headers automatically
Future<Map<String, dynamic>> _callFunction(
String functionName, {
HttpMethod method = HttpMethod.post,
Map<String, dynamic>? queryParams,
Object? body,
}) async {
final response = await _supabase.functions.invoke(
functionName,
method: method,
queryParameters: queryParams,
body: body,
);
// Parse response data
final data = response.data;
if (data is Map<String, dynamic>) {
return data;
}
if (data is Map) {
return Map<String, dynamic>.from(data);
}
if (data is String && data.isNotEmpty) {
final parsed = jsonDecode(data);
if (parsed is Map<String, dynamic>) {
return parsed;
}
}
throw Exception('Unexpected response from $functionName');
}
List<Map<String, dynamic>> _normalizeListResponse(dynamic response) {
try {
// Supabase Flutter SDK returns data directly, not wrapped in PostgrestResponse
if (response == null) return [];
// Handle different response types
List<dynamic> rawData;
if (response is List<dynamic>) {
rawData = response;
} else if (response is Map<String, dynamic>) {
// If it's a single object, wrap it in a list
return [response];
} else {
return [];
}
return rawData
.where((item) => item is Map<String, dynamic>)
.map((item) => Map<String, dynamic>.from(item as Map))
.toList();
} catch (e) {
print('_normalizeListResponse error: $e');
return [];
}
}
static const String _postSelect = '''
id,
body,
author_id,
category_id,
tone_label,
cis_score,
status,
created_at,
edited_at,
deleted_at,
is_edited,
allow_chain,
chain_parent_id,
image_url,
chain_parent:posts (
id,
body,
created_at,
author:profiles!posts_author_id_fkey (
id,
handle,
display_name
)
),
metrics:post_metrics!post_metrics_post_id_fkey (
like_count,
save_count,
view_count
),
author:profiles!posts_author_id_fkey (
id,
handle,
display_name,
trust_state (
user_id,
harmony_score,
tier,
posts_today
)
)
''';
// ============================================================================
// CATEGORIES
// ============================================================================
/// Fetch all categories
Future<List<Category>> getCategories() async {
final response = await _supabase
.from('categories')
.select()
.order('name', ascending: true);
return (response as List).map((json) => Category.fromJson(json)).toList();
}
/// Fetch categories the user has enabled
Future<List<Category>> getEnabledCategories() async {
final categories = await getCategories();
final enabledIds = await _getEnabledCategoryIds();
if (enabledIds.isEmpty) {
return categories.where((category) => !category.defaultOff).toList();
}
return categories
.where((category) => enabledIds.contains(category.id))
.toList();
}
/// Check if the current user has a profile
Future<bool> hasProfile() async {
final user = _supabase.auth.currentUser;
if (user == null) {
return false;
}
try {
final response = await _supabase
.from('profiles')
.select('id')
.eq('id', user.id)
.limit(1);
return (response as List).isNotEmpty;
} catch (e) {
// If we get an error (like RLS blocking or JWT issues), assume no profile
// This allows the signup flow to work even if RLS is strict
print('hasProfile error (treating as false): $e');
return false;
}
}
/// Check if the user has completed category selection
Future<bool> hasCategorySelection() async {
try {
final enabledIds = await _getEnabledCategoryIds();
return enabledIds.isNotEmpty;
} catch (e) {
// If we get an error, assume no category selection
print('hasCategorySelection error (treating as false): $e');
return false;
}
}
/// Persist user category settings for all categories
Future<void> setUserCategorySettings({
required List<Category> categories,
required Set<String> enabledCategoryIds,
}) async {
final user = _supabase.auth.currentUser;
if (user == null) {
throw Exception('Not authenticated');
}
final payload = categories
.map(
(category) => {
'user_id': user.id,
'category_id': category.id,
'enabled': enabledCategoryIds.contains(category.id),
},
)
.toList();
await _supabase.from('user_category_settings').upsert(payload);
}
Future<Set<String>> _getEnabledCategoryIds() async {
try {
final response = await _supabase
.from('user_category_settings')
.select('category_id')
.eq('enabled', true);
return (response as List)
.map((row) => row['category_id'] as String)
.toSet();
} catch (e) {
// If query fails, return empty set
print('_getEnabledCategoryIds error: $e');
return {};
}
}
// ============================================================================
// USER MANAGEMENT
// ============================================================================
/// Create profile (after Supabase Auth signup)
Future<Profile> createProfile({
required String handle,
required String displayName,
String? bio,
}) async {
final response = await _callFunction(
'signup',
method: 'POST',
body: {
'handle': handle,
'display_name': displayName,
if (bio != null) 'bio': bio,
},
);
if (_isSuccessStatus(response)) {
final data = jsonDecode(response.body);
return Profile.fromJson(data['profile']);
} else {
throw Exception(_extractError(response, 'Failed to create profile'));
}
}
/// Get profile by handle
Future<Map<String, dynamic>> getProfile({String? handle}) async {
final response = await _callFunction(
'profile',
queryParams: handle != null ? {'handle': handle} : null,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return {
'profile': Profile.fromJson(data['profile']),
'stats': ProfileStats.fromJson(data['stats']),
'is_following': data['is_following'] as bool? ?? false,
};
} else {
throw Exception(_extractError(response, 'Failed to get profile'));
}
}
/// Fetch posts authored by the given profile
Future<List<Post>> getProfilePosts({
required String authorId,
int limit = 20,
int offset = 0,
}) async {
final safeOffset = offset < 0 ? 0 : offset;
final safeLimit = limit.clamp(1, 100).toInt();
final rangeEnd = safeOffset + safeLimit - 1;
try {
final response = await _supabase
.from('posts')
.select('''
id,
body,
author_id,
category_id,
tone_label,
cis_score,
status,
created_at,
edited_at,
deleted_at,
allow_chain,
chain_parent_id,
image_url,
chain_parent:posts (
id,
body,
created_at,
author:profiles!posts_author_id_fkey (
id,
handle,
display_name
)
),
metrics:post_metrics!post_metrics_post_id_fkey (
like_count,
save_count,
view_count
),
author:profiles!posts_author_id_fkey (
id,
handle,
display_name,
trust_state (
user_id,
harmony_score,
tier,
posts_today
)
)
''')
.eq('author_id', authorId)
.order('created_at', ascending: false)
.range(safeOffset, rangeEnd);
final rows = _normalizeListResponse(response);
return rows.map(Post.fromJson).toList();
} catch (error) {
final message = error is PostgrestException
? error.message
: 'Failed to load profile posts';
print('Error fetching profile posts: $error');
throw Exception('Failed to load profile posts: $message');
}
}
/// Fetch a single post by id
Future<Post> getPostById(String postId) async {
try {
final response = await _supabase
.from('posts')
.select(_postSelect)
.eq('id', postId)
.single();
// Handle both Map and List response formats
final Map<String, dynamic> data;
if (response is Map<String, dynamic>) {
data = response;
} else if (response is List && response.isNotEmpty) {
data = Map<String, dynamic>.from(response[0] as Map);
} else {
data = {};
}
return Post.fromJson(data);
} catch (error) {
final message = error is PostgrestException
? error.message
: 'Failed to load post';
throw Exception('Failed to load post: $message');
}
}
/// Update own profile
Future<Profile> updateProfile({
String? handle,
String? displayName,
String? bio,
String? location,
String? website,
List<String>? interests,
String? avatarUrl,
String? coverUrl,
}) async {
final response = await _callFunction(
'profile',
method: 'PATCH',
body: {
if (handle != null) 'handle': handle,
if (displayName != null) 'display_name': displayName,
if (bio != null) 'bio': bio,
if (location != null) 'location': location,
if (website != null) 'website': website,
if (interests != null) 'interests': interests,
if (avatarUrl != null) 'avatar_url': avatarUrl,
if (coverUrl != null) 'cover_url': coverUrl,
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return Profile.fromJson(data['profile']);
} else {
throw Exception(_extractError(response, 'Failed to update profile'));
}
}
/// Fetch or initialize privacy settings for the current user
Future<ProfilePrivacySettings> getPrivacySettings() async {
final userId = _supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('Not authenticated');
}
final response = await _supabase
.from('profile_privacy_settings')
.select()
.eq('user_id', userId)
.limit(1);
final rows = _normalizeListResponse(response);
if (rows.isNotEmpty) {
return ProfilePrivacySettings.fromJson(rows.first);
}
final now = DateTime.now().toIso8601String();
final defaults = ProfilePrivacySettings.defaults(userId);
final inserted = await _supabase
.from('profile_privacy_settings')
.insert({
...defaults.toJson(),
'created_at': now,
'updated_at': now,
})
.select()
.single();
return ProfilePrivacySettings.fromJson(inserted);
}
/// Update privacy settings for the current user
Future<ProfilePrivacySettings> updatePrivacySettings(
ProfilePrivacySettings settings,
) async {
final userId = _supabase.auth.currentUser?.id;
if (userId == null) {
throw Exception('Not authenticated');
}
final payload = settings.toJson()
..['user_id'] = userId
..['updated_at'] = DateTime.now().toIso8601String();
final response = await _supabase
.from('profile_privacy_settings')
.upsert(payload)
.select()
.single();
return ProfilePrivacySettings.fromJson(response);
}
/// Fetch posts the user has appreciated
Future<List<Post>> getAppreciatedPosts({
required String userId,
int limit = 20,
int offset = 0,
}) async {
final safeLimit = limit.clamp(1, 100).toInt();
final safeOffset = offset < 0 ? 0 : offset;
final rangeEnd = safeOffset + safeLimit - 1;
final response = await _supabase
.from('post_likes')
.select('post:posts ($_postSelect)')
.eq('user_id', userId)
.order('created_at', ascending: false)
.range(safeOffset, rangeEnd);
// Handle embedded response format
final List<dynamic> rawData = response as List<dynamic>;
return rawData
.map((row) => row['post'] as Map<String, dynamic>?)
.where((post) => post != null)
.map((post) {
final payload = Map<String, dynamic>.from(post!);
payload['user_liked'] = true;
return Post.fromJson(payload);
})
.toList();
}
/// Fetch posts the user has saved
Future<List<Post>> getSavedPosts({
required String userId,
int limit = 20,
int offset = 0,
}) async {
final safeLimit = limit.clamp(1, 100).toInt();
final safeOffset = offset < 0 ? 0 : offset;
final rangeEnd = safeOffset + safeLimit - 1;
final response = await _supabase
.from('post_saves')
.select('post:posts ($_postSelect)')
.eq('user_id', userId)
.order('created_at', ascending: false)
.range(safeOffset, rangeEnd);
// Handle embedded response format
final List<dynamic> rawData = response as List<dynamic>;
return rawData
.map((row) => row['post'] as Map<String, dynamic>?)
.where((post) => post != null)
.map((post) {
final payload = Map<String, dynamic>.from(post!);
payload['user_saved'] = true;
return Post.fromJson(payload);
})
.toList();
}
/// Fetch posts by an author that have chain responses
Future<List<Post>> getChainedPostsForAuthor({
required String authorId,
int limit = 20,
int offset = 0,
}) async {
final safeLimit = limit.clamp(1, 100).toInt();
final safeOffset = offset < 0 ? 0 : offset;
final rangeEnd = safeOffset + safeLimit - 1;
final response = await _supabase
.from('posts')
.select('$_postSelect, chain_children:posts!inner(id)')
.eq('author_id', authorId)
.order('created_at', ascending: false)
.range(safeOffset, rangeEnd);
final rows = _normalizeListResponse(response);
return rows.map(Post.fromJson).toList();
}
/// Follow a user
Future<void> followUser(String userId) async {
final response = await _callFunction(
'follow',
method: 'POST',
body: {'user_id': userId},
);
if (response.statusCode != 200) {
throw Exception(
jsonDecode(response.body)['error'] ?? 'Failed to follow user',
);
}
}
/// Unfollow a user
Future<void> unfollowUser(String userId) async {
final response = await _callFunction(
'follow',
method: 'DELETE',
body: {'user_id': userId},
);
if (response.statusCode != 200) {
throw Exception(
jsonDecode(response.body)['error'] ?? 'Failed to unfollow user',
);
}
}
/// Block a user
Future<void> blockUser(String userId) async {
final response = await _callFunction(
'block',
method: 'POST',
body: {'user_id': userId},
);
if (response.statusCode != 200) {
throw Exception(
jsonDecode(response.body)['error'] ?? 'Failed to block user',
);
}
}
/// Unblock a user
Future<void> unblockUser(String userId) async {
final response = await _callFunction(
'block',
method: 'DELETE',
body: {'user_id': userId},
);
if (response.statusCode != 200) {
throw Exception(
jsonDecode(response.body)['error'] ?? 'Failed to unblock user',
);
}
}
// ============================================================================
// CONTENT PUBLISHING
// ============================================================================
/// Publish a post
///
/// Supports both regular posts and beacons through unified API.
/// For beacons, set [isBeacon] to true and provide [beaconType], [lat], and [long].
Future<Post> publishPost({
required String categoryId,
required String body,
String bodyFormat = 'plain',
bool allowChain = true,
String? chainParentId,
String? imageUrl,
// Beacon-specific parameters
bool isBeacon = false,
BeaconType? beaconType,
double? lat,
double? long,
}) async {
final response = await _callFunction(
'publish-post',
method: 'POST',
body: {
'category_id': categoryId,
'body': body,
'body_format': bodyFormat,
'allow_chain': allowChain,
if (chainParentId != null) 'chain_parent_id': chainParentId,
if (imageUrl != null) 'image_url': imageUrl,
// Beacon fields
if (isBeacon) 'is_beacon': true,
if (beaconType != null) 'beacon_type': beaconType.value,
if (lat != null) 'beacon_lat': lat,
if (long != null) 'beacon_long': long,
},
);
if (_isSuccessStatus(response)) {
final data = jsonDecode(response.body);
return Post.fromJson(data['post']);
} else {
throw Exception(_extractError(response, 'Failed to publish post'));
}
}
/// Publish a comment (mutual-follow only)
Future<Comment> publishComment({
required String postId,
required String body,
}) async {
final response = await _callFunction(
'publish-comment',
method: 'POST',
body: {'post_id': postId, 'body': body},
);
if (_isSuccessStatus(response)) {
final data = jsonDecode(response.body);
return Comment.fromJson(data['comment']);
} else {
throw Exception(_extractError(response, 'Failed to publish comment'));
}
}
// ============================================================================
// TONE ANALYSIS
// ============================================================================
/// Analyze the tone of content using AI moderation
///
/// Uses intent-based detection to allow authentic expression while
/// rejecting slurs and hostile attacks.
///
/// [text] The text to analyze
/// [imageUrl] Optional image URL for multi-modal analysis
///
/// Returns a [ToneCheckResult]. If the analysis fails (API error,
/// timeout, etc.), returns a default "neutral" result to allow the post.
Future<ToneCheckResult> checkTone(
String text, {
String? imageUrl,
}) async {
try {
final response = await _supabase.functions.invoke(
'tone-check',
method: HttpMethod.post,
body: {
'text': text,
if (imageUrl != null) 'imageUrl': imageUrl,
},
);
final data = response.data;
if (data is Map<String, dynamic>) {
return ToneCheckResult.fromJson(data);
}
if (data is Map) {
return ToneCheckResult.fromJson(Map<String, dynamic>.from(data));
}
if (data is String && data.isNotEmpty) {
final parsed = jsonDecode(data);
if (parsed is Map<String, dynamic>) {
return ToneCheckResult.fromJson(parsed);
}
}
// Fallback to neutral if response format is invalid
return ToneCheckResult(
tone: ToneCategory.neutral,
cis: 0.8,
flags: [],
reason: 'Analysis complete',
);
} catch (e) {
print('Tone analysis error: $e');
// FALLBACK: Allow post with neutral rating when AI is unavailable
// This prevents blocking users when OpenAI has issues
return ToneCheckResult(
tone: ToneCategory.neutral,
cis: 0.8,
flags: [],
reason: 'Content approved (moderation temporarily unavailable)',
);
}
}
// ============================================================================
// ENGAGEMENT
// ============================================================================
/// Appreciate a post
Future<void> appreciatePost(String postId) async {
final response = await _callFunction(
'appreciate',
method: 'POST',
body: {'post_id': postId},
);
if (response.statusCode != 200) {
throw Exception(_extractError(response, 'Failed to appreciate post'));
}
}
/// Remove appreciation
Future<void> unappreciatePost(String postId) async {
final response = await _callFunction(
'appreciate',
method: 'DELETE',
body: {'post_id': postId},
);
if (response.statusCode != 200) {
throw Exception(_extractError(response, 'Failed to remove appreciation'));
}
}
/// Save a post
Future<void> savePost(String postId) async {
final response = await _callFunction(
'save',
method: 'POST',
body: {'post_id': postId},
);
if (response.statusCode != 200) {
throw Exception(_extractError(response, 'Failed to save post'));
}
}
/// Unsave a post
Future<void> unsavePost(String postId) async {
final response = await _callFunction(
'save',
method: 'DELETE',
body: {'post_id': postId},
);
if (response.statusCode != 200) {
throw Exception(_extractError(response, 'Failed to unsave post'));
}
}
/// Report content or user
Future<void> reportContent({
required String targetType,
required String targetId,
required String reason,
}) async {
final response = await _callFunction(
'report',
method: 'POST',
body: {
'target_type': targetType,
'target_id': targetId,
'reason': reason,
},
);
if (!_isSuccessStatus(response)) {
throw Exception(_extractError(response, 'Failed to submit report'));
}
}
// ============================================================================
// FEEDS
// ============================================================================
/// Get personal feed (chronological from follows)
Future<List<Post>> getPersonalFeed({int limit = 50, int offset = 0}) async {
final response = await _callFunction(
'feed-personal',
method: 'GET',
queryParams: {'limit': limit.toString(), 'offset': offset.toString()},
);
if (_isSuccessStatus(response)) {
final data = jsonDecode(response.body);
return (data['posts'] as List)
.map((json) => Post.fromJson(json))
.toList();
} else {
throw Exception(_extractError(response, 'Failed to get personal feed'));
}
}
/// Get sojorn feed (algorithmic FYP)
Future<List<Post>> getsojornFeed({int limit = 50, int offset = 0}) async {
final response = await _callFunction(
'feed-sojorn',
method: 'GET',
queryParams: {'limit': limit.toString(), 'offset': offset.toString()},
);
if (_isSuccessStatus(response)) {
final data = jsonDecode(response.body);
return (data['posts'] as List)
.map((json) => Post.fromJson(json))
.toList();
} else {
throw Exception(_extractError(response, 'Failed to get sojorn feed'));
}
}
/// Get trending posts by category
Future<List<Post>> getTrending({
String category = 'general',
int limit = 20,
}) async {
final response = await _callFunction(
'trending',
queryParams: {'category': category, 'limit': limit.toString()},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return (data['posts'] as List)
.map((json) => Post.fromJson(json))
.toList();
} else {
throw Exception(_extractError(response, 'Failed to get trending'));
}
}
// ============================================================================
// POST MANAGEMENT (EDIT/DELETE)
// ============================================================================
/// Edit a post (within 2-minute window)
/// Requires passing moderation check
Future<void> editPost({
required String postId,
required String content,
}) async {
final response = await _callFunction(
'manage-post',
method: 'POST',
body: {
'action': 'edit',
'post_id': postId,
'content': content,
},
);
if (!_isSuccessStatus(response)) {
throw Exception(_extractError(response, 'Failed to edit post'));
}
}
/// Soft delete a post
Future<void> deletePost(String postId) async {
final response = await _callFunction(
'manage-post',
method: 'POST',
body: {
'action': 'delete',
'post_id': postId,
},
);
if (!_isSuccessStatus(response)) {
throw Exception(_extractError(response, 'Failed to delete post'));
}
}
// ============================================================================
// CHAIN RESPONSES
// ============================================================================
/// Fetch chain responses for a post (chronological)
Future<List<Post>> getChainPosts({
required String parentPostId,
int limit = 50,
}) async {
final safeLimit = limit.clamp(1, 100).toInt();
final response = await _supabase
.from('posts')
.select('''
id,
body,
author_id,
category_id,
tone_label,
cis_score,
status,
created_at,
edited_at,
deleted_at,
allow_chain,
chain_parent_id,
image_url,
chain_parent:posts (
id,
body,
created_at,
author:profiles!posts_author_id_fkey (
id,
handle,
display_name
)
),
metrics:post_metrics!post_metrics_post_id_fkey (
like_count,
save_count,
view_count
),
author:profiles!posts_author_id_fkey (
id,
handle,
display_name,
trust_state (
user_id,
harmony_score,
tier,
posts_today
)
)
''')
.eq('chain_parent_id', parentPostId)
.order('created_at', ascending: true)
.limit(safeLimit);
final rows = _normalizeListResponse(response);
return rows.map(Post.fromJson).toList();
}
/// Deactivate user account
Future<void> deactivateAccount() async {
final response = await _callFunction(
'deactivate-account',
method: 'POST',
);
if (response.statusCode != 200) {
throw Exception(_extractError(response, 'Failed to deactivate account'));
}
}
/// Reactivate a deactivated account
Future<void> reactivateAccount() async {
final response = await _callFunction(
'deactivate-account/reactivate',
method: 'POST',
);
if (response.statusCode != 200) {
throw Exception(_extractError(response, 'Failed to reactivate account'));
}
}
/// Request permanent account deletion (30-day waiting period)
Future<Map<String, dynamic>> requestAccountDeletion() async {
final response = await _callFunction(
'delete-account',
method: 'POST',
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data;
} else {
throw Exception(_extractError(response, 'Failed to request account deletion'));
}
}
/// Cancel pending account deletion request
Future<void> cancelAccountDeletion() async {
final response = await _callFunction(
'delete-account/cancel',
method: 'POST',
);
if (response.statusCode != 200) {
throw Exception(_extractError(response, 'Failed to cancel account deletion'));
}
}
// ============================================================================
// SEARCH
// ============================================================================
/// Search for users and hashtags simultaneously
/// Uses the database RPC function for efficient searching
Future<SearchResults> search(String query) async {
if (query.trim().isEmpty) {
return SearchResults(users: [], tags: [], posts: []);
}
try {
final response = await _supabase
.rpc('search_sojorn', params: {'p_query': query.trim()});
if (response is Map<String, dynamic>) {
return SearchResults.fromJson(response);
} else if (response is String && response.isNotEmpty) {
final parsed = jsonDecode(response);
if (parsed is Map<String, dynamic>) {
return SearchResults.fromJson(parsed);
}
}
// Fallback for empty results
return SearchResults(users: [], tags: [], posts: []);
} catch (e) {
print('Search error: $e');
// Return empty results on error
return SearchResults(users: [], tags: [], posts: []);
}
}
// ============================================================================
// NOTIFICATIONS
// ============================================================================
/// Get notifications for the current user
/// [limit] Maximum number of notifications to fetch (default: 20)
/// [offset] Number of notifications to skip for pagination (default: 0)
/// [unreadOnly] If true, only fetch unread notifications (default: false)
Future<List<AppNotification>> getNotifications({
int limit = 20,
int offset = 0,
bool unreadOnly = false,
}) async {
final response = await _callFunction(
'notifications',
method: 'GET',
queryParams: {
'limit': limit.toString(),
'offset': offset.toString(),
if (unreadOnly) 'unread_only': 'true',
},
);
if (_isSuccessStatus(response)) {
final data = jsonDecode(response.body);
if (data is List) {
return data.map((json) => AppNotification.fromJson(json)).toList();
}
return [];
} else {
throw Exception(_extractError(response, 'Failed to get notifications'));
}
}
/// Mark specific notifications as read
/// [notificationIds] List of notification IDs to mark as read
Future<void> markNotificationsAsRead(List<String> notificationIds) async {
final response = await _callFunction(
'notifications',
method: 'PATCH',
body: {'notification_ids': notificationIds},
);
if (!_isSuccessStatus(response)) {
throw Exception(_extractError(response, 'Failed to mark notifications as read'));
}
}
/// Mark all notifications as read
Future<void> markAllNotificationsAsRead() async {
final response = await _callFunction(
'notifications',
method: 'PATCH',
body: {'mark_all_read': true},
);
if (!_isSuccessStatus(response)) {
throw Exception(_extractError(response, 'Failed to mark all notifications as read'));
}
}
/// Get the count of unread notifications
Future<int> getUnreadNotificationCount() async {
final userId = _supabase.auth.currentUser?.id;
if (userId == null) {
return 0;
}
try {
final response = await _supabase
.rpc('get_unread_notification_count', params: {'p_user_id': userId});
if (response is int) {
return response;
}
return 0;
} catch (e) {
print('Error getting unread notification count: $e');
return 0;
}
}
}