- Ajout système complet de gestion des secteurs avec contours géographiques - Import des contours départementaux depuis GeoJSON - API REST pour la gestion des secteurs (/api/sectors) - Service de géolocalisation pour déterminer les secteurs - Migration base de données avec tables x_departements_contours et sectors_adresses - Interface Flutter pour visualisation et gestion des secteurs - Ajout thème sombre dans l'application - Corrections diverses et optimisations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
330 lines
9.3 KiB
Dart
Executable File
330 lines
9.3 KiB
Dart
Executable File
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:mqtt5_client/mqtt5_client.dart';
|
|
import 'package:mqtt5_client/mqtt5_server_client.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
|
|
/// Service de gestion des notifications chat via MQTT
|
|
///
|
|
/// Utilise MQTT pour recevoir des notifications en temps réel
|
|
/// et afficher des notifications locales
|
|
|
|
class MqttNotificationService {
|
|
static final MqttNotificationService _instance =
|
|
MqttNotificationService._internal();
|
|
factory MqttNotificationService() => _instance;
|
|
MqttNotificationService._internal();
|
|
|
|
late MqttServerClient _client;
|
|
final FlutterLocalNotificationsPlugin _localNotifications =
|
|
FlutterLocalNotificationsPlugin();
|
|
|
|
// Configuration
|
|
final String mqttHost;
|
|
final int mqttPort;
|
|
final String mqttUsername;
|
|
final String mqttPassword;
|
|
final String clientId;
|
|
|
|
// État
|
|
bool _initialized = false;
|
|
String? _userId;
|
|
StreamSubscription? _messageSubscription;
|
|
|
|
// Callbacks
|
|
Function(String messageId)? onMessageTap;
|
|
Function(Map<String, dynamic>)? onNotificationReceived;
|
|
|
|
MqttNotificationService({
|
|
this.mqttHost = 'mqtt.geosector.fr',
|
|
this.mqttPort = 1883,
|
|
this.mqttUsername = '',
|
|
this.mqttPassword = '',
|
|
String? clientId,
|
|
}) : clientId = clientId ??
|
|
'geosector_chat_${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
/// Initialise le service de notifications
|
|
Future<void> initialize({required String userId}) async {
|
|
if (_initialized) return;
|
|
|
|
_userId = userId;
|
|
|
|
// Initialiser les notifications locales
|
|
await _initializeLocalNotifications();
|
|
|
|
// Initialiser le client MQTT
|
|
await _initializeMqttClient();
|
|
|
|
_initialized = true;
|
|
}
|
|
|
|
/// Initialise le client MQTT
|
|
Future<void> _initializeMqttClient() async {
|
|
try {
|
|
_client = MqttServerClient.withPort(mqttHost, clientId, mqttPort);
|
|
|
|
_client.logging(on: kDebugMode);
|
|
_client.keepAlivePeriod = 60;
|
|
_client.onConnected = _onConnected;
|
|
_client.onDisconnected = _onDisconnected;
|
|
_client.onSubscribed = _onSubscribed;
|
|
_client.autoReconnect = true;
|
|
|
|
// Configurer les options de connexion
|
|
final connMessage = MqttConnectMessage()
|
|
.authenticateAs(mqttUsername, mqttPassword)
|
|
.withClientIdentifier(clientId)
|
|
.startClean()
|
|
.keepAliveFor(60);
|
|
|
|
_client.connectionMessage = connMessage;
|
|
|
|
// Se connecter
|
|
await _connect();
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de l\'initialisation MQTT : $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Se connecte au broker MQTT
|
|
Future<void> _connect() async {
|
|
try {
|
|
await _client.connect();
|
|
} catch (e) {
|
|
debugPrint('Erreur de connexion MQTT : $e');
|
|
_client.disconnect();
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Callback lors de la connexion
|
|
void _onConnected() {
|
|
debugPrint('Connecté au broker MQTT');
|
|
|
|
// S'abonner aux topics de l'utilisateur
|
|
if (_userId != null) {
|
|
_subscribeToUserTopics(_userId!);
|
|
}
|
|
|
|
// Écouter les messages
|
|
_messageSubscription = _client.updates.listen(_onMessageReceived);
|
|
}
|
|
|
|
/// Callback lors de la déconnexion
|
|
void _onDisconnected() {
|
|
debugPrint('Déconnecté du broker MQTT');
|
|
|
|
// Tenter une reconnexion
|
|
if (_client.autoReconnect) {
|
|
Future.delayed(const Duration(seconds: 5), () {
|
|
_connect();
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Callback lors de l'abonnement
|
|
void _onSubscribed(MqttSubscription subscription) {
|
|
debugPrint('Abonné au topic : ${subscription.topic.rawTopic}');
|
|
}
|
|
|
|
/// S'abonner aux topics de l'utilisateur
|
|
void _subscribeToUserTopics(String userId) {
|
|
// Topic pour les messages personnels
|
|
_client.subscribe('chat/user/$userId/messages', MqttQos.atLeastOnce);
|
|
|
|
// Topic pour les annonces
|
|
_client.subscribe('chat/announcement', MqttQos.atLeastOnce);
|
|
|
|
// Topic pour les groupes de l'utilisateur (si disponibles)
|
|
_client.subscribe('chat/user/$userId/groups/+', MqttQos.atLeastOnce);
|
|
}
|
|
|
|
/// Gère les messages reçus
|
|
void _onMessageReceived(List<MqttReceivedMessage<MqttMessage>> messages) {
|
|
for (var message in messages) {
|
|
final topic = message.topic;
|
|
final payload = message.payload as MqttPublishMessage;
|
|
final messageText =
|
|
MqttUtilities.bytesToStringAsString(payload.payload.message!);
|
|
|
|
try {
|
|
final data = jsonDecode(messageText) as Map<String, dynamic>;
|
|
_handleNotification(topic, data);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du décodage du message : $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Traite la notification reçue
|
|
Future<void> _handleNotification(
|
|
String topic, Map<String, dynamic> data) async {
|
|
// Vérifier les paramètres de notification de l'utilisateur
|
|
if (!await _shouldShowNotification(data)) {
|
|
return;
|
|
}
|
|
|
|
String title = '';
|
|
String body = '';
|
|
String messageId = '';
|
|
String conversationId = '';
|
|
|
|
if (topic.startsWith('chat/user/')) {
|
|
// Message personnel
|
|
title = data['senderName'] ?? 'Nouveau message';
|
|
body = data['content'] ?? '';
|
|
messageId = data['messageId'] ?? '';
|
|
conversationId = data['conversationId'] ?? '';
|
|
} else if (topic.startsWith('chat/announcement')) {
|
|
// Annonce
|
|
title = data['title'] ?? 'Annonce';
|
|
body = data['content'] ?? '';
|
|
messageId = data['messageId'] ?? '';
|
|
conversationId = data['conversationId'] ?? '';
|
|
}
|
|
|
|
// Afficher la notification locale
|
|
await _showLocalNotification(
|
|
title: title,
|
|
body: body,
|
|
payload: jsonEncode({
|
|
'messageId': messageId,
|
|
'conversationId': conversationId,
|
|
}),
|
|
);
|
|
|
|
// Appeler le callback si défini
|
|
onNotificationReceived?.call(data);
|
|
}
|
|
|
|
/// Vérifie si la notification doit être affichée
|
|
Future<bool> _shouldShowNotification(Map<String, dynamic> data) async {
|
|
// TODO: Vérifier les paramètres de notification de l'utilisateur
|
|
// - Notifications désactivées
|
|
// - Conversation en silencieux
|
|
// - Mode Ne pas déranger
|
|
return true;
|
|
}
|
|
|
|
/// Initialise les notifications locales
|
|
Future<void> _initializeLocalNotifications() async {
|
|
const androidSettings =
|
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
const iosSettings = DarwinInitializationSettings(
|
|
requestAlertPermission: true,
|
|
requestBadgePermission: true,
|
|
requestSoundPermission: true,
|
|
);
|
|
|
|
const initSettings = InitializationSettings(
|
|
android: androidSettings,
|
|
iOS: iosSettings,
|
|
);
|
|
|
|
await _localNotifications.initialize(
|
|
initSettings,
|
|
onDidReceiveNotificationResponse: _onNotificationTap,
|
|
);
|
|
}
|
|
|
|
/// Affiche une notification locale
|
|
Future<void> _showLocalNotification({
|
|
required String title,
|
|
required String body,
|
|
required String payload,
|
|
}) async {
|
|
const androidDetails = AndroidNotificationDetails(
|
|
'chat_messages',
|
|
'Messages de chat',
|
|
channelDescription: 'Notifications pour les nouveaux messages de chat',
|
|
importance: Importance.high,
|
|
priority: Priority.high,
|
|
icon: '@mipmap/ic_launcher',
|
|
);
|
|
|
|
const iosDetails = DarwinNotificationDetails(
|
|
presentAlert: true,
|
|
presentBadge: true,
|
|
presentSound: true,
|
|
);
|
|
|
|
const notificationDetails = NotificationDetails(
|
|
android: androidDetails,
|
|
iOS: iosDetails,
|
|
);
|
|
|
|
await _localNotifications.show(
|
|
DateTime.now().microsecondsSinceEpoch,
|
|
title,
|
|
body,
|
|
notificationDetails,
|
|
payload: payload,
|
|
);
|
|
}
|
|
|
|
/// Handler pour le clic sur une notification
|
|
void _onNotificationTap(NotificationResponse response) {
|
|
final payload = response.payload;
|
|
if (payload != null) {
|
|
try {
|
|
final data = jsonDecode(payload) as Map<String, dynamic>;
|
|
final messageId = data['messageId'] as String?;
|
|
if (messageId != null) {
|
|
onMessageTap?.call(messageId);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du traitement du clic sur notification : $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Publie un message MQTT
|
|
Future<void> publishMessage(
|
|
String topic, Map<String, dynamic> message) async {
|
|
if (_client.connectionStatus?.state != MqttConnectionState.connected) {
|
|
await _connect();
|
|
}
|
|
|
|
final messagePayload = jsonEncode(message);
|
|
final builder = MqttPayloadBuilder();
|
|
builder.addString(messagePayload);
|
|
|
|
_client.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
|
}
|
|
|
|
/// S'abonner à une conversation spécifique
|
|
Future<void> subscribeToConversation(String conversationId) async {
|
|
if (_client.connectionStatus?.state == MqttConnectionState.connected) {
|
|
_client.subscribe(
|
|
'chat/conversation/$conversationId', MqttQos.atLeastOnce);
|
|
}
|
|
}
|
|
|
|
/// Se désabonner d'une conversation
|
|
Future<void> unsubscribeFromConversation(String conversationId) async {
|
|
if (_client.connectionStatus?.state == MqttConnectionState.connected) {
|
|
_client.unsubscribeStringTopic('chat/conversation/$conversationId');
|
|
}
|
|
}
|
|
|
|
/// Désactive temporairement les notifications
|
|
void pauseNotifications() {
|
|
_client.pause();
|
|
}
|
|
|
|
/// Réactive les notifications
|
|
void resumeNotifications() {
|
|
_client.resume();
|
|
}
|
|
|
|
/// Libère les ressources
|
|
void dispose() {
|
|
_messageSubscription?.cancel();
|
|
_client.disconnect();
|
|
_initialized = false;
|
|
}
|
|
}
|