Files
sure/mobile/lib/services/chat_service.dart
Tristan the Katana d5bfccc4c7 feat(mobile): add mass delete for chats (#1779)
* feat(mobile): add mass delete for chats
Long-press any chat to enter selection mode, tap items to select/deselect,
use Select All to toggle all, then delete with a single confirmation.
Swipe-to-delete continues to work outside selection mode.

* fix(mobile): address PR review comments on mass delete

- Wrap each deleteChat call in its own try-catch so a single network
  failure doesn't abort the entire Future.wait operation
- Add null-safe casting for deletedCount and failedIds in provider
- Fix misleading error snackbar copy ("Some chats could not be deleted"
  implied partial failure; provider only returns false on total failure)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:13:11 +02:00

406 lines
11 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/chat.dart';
import '../models/message.dart';
import 'api_config.dart';
class ChatService {
/// Get list of chats with pagination
Future<Map<String, dynamic>> getChats({
required String accessToken,
int page = 1,
int perPage = 25,
}) async {
try {
final url = Uri.parse(
'${ApiConfig.baseUrl}/api/v1/chats?page=$page&per_page=$perPage',
);
final response = await http.get(
url,
headers: ApiConfig.getAuthHeaders(accessToken),
).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
final chatsList = (responseData['chats'] as List)
.map((json) => Chat.fromJson(json))
.toList();
return {
'success': true,
'chats': chatsList,
'pagination': responseData['pagination'],
};
} else if (response.statusCode == 401) {
return {
'success': false,
'error': 'unauthorized',
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 403) {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': 'feature_disabled',
'message': responseData['message'] ?? 'AI features not enabled',
};
} else {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': responseData['error'] ?? 'Failed to fetch chats',
};
}
} catch (e) {
return {
'success': false,
'error': 'Network error: ${e.toString()}',
};
}
}
/// Get a specific chat with messages
Future<Map<String, dynamic>> getChat({
required String accessToken,
required String chatId,
int page = 1,
int perPage = 50,
}) async {
try {
final url = Uri.parse(
'${ApiConfig.baseUrl}/api/v1/chats/$chatId?page=$page&per_page=$perPage',
);
final response = await http.get(
url,
headers: ApiConfig.getAuthHeaders(accessToken),
).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
final chat = Chat.fromJson(responseData);
return {
'success': true,
'chat': chat,
};
} else if (response.statusCode == 401) {
return {
'success': false,
'error': 'unauthorized',
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 404) {
return {
'success': false,
'error': 'not_found',
'message': 'Chat not found',
};
} else {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': responseData['error'] ?? 'Failed to fetch chat',
};
}
} catch (e) {
return {
'success': false,
'error': 'Network error: ${e.toString()}',
};
}
}
/// Create a new chat with optional initial message
Future<Map<String, dynamic>> createChat({
required String accessToken,
String? title,
String? initialMessage,
}) async {
try {
final url = Uri.parse('${ApiConfig.baseUrl}/api/v1/chats');
final body = <String, dynamic>{};
if (title != null) {
body['title'] = title;
}
if (initialMessage != null) {
body['message'] = initialMessage;
}
final response = await http.post(
url,
headers: {
...ApiConfig.getAuthHeaders(accessToken),
'Content-Type': 'application/json',
},
body: jsonEncode(body),
).timeout(const Duration(seconds: 30));
if (response.statusCode == 201) {
final responseData = jsonDecode(response.body);
final chat = Chat.fromJson(responseData);
return {
'success': true,
'chat': chat,
};
} else if (response.statusCode == 401) {
return {
'success': false,
'error': 'unauthorized',
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 403) {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': 'feature_disabled',
'message': responseData['message'] ?? 'AI features not enabled',
};
} else {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': responseData['error'] ?? 'Failed to create chat',
};
}
} catch (e) {
return {
'success': false,
'error': 'Network error: ${e.toString()}',
};
}
}
/// Send a message to a chat
Future<Map<String, dynamic>> sendMessage({
required String accessToken,
required String chatId,
required String content,
}) async {
try {
final url = Uri.parse('${ApiConfig.baseUrl}/api/v1/chats/$chatId/messages');
final response = await http.post(
url,
headers: {
...ApiConfig.getAuthHeaders(accessToken),
'Content-Type': 'application/json',
},
body: jsonEncode({
'content': content,
}),
).timeout(const Duration(seconds: 30));
if (response.statusCode == 201) {
final responseData = jsonDecode(response.body);
final message = Message.fromJson(responseData);
return {
'success': true,
'message': message,
};
} else if (response.statusCode == 401) {
return {
'success': false,
'error': 'unauthorized',
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 404) {
return {
'success': false,
'error': 'not_found',
'message': 'Chat not found',
};
} else {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': responseData['error'] ?? 'Failed to send message',
};
}
} catch (e) {
return {
'success': false,
'error': 'Network error: ${e.toString()}',
};
}
}
/// Update chat title
Future<Map<String, dynamic>> updateChat({
required String accessToken,
required String chatId,
required String title,
}) async {
try {
final url = Uri.parse('${ApiConfig.baseUrl}/api/v1/chats/$chatId');
final response = await http.patch(
url,
headers: {
...ApiConfig.getAuthHeaders(accessToken),
'Content-Type': 'application/json',
},
body: jsonEncode({
'title': title,
}),
).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
final chat = Chat.fromJson(responseData);
return {
'success': true,
'chat': chat,
};
} else if (response.statusCode == 401) {
return {
'success': false,
'error': 'unauthorized',
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 404) {
return {
'success': false,
'error': 'not_found',
'message': 'Chat not found',
};
} else {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': responseData['error'] ?? 'Failed to update chat',
};
}
} catch (e) {
return {
'success': false,
'error': 'Network error: ${e.toString()}',
};
}
}
/// Delete a chat
Future<Map<String, dynamic>> deleteChat({
required String accessToken,
required String chatId,
}) async {
try {
final url = Uri.parse('${ApiConfig.baseUrl}/api/v1/chats/$chatId');
final response = await http.delete(
url,
headers: ApiConfig.getAuthHeaders(accessToken),
).timeout(const Duration(seconds: 30));
if (response.statusCode == 204) {
return {
'success': true,
};
} else if (response.statusCode == 401) {
return {
'success': false,
'error': 'unauthorized',
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 404) {
return {
'success': false,
'error': 'not_found',
'message': 'Chat not found',
};
} else {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': responseData['error'] ?? 'Failed to delete chat',
};
}
} catch (e) {
return {
'success': false,
'error': 'Network error: ${e.toString()}',
};
}
}
/// Delete multiple chats in parallel
Future<Map<String, dynamic>> deleteMultipleChats({
required String accessToken,
required List<String> chatIds,
}) async {
final results = await Future.wait(
chatIds.map((id) async {
try {
return await deleteChat(accessToken: accessToken, chatId: id);
} catch (_) {
return {'success': false};
}
}),
eagerError: false,
);
final failedIds = chatIds
.asMap()
.entries
.where((e) => results[e.key]['success'] != true)
.map((e) => e.value)
.toList();
return {
'success': failedIds.isEmpty,
'deletedCount': chatIds.length - failedIds.length,
'failedIds': failedIds,
};
}
/// Retry the last assistant response in a chat
Future<Map<String, dynamic>> retryMessage({
required String accessToken,
required String chatId,
}) async {
try {
final url = Uri.parse('${ApiConfig.baseUrl}/api/v1/chats/$chatId/messages/retry');
final response = await http.post(
url,
headers: ApiConfig.getAuthHeaders(accessToken),
).timeout(const Duration(seconds: 30));
if (response.statusCode == 202) {
return {
'success': true,
};
} else if (response.statusCode == 401) {
return {
'success': false,
'error': 'unauthorized',
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 404) {
return {
'success': false,
'error': 'not_found',
'message': 'Chat not found',
};
} else {
final responseData = jsonDecode(response.body);
return {
'success': false,
'error': responseData['error'] ?? 'Failed to retry message',
};
}
} catch (e) {
return {
'success': false,
'error': 'Network error: ${e.toString()}',
};
}
}
}