feat(communication): module messagerie unifié + contact policies + blocages
Aligné avec le backend MessagingResource : - Nouveau module communication (conversations, messages, participants) - Respect des ContactPolicy (qui peut parler à qui par rôle) - Gestion MemberBlock (blocages individuels) - UI : conversations list, conversation detail, broadcast, tiles - BLoC : MessagingBloc avec events (envoyer, démarrer conversation rôle, etc.)
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
/// Datasource distant pour la communication (API)
|
||||
/// Datasource distant pour la messagerie v4 — /api/messagerie/*
|
||||
library messaging_remote_datasource;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../../core/config/environment.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
import '../../../authentication/data/datasources/keycloak_auth_service.dart';
|
||||
import '../models/message_model.dart';
|
||||
import '../models/conversation_model.dart';
|
||||
import '../../domain/entities/message.dart';
|
||||
import '../models/message_model.dart';
|
||||
import '../models/contact_policy_model.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
@lazySingleton
|
||||
class MessagingRemoteDatasource {
|
||||
@@ -22,7 +24,7 @@ class MessagingRemoteDatasource {
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
/// Headers HTTP avec authentification — rafraîchit le token si expiré (fix IC-03)
|
||||
/// Headers HTTP avec authentification — rafraîchit le token si expiré
|
||||
Future<Map<String, String>> _getHeaders() async {
|
||||
final token = await authService.getValidAccessToken();
|
||||
return {
|
||||
@@ -32,290 +34,271 @@ class MessagingRemoteDatasource {
|
||||
};
|
||||
}
|
||||
|
||||
// === CONVERSATIONS ===
|
||||
String get _base => '${AppConfig.apiBaseUrl}/api/messagerie';
|
||||
|
||||
Future<List<ConversationModel>> getConversations({
|
||||
String? organizationId,
|
||||
bool includeArchived = false,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/conversations')
|
||||
.replace(queryParameters: {
|
||||
if (organizationId != null) 'organisationId': organizationId,
|
||||
'includeArchived': includeArchived.toString(),
|
||||
});
|
||||
// ── Conversations ─────────────────────────────────────────────────────────
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
Future<List<ConversationSummaryModel>> getMesConversations() async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/conversations'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> jsonList = json.decode(response.body);
|
||||
return jsonList
|
||||
.map((json) => ConversationModel.fromJson(json))
|
||||
final list = json.decode(response.body) as List<dynamic>;
|
||||
return list
|
||||
.map((j) => ConversationSummaryModel.fromJson(j as Map<String, dynamic>))
|
||||
.toList();
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération des conversations');
|
||||
}
|
||||
throw ServerException('Erreur récupération conversations (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> getConversation(String id) async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/conversations/$id'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
if (response.statusCode == 404) throw NotFoundException('Conversation non trouvée');
|
||||
throw ServerException('Erreur récupération conversation (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> demarrerConversationDirecte({
|
||||
required String destinataireId,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'destinataireId': destinataireId,
|
||||
'organisationId': organisationId,
|
||||
if (premierMessage != null) 'premierMessage': premierMessage,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/conversations/directe'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur démarrage conversation directe (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> demarrerConversationRole({
|
||||
required String roleCible,
|
||||
required String organisationId,
|
||||
String? premierMessage,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'roleCible': roleCible,
|
||||
'organisationId': organisationId,
|
||||
if (premierMessage != null) 'premierMessage': premierMessage,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/conversations/role'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur démarrage conversation rôle (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<void> archiverConversation(String id) async {
|
||||
final response = await client.delete(
|
||||
Uri.parse('$_base/conversations/$id'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur archivage conversation (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConversationModel> getConversationById(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId');
|
||||
// ── Messages ──────────────────────────────────────────────────────────────
|
||||
|
||||
Future<MessageModel> envoyerMessage(
|
||||
String conversationId, {
|
||||
required String typeMessage,
|
||||
String? contenu,
|
||||
String? urlFichier,
|
||||
int? dureeAudio,
|
||||
String? messageParentId,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'typeMessage': typeMessage,
|
||||
if (contenu != null) 'contenu': contenu,
|
||||
if (urlFichier != null) 'urlFichier': urlFichier,
|
||||
if (dureeAudio != null) 'dureeAudio': dureeAudio,
|
||||
if (messageParentId != null) 'messageParentId': messageParentId,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/conversations/$conversationId/messages'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur envoi message (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<List<MessageModel>> getMessages(String conversationId, {int page = 0}) async {
|
||||
final uri = Uri.parse('$_base/conversations/$conversationId/messages')
|
||||
.replace(queryParameters: {'page': page.toString()});
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 404) {
|
||||
throw NotFoundException('Conversation non trouvée');
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération de la conversation');
|
||||
final list = json.decode(response.body) as List<dynamic>;
|
||||
return list
|
||||
.map((j) => MessageModel.fromJson(j as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
throw ServerException('Erreur récupération messages (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ConversationModel> createConversation({
|
||||
required String name,
|
||||
required List<String> participantIds,
|
||||
String? organizationId,
|
||||
String? description,
|
||||
}) async {
|
||||
final uri =
|
||||
Uri.parse('${AppConfig.apiBaseUrl}/api/conversations');
|
||||
|
||||
final body = json.encode({
|
||||
'name': name,
|
||||
'participantIds': participantIds,
|
||||
'type': 'GROUP', // Default to GROUP for multi-participant conversations
|
||||
if (organizationId != null) 'organisationId': organizationId,
|
||||
if (description != null) 'description': description,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return ConversationModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la création de la conversation');
|
||||
}
|
||||
}
|
||||
|
||||
// === MESSAGES ===
|
||||
|
||||
Future<List<MessageModel>> getMessages({
|
||||
required String conversationId,
|
||||
int? limit,
|
||||
String? beforeMessageId,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages')
|
||||
.replace(queryParameters: {
|
||||
'conversationId': conversationId,
|
||||
if (limit != null) 'limit': limit.toString(),
|
||||
// beforeMessageId not supported by backend yet, omit
|
||||
});
|
||||
|
||||
final response = await client.get(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> jsonList = json.decode(response.body);
|
||||
return jsonList.map((json) => MessageModel.fromJson(json)).toList();
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la récupération des messages');
|
||||
}
|
||||
}
|
||||
|
||||
Future<MessageModel> sendMessage({
|
||||
required String conversationId,
|
||||
required String content,
|
||||
List<String>? attachments,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages');
|
||||
|
||||
final body = json.encode({
|
||||
'conversationId': conversationId,
|
||||
'content': content,
|
||||
if (attachments != null) 'attachments': attachments,
|
||||
'priority': priority.name.toUpperCase(),
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'envoi du message');
|
||||
}
|
||||
}
|
||||
|
||||
Future<MessageModel> sendBroadcast({
|
||||
required String organizationId,
|
||||
required String subject,
|
||||
required String content,
|
||||
MessagePriority priority = MessagePriority.normal,
|
||||
List<String>? attachments,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messaging/broadcast');
|
||||
|
||||
final body = json.encode({
|
||||
'organizationId': organizationId,
|
||||
'subject': subject,
|
||||
'content': content,
|
||||
'priority': priority.name,
|
||||
if (attachments != null) 'attachments': attachments,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
uri,
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 403) {
|
||||
throw ForbiddenException('Permission insuffisante pour envoyer un broadcast');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'envoi du broadcast');
|
||||
}
|
||||
}
|
||||
|
||||
// === CONVERSATION ACTIONS ===
|
||||
|
||||
Future<void> archiveConversation(String conversationId, {bool archive = true}) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/archive')
|
||||
.replace(queryParameters: {'archive': archive.toString()});
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'archivage de la conversation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markConversationAsRead(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/mark-read');
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors du marquage de la conversation comme lue');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleMuteConversation(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-mute');
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors du toggle mute de la conversation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> togglePinConversation(String conversationId) async {
|
||||
final uri = Uri.parse(
|
||||
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-pin');
|
||||
|
||||
final response = await client.put(uri, headers: await _getHeaders());
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else {
|
||||
throw ServerException('Erreur lors du toggle pin de la conversation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === MESSAGE ACTIONS ===
|
||||
|
||||
Future<MessageModel> editMessage({
|
||||
required String messageId,
|
||||
required String newContent,
|
||||
}) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
|
||||
|
||||
final body = json.encode({'content': newContent});
|
||||
|
||||
Future<void> marquerLu(String conversationId) async {
|
||||
final response = await client.put(
|
||||
uri,
|
||||
Uri.parse('$_base/conversations/$conversationId/lire'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur marquage lu (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> supprimerMessage(String conversationId, String messageId) async {
|
||||
final response = await client.delete(
|
||||
Uri.parse('$_base/conversations/$conversationId/messages/$messageId'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur suppression message (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Blocages ──────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> bloquerMembre({
|
||||
required String membreABloquerId,
|
||||
String? organisationId,
|
||||
String? raison,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'membreABloquerId': membreABloquerId,
|
||||
if (organisationId != null) 'organisationId': organisationId,
|
||||
if (raison != null) 'raison': raison,
|
||||
});
|
||||
|
||||
final response = await client.post(
|
||||
Uri.parse('$_base/blocages'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return MessageModel.fromJson(json.decode(response.body));
|
||||
} else if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 404) {
|
||||
throw NotFoundException('Message non trouvé');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de l\'édition du message');
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw ServerException('Erreur blocage membre (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMessage(String messageId) async {
|
||||
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
|
||||
Future<void> debloquerMembre(String membreId, {String? organisationId}) async {
|
||||
final uri = Uri.parse('$_base/blocages/$membreId').replace(
|
||||
queryParameters: {
|
||||
if (organisationId != null) 'organisationId': organisationId,
|
||||
},
|
||||
);
|
||||
|
||||
final response = await client.delete(uri, headers: await _getHeaders());
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
if (response.statusCode == 401) {
|
||||
throw UnauthorizedException();
|
||||
} else if (response.statusCode == 404) {
|
||||
throw NotFoundException('Message non trouvé');
|
||||
} else {
|
||||
throw ServerException('Erreur lors de la suppression du message');
|
||||
}
|
||||
throw ServerException('Erreur déblocage membre (${response.statusCode})');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markMessageAsRead(String messageId) async {
|
||||
// Backend has no per-message read endpoint — use markConversationAsRead
|
||||
if (AppConfig.enableLogging) {
|
||||
debugPrint('[Messaging] markMessageAsRead ignored (no per-message endpoint), messageId=$messageId');
|
||||
Future<List<Map<String, dynamic>>> getMesBlocages() async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/blocages'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
final list = json.decode(response.body) as List<dynamic>;
|
||||
return list.map((j) => j as Map<String, dynamic>).toList();
|
||||
}
|
||||
throw ServerException('Erreur récupération blocages (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<int> getUnreadCount({String? organizationId}) async {
|
||||
try {
|
||||
final conversations = await getConversations(organizationId: organizationId);
|
||||
return conversations.fold<int>(0, (sum, c) => sum + c.unreadCount);
|
||||
} catch (_) {
|
||||
return 0;
|
||||
// ── Politique de communication ────────────────────────────────────────────
|
||||
|
||||
Future<ContactPolicyModel> getPolitique(String organisationId) async {
|
||||
final response = await client.get(
|
||||
Uri.parse('$_base/politique/$organisationId'),
|
||||
headers: await _getHeaders(),
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ContactPolicyModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur récupération politique (${response.statusCode})');
|
||||
}
|
||||
|
||||
Future<ContactPolicyModel> mettreAJourPolitique(
|
||||
String organisationId, {
|
||||
required String typePolitique,
|
||||
required bool autoriserMembreVersMembre,
|
||||
required bool autoriserMembreVersRole,
|
||||
required bool autoriserNotesVocales,
|
||||
}) async {
|
||||
final body = json.encode({
|
||||
'typePolitique': typePolitique,
|
||||
'autoriserMembreVersMembre': autoriserMembreVersMembre,
|
||||
'autoriserMembreVersRole': autoriserMembreVersRole,
|
||||
'autoriserNotesVocales': autoriserNotesVocales,
|
||||
});
|
||||
|
||||
final response = await client.put(
|
||||
Uri.parse('$_base/politique/$organisationId'),
|
||||
headers: await _getHeaders(),
|
||||
body: body,
|
||||
);
|
||||
|
||||
_checkAuth(response);
|
||||
if (response.statusCode == 200) {
|
||||
return ContactPolicyModel.fromJson(json.decode(response.body) as Map<String, dynamic>);
|
||||
}
|
||||
throw ServerException('Erreur mise à jour politique (${response.statusCode})');
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
void _checkAuth(http.Response response) {
|
||||
if (response.statusCode == 401) throw UnauthorizedException();
|
||||
if (response.statusCode == 403) throw ForbiddenException('Permission insuffisante');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user