2026-04-10 18:50:08 +08:00
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
|
|
|
|
|
|
import '../../../../core/logging/logger.dart';
|
|
|
|
|
import '../../../../core/network/api_problem.dart';
|
|
|
|
|
import '../../../../data/network/api_client.dart';
|
|
|
|
|
import '../models/notification_item.dart';
|
|
|
|
|
import '../models/notification_list_result.dart';
|
|
|
|
|
|
|
|
|
|
class NotificationApi {
|
|
|
|
|
NotificationApi({required ApiClient apiClient}) : _apiClient = apiClient;
|
|
|
|
|
|
|
|
|
|
final ApiClient _apiClient;
|
|
|
|
|
final Logger _logger = getLogger('features.notifications.data.apis');
|
|
|
|
|
|
|
|
|
|
Future<NotificationListResult> listNotifications({
|
|
|
|
|
int limit = 20,
|
|
|
|
|
String? cursor,
|
|
|
|
|
}) async {
|
|
|
|
|
final queryParts = <String>['limit=$limit'];
|
|
|
|
|
if (cursor != null) {
|
|
|
|
|
queryParts.add('cursor=$cursor');
|
|
|
|
|
}
|
|
|
|
|
final path = '/api/v1/notifications?${queryParts.join("&")}';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final json = await _apiClient.getJson(path);
|
|
|
|
|
final itemsJson = json['items'] as List<dynamic>? ?? [];
|
|
|
|
|
final items = itemsJson
|
|
|
|
|
.map((e) => parseNotificationItem(e as Map<String, dynamic>))
|
|
|
|
|
.toList();
|
|
|
|
|
return NotificationListResult(
|
|
|
|
|
items: items,
|
|
|
|
|
nextCursor: json['nextCursor'] as String?,
|
|
|
|
|
hasMore: json['hasMore'] as bool? ?? false,
|
|
|
|
|
);
|
|
|
|
|
} on DioException catch (error, stackTrace) {
|
|
|
|
|
_logger.error(
|
|
|
|
|
message: 'List notifications failed',
|
|
|
|
|
error: error,
|
|
|
|
|
stackTrace: stackTrace,
|
|
|
|
|
);
|
|
|
|
|
throw _mapProblem(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<int> getUnreadCount() async {
|
|
|
|
|
try {
|
|
|
|
|
final json = await _apiClient.getJson(
|
|
|
|
|
'/api/v1/notifications/unread-count',
|
|
|
|
|
);
|
|
|
|
|
return json['count'] as int? ?? 0;
|
|
|
|
|
} on DioException catch (error, stackTrace) {
|
|
|
|
|
_logger.error(
|
|
|
|
|
message: 'Get unread count failed',
|
|
|
|
|
error: error,
|
|
|
|
|
stackTrace: stackTrace,
|
|
|
|
|
);
|
|
|
|
|
throw _mapProblem(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<NotificationItem> markRead({required String notificationId}) async {
|
2026-04-13 14:52:22 +08:00
|
|
|
_logger.info(
|
|
|
|
|
message: 'Mark read request started',
|
|
|
|
|
extra: {'notification_id': notificationId},
|
|
|
|
|
);
|
2026-04-10 18:50:08 +08:00
|
|
|
try {
|
|
|
|
|
final response = await _apiClient.rawDio.patch<Map<String, dynamic>>(
|
|
|
|
|
'/api/v1/notifications/$notificationId/read',
|
|
|
|
|
);
|
2026-04-13 14:52:22 +08:00
|
|
|
final item = parseNotificationItem(response.data!);
|
|
|
|
|
_logger.info(
|
|
|
|
|
message: 'Mark read request succeeded',
|
|
|
|
|
extra: {'notification_id': notificationId, 'is_read': item.isRead},
|
|
|
|
|
);
|
|
|
|
|
return item;
|
2026-04-10 18:50:08 +08:00
|
|
|
} on DioException catch (error, stackTrace) {
|
|
|
|
|
_logger.error(
|
|
|
|
|
message: 'Mark read failed',
|
|
|
|
|
error: error,
|
|
|
|
|
stackTrace: stackTrace,
|
|
|
|
|
);
|
|
|
|
|
throw _mapProblem(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<int> markAllRead() async {
|
2026-04-13 14:52:22 +08:00
|
|
|
_logger.info(message: 'Mark all read request started');
|
2026-04-10 18:50:08 +08:00
|
|
|
try {
|
|
|
|
|
final response = await _apiClient.rawDio.patch<Map<String, dynamic>>(
|
|
|
|
|
'/api/v1/notifications/mark-all-read',
|
|
|
|
|
);
|
2026-04-13 14:52:22 +08:00
|
|
|
final updatedCount = response.data?['updatedCount'] as int? ?? 0;
|
|
|
|
|
_logger.info(
|
|
|
|
|
message: 'Mark all read request succeeded',
|
|
|
|
|
extra: {'updated_count': updatedCount},
|
|
|
|
|
);
|
|
|
|
|
return updatedCount;
|
2026-04-10 18:50:08 +08:00
|
|
|
} on DioException catch (error, stackTrace) {
|
|
|
|
|
_logger.error(
|
|
|
|
|
message: 'Mark all read failed',
|
|
|
|
|
error: error,
|
|
|
|
|
stackTrace: stackTrace,
|
|
|
|
|
);
|
|
|
|
|
throw _mapProblem(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ApiProblem _mapProblem(DioException error) {
|
|
|
|
|
final status = error.response?.statusCode ?? 500;
|
|
|
|
|
final data = error.response?.data;
|
|
|
|
|
|
|
|
|
|
if (data is Map<String, dynamic>) {
|
|
|
|
|
return ApiProblem(
|
|
|
|
|
status: status,
|
|
|
|
|
title: (data['title'] as String?) ?? 'Request failed',
|
|
|
|
|
detail: (data['detail'] as String?) ?? '',
|
|
|
|
|
code: data['code'] as String?,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ApiProblem(
|
|
|
|
|
status: status,
|
|
|
|
|
title: 'Network error',
|
|
|
|
|
detail: error.message ?? 'Request failed',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|