From 863dfe6a0b9d7378696e66e609cfbc5eb4155252 Mon Sep 17 00:00:00 2001 From: exmKrd Date: Fri, 13 Jun 2025 08:22:17 +0200 Subject: [PATCH] update --- .DS_Store | Bin 10244 -> 10244 bytes nexuschat/lib/chat.dart | 713 +++++++++++++++++++----------- nexuschat/lib/contacts.dart | 152 ++++--- nexuschat/lib/forgotpassword.dart | 108 +++++ nexuschat/lib/login.dart | 50 +++ nexuschat/lib/main.dart | 6 +- nexuschat/lib/menu.dart | 6 + nexuschat/lib/notifications.dart | 327 ++++++++++++++ nexuschat/lib/profil.dart | 9 - nexuschat/pubspec.lock | 16 + nexuschat/pubspec.yaml | 3 + 11 files changed, 1083 insertions(+), 307 deletions(-) create mode 100644 nexuschat/lib/forgotpassword.dart create mode 100644 nexuschat/lib/notifications.dart diff --git a/.DS_Store b/.DS_Store index 5bf01bd2791183c4bfb33f2ad53a309a09346e3a..57e2dbdd56f586ea337e4285eaa6f1473d0cad08 100644 GIT binary patch literal 10244 zcmeHMYitx%6h3FVz>FhwT8iMX#f2*6(ZE(7QWV(kwjva0?Y8v6qs;CMbi(Y+x-;7r zD_Hyq>Q5!2QT(As6JjFZBkCXHD>444XoSQ_Vq!vKq9n#dOpNEwoyET3hlvKv+~nSK z&+9(EJ!fw2T>!vjQLhJx0f0!AQRQ}OrYKC$>ZBqB66S~G57vSDbk@n`Cs~Gq5P=YZ z5P=YZ5P=YZn*{+nvsn>kPQx}tAVeTU;2Hw#`w*kbXd<8^ocf;*YWynz(FlqCLVbmG zh{gh%2q@jY`l_+<`;EI9Vo#e5=E)mcXPPsc^aQVPsW(;m9=+92$$9!|Z1gBve zA`l`l83A^8kH88z048|j?(efVUK*>*IZaI&%gK;T99_7seDv}sjpOzD4fXl{F>eEl z*w7$>4F(KB5nRYZKWN}(2Hkux%R8C8Wx8HG{tXIck!jOsh%-c4+>`Ei2GgGA72Tv( z*u}UG$FVY{zLwc(8iRdGRm!nF&9sez?_lZ%ne6T|ZQU76Iz?M|ecv=!h>|GvDf5Ph z*Tmy>vDK>@4%fwo8}9Y{YY!im#5px9?{DurXyt6@7+X4hV1f@3%;fyF_Bi#X*qQET ztqiS{xlN%S3;q0)>-Sy9${k2?c*@q4;JH|WN4iaxXQ~~YJG;7j`V^(;8tsOqdFFu8 zq$H*Wj?PBYm9rwZU$j6~cQH+>U{I?E4vTA5xx=MxDOzB- zyiS%o4pO8jZZK86URFCTEn`@UF7bKRtd-TS0lq7xB@nK!lGSc{0L#%<+(5Q*ldA4z z({9sTuQhMjw4~31xl^R;b^En~(K<+x8b~*j)c>(a&YYTc({oD77TQG*8?I5;{XJAF z73FH1+Ab=iBK_*7Jz!FH5Y{|N#G1sB7zGnRO=6i(p@tIxAJCdaoby#BaX{ILTn%wZ z!dBP|c{l{e;Td=l&cInX2Oq<#D26Z%WKRQ^%K^($E_yiuqC-EtK8lT0N@D#p`Z{U0QK7N27;-@%@ zU*UQD4u8g9@CsfPBEmFbx==1CLZvWUSS-{EaiKv-3Y&#CzILIMo4w=rBxuA>m3;0% z@YNAcPn}lL%@1wa+OqAYO{)vDcp}ZYV{VLP*SZaj*8-78ZYfyDrhp&!x-Y1C)a%R2!fJDitL^xY5CnQM$pNXcT%_1d~BK}*E{yB`ow{Qu5 zqDcP}%TUC*7{evF0@vY2+(faR!e-oot@toSIE(MSxDPeVqJ=q%@jMoA7$3tU_&6S= zI6qF2&f@$8K8Gjq1$+fx#n^#1=B$kplk literal 10244 zcmeHMOK=oL818=($P5|C06_xo6bpd@QcDOBO~S+4yyPJeU=y+lfn|1QBm=WE%g*d3 z5TYroyzsI-`F>Sdr6nZ?3xwk0K}GbaXcd-vpp-&Az`={k!M~?xV;&0^wS=nfs_wu0 z|NHOm`R4n3s(T0_(2~fmN}Tlg>=)$rT&{ zjsQo1Bft^h2;46S;5VBlv7AX>as)U69Dz{;=>8DKOl35XBTV{R2NwPj0A(qv{e*Re zR#1im84ctJlW4&N%oQbbh2V$*%pLXNpkFkQBTO=P0>R+}f|(&Wp@2U-jvw~B6Noa& zOO60XU@`*q>|RQ0$pK;#Z`}F4{B3=#E$y_mB`qh3Ix&Bt=lrGbFE@|3>-V&0`p4W2 zRNY2bA`zPyWRT>DOH!ntXv9knxS3#M~8gMH{Z(ODTx)}`L+O$G_72ou6DW?AHfy)9KL|B;5z&WKfw*S1%JSwa7UOT%oVDHdBP$gBCHT~ zh724=#vg-Wcp66F890HAKMl{p^YAjf0$;)eevFT(t^^Y=IX-aW^D-OKF4MlpUpyu}gV z2yg^A0vrL3z%kcl0=b^=P0QVG=c%{->V-{QS?)|AhRr`>qG} G{Qnnws`4)Y diff --git a/nexuschat/lib/chat.dart b/nexuschat/lib/chat.dart index e6870ba..835aab3 100644 --- a/nexuschat/lib/chat.dart +++ b/nexuschat/lib/chat.dart @@ -1,10 +1,16 @@ import 'dart:async'; +import 'dart:io' as io; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_giphy_picker/giphy_ui.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:file_picker/file_picker.dart'; +import 'dart:io'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:intl/intl.dart'; class ChatScreen extends StatefulWidget { final String usernameExpediteur; @@ -27,6 +33,7 @@ class _ChatScreenState extends State { bool _isButtonEnabled = false; late Timer _pollingTimer; bool _isInitialLoading = true; + bool _showScrollToBottom = false; @override void initState() { @@ -35,6 +42,19 @@ class _ChatScreenState extends State { _loadExpediteur(); _controller.addListener(_updateButtonState); _startPolling(); + _scrollController.addListener(() { + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.offset; + final threshold = 100.0; + + final shouldShow = (maxScroll - currentScroll) > threshold; + + if (shouldShow != _showScrollToBottom) { + setState(() { + _showScrollToBottom = shouldShow; + }); + } + }); } void _startPolling() { @@ -155,18 +175,26 @@ class _ChatScreenState extends State { Future _dechiffrerMessage(String encryptedMessage, String key) async { final uri = - Uri.parse('https://nexuschat.derickexm.be/messages/uncrypt_message/'); + Uri.parse('https://nexuschat.derickexm.be/messages/uncrypt_messages/'); final response = await http.post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ - 'encrypted_message': encryptedMessage, - 'key': key, + "messages": [ + {"encrypted_message": encryptedMessage, "key": key} + ] }), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); - return data['decrypted_message'] ?? encryptedMessage; + if (data['results'] != null && + data['results'] is List && + data['results'].isNotEmpty) { + final first = data['results'][0]; + return first['decrypted_message'] ?? encryptedMessage; + } else { + return encryptedMessage; + } } else { print("Erreur déchiffrement: ${response.body}"); return encryptedMessage; @@ -184,24 +212,51 @@ class _ChatScreenState extends State { final jsonResponse = jsonDecode(response.body); if (jsonResponse.containsKey('messages')) { final List messagesList = jsonResponse['messages']; - final decryptedMessages = - await Future.wait(messagesList.map((msg) async { - final isMe = msg['expediteur'].toString() == expediteur; - final encrypted = msg['messages'].toString(); - final cle = msg['key']?.toString() ?? ''; - final texte = await _dechiffrerMessage(encrypted, cle); - return { + final batch = messagesList + .map((msg) => { + "encrypted_message": msg['messages'].toString(), + "key": msg['key']?.toString() ?? '' + }) + .toList(); + + final decryptUri = Uri.parse( + 'https://nexuschat.derickexm.be/messages/uncrypt_messages/'); + final decryptResponse = await http.post( + decryptUri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({"messages": batch}), + ); + + List textesDecryptes = []; + + if (decryptResponse.statusCode == 200) { + final decryptedData = jsonDecode(decryptResponse.body); + if (decryptedData['results'] is List) { + textesDecryptes = List.from(decryptedData['results'] + .map((item) => item['decrypted_message'] ?? '')); + } + } + + final List> finalMessages = []; + for (int i = 0; i < messagesList.length; i++) { + final msg = messagesList[i]; + final text = i < textesDecryptes.length + ? textesDecryptes[i] + : msg['messages'].toString(); + finalMessages.add({ 'sender': msg['expediteur'].toString(), - 'text': texte, + 'text': text, 'encrypted': msg['messages'].toString(), - 'timestamp': msg['sent_at'].toString(), + 'sent_at': msg['sent_at'].toString(), 'key': msg['key']?.toString() ?? '', - 'type': msg['type'] ?? 'text' - }; - })); + 'type': msg['type'] ?? 'text', + }); + print(msg['sent_at'].toString()); + } + setState(() { - messages = decryptedMessages; + messages = finalMessages; _isInitialLoading = false; }); _scrollToBottom(); @@ -227,13 +282,38 @@ class _ChatScreenState extends State { }), ); if (response.statusCode == 200) { - print("✅ Message supprimé"); + print(" Message supprimé"); _fetchMessages(); // refresh } else { - print("❌ Erreur suppression: ${response.body}"); + print("Erreur suppression: ${response.body}"); } } catch (e) { - print("❌ Erreur _supprimerMessage: $e"); + print(" Erreur _supprimerMessage: $e"); + } + } + + Future _sendnotification(String messageText) async { + if (destinataire.isEmpty) return; + + final url = + Uri.parse("https://nexuschat.derickexm.be/users/send_notifications"); + final body = jsonEncode({ + 'destinataire': destinataire, + 'type': 'text', + 'contenu': messageText, + 'owner': expediteur + }); + + try { + final response = await http.post(url, + headers: {'Content-Type': 'application/json'}, body: body); + if (response.statusCode == 200) { + print("✅ Notification envoyée."); + } else { + print("❌ Erreur envoi notification: ${response.body}"); + } + } catch (e) { + print("❌ Exception envoi notification: $e"); } } @@ -273,26 +353,31 @@ class _ChatScreenState extends State { print("🔍 expediteur: $expediteur"); if (expediteur == null || message.trim().isEmpty) return; - final now = DateTime.now(); - final cryptoData = await _chiffrerMessage(message.trim()); - print("Résultat chiffrement : $cryptoData"); final encryptedMessage = cryptoData['encrypted_message'] ?? ''; final key = cryptoData['key'] ?? ''; - // Ajout du texte en clair final plainText = _controller.text.trim(); - if (key == null || key.isEmpty) { + + // --- MODIFIED LINE FOR TIMESTAMP --- + // Get current local time, then convert to UTC, then add 2 hours (for CEST / UTC+2) + // This ensures the time sent is what CEST time would be for this moment. + final nowCestTime = DateTime.now().toUtc().add(Duration(hours: 2)); + final nowCestIsoFormatted = + DateFormat("yyyy-MM-dd HH:mm:ss").format(nowCestTime); + + if (key.isEmpty) { print('❌ Clé de chiffrement manquante. Message non envoyé.'); return; } + // Affichage local temporaire (sera remplacé par le polling) setState(() { messages = List>.from(messages) ..add({ 'sender': expediteur ?? '', 'text': plainText, 'encrypted': encryptedMessage, - 'timestamp': now.toIso8601String(), + 'timestamp': 'En cours...', // Temporaire, will be updated by polling 'key': key, 'type': 'text' }); @@ -303,6 +388,8 @@ class _ChatScreenState extends State { print("✉️ Envoi du message chiffré : $encryptedMessage"); print("🔑 Clé : $key"); + print( + "⏰ Timestamp envoyé à l'API (CEST): $nowCestIsoFormatted"); // Add this debug print try { final response = await http.post( @@ -312,74 +399,162 @@ class _ChatScreenState extends State { 'expediteur': expediteur, 'destinataire': destinataire, 'message': encryptedMessage, - // Correction : horodatage en heure locale (Belgique) - 'timestamp': DateTime.now().toIso8601String(), 'id_conversation': idConversation, - // Correction : forcer 'key' non vide - 'key': key.isNotEmpty ? key : 'test' + 'key': key, + 'type': 'text', + 'timestamp': + nowCestIsoFormatted, // --- USE THE CEST FORMATTED STRING --- }), ); + + print("📡 Status Code: ${response.statusCode}"); + print("📡 Response: ${response.body}"); + if (response.statusCode == 200) { + print("✅ Message envoyé avec succès"); + + await Future.delayed(Duration(milliseconds: 500)); + await _fetchMessages(); + final jsonResponse = jsonDecode(response.body); if (jsonResponse.containsKey('reply')) { setState(() { messages.add({ 'sender': destinataire, 'text': jsonResponse['reply'], - 'timestamp': DateTime.now().toIso8601String(), + 'timestamp': 'À l\'instant', + 'type': 'text' }); }); _scrollToBottom(); } + await _sendnotification(message.trim()); + } else { + print('❌ Erreur HTTP ${response.statusCode}: ${response.body}'); + setState(() { + messages.removeLast(); + }); } } catch (e) { - print('Erreur sendMessage: $e'); + print('❌ Erreur exception sendMessage: $e'); + setState(() { + messages.removeLast(); + }); } } +// 🎭 Fonction _sendGif simplifiée Future _sendGif(String gifUrl) async { if (expediteur == null || gifUrl.isEmpty) return; - print('📤 Préparation à l’envoi du GIF :'); - print(' ↪️ URL : $gifUrl'); - final nowUtcIso = DateTime.now().toIso8601String(); - // Désactivation du chiffrement pour les GIFs - final encryptedMessage = gifUrl; - final key = 'test'; - print(' 🔐 Encrypted : $encryptedMessage'); - print(' 🔑 Key : $key'); - print(' 💬 ID conversation : $idConversation'); + print('📤 Préparation à l\'envoi du GIF : $gifUrl'); + final nowCestTime = DateTime.now().toUtc().add(Duration(hours: 2)); + final nowCestIsoFormatted = + DateFormat("yyyy-MM-dd HH:mm:ss").format(nowCestTime); + + final cryptoData = await _chiffrerMessage(gifUrl); + final encryptedMessage = cryptoData['encrypted_message'] ?? gifUrl; + final key = cryptoData['key'] ?? 'test'; + + // Affichage local temporaire setState(() { messages = List>.from(messages) ..add({ 'sender': expediteur ?? '', 'text': gifUrl, 'encrypted': encryptedMessage, - 'timestamp': nowUtcIso, + 'sent_at': 'En cours...', 'key': key, 'type': 'gif' }); }); _scrollToBottom(); + try { + final response = await http.post( + Uri.parse('https://nexuschat.derickexm.be/messages/send_message/'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'expediteur': expediteur, + 'destinataire': destinataire, + 'message': encryptedMessage, + 'id_conversation': idConversation, + 'key': key, + 'type': 'gif', + 'timestamp': nowCestIsoFormatted, // --- INCLUDE THIS IN THE BODY --- + }), + ); + + print("📡 GIF Status: ${response.statusCode}"); + + if (response.statusCode == 200) { + print('✅ GIF envoyé avec succès.'); + // Refresh pour récupérer le vrai timestamp + await Future.delayed(Duration(milliseconds: 500)); + await _fetchMessages(); + } else { + print('❌ Erreur envoi GIF : ${response.body}'); + setState(() { + messages.removeLast(); + }); + } + } catch (e) { + print('❌ Erreur réseau envoi GIF : $e'); + setState(() { + messages.removeLast(); + }); + } + } + + void _scrollToBottom({bool force = false}) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.offset; + final threshold = 100.0; + + if (force || (maxScroll - currentScroll) < threshold) { + _scrollController.animateTo( + maxScroll, + duration: Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + }); + } + + Future _sendFile(String fileUrl) async { + if (expediteur == null || fileUrl.isEmpty) return; + + final nowUtcIso = DateTime.now().toIso8601String(); + final cryptoData = await _chiffrerMessage(fileUrl); + final encryptedMessage = cryptoData['encrypted_message'] ?? fileUrl; + final key = cryptoData['key'] ?? 'test'; + + setState(() { + messages = List>.from(messages) + ..add({ + 'sender': expediteur ?? '', + 'text': fileUrl, + 'encrypted': encryptedMessage, + 'timestamp': nowUtcIso, + 'key': key, + 'type': 'file' + }); + }); + _scrollToBottom(); + final body = jsonEncode({ 'expediteur': expediteur, 'destinataire': destinataire, 'message': encryptedMessage, - 'timestamp': nowUtcIso, + // 'timestamp': nowUtcIso, // removed 'id_conversation': idConversation, 'key': key, - 'type': 'gif' + 'type': 'file' }); - print("📦 Corps envoyé à l’API :"); - print(" expediteur : ${expediteur}"); - print(" destinataire : ${destinataire}"); - print(" message : $encryptedMessage"); - print(" key : $key"); - print(" timestamp : $nowUtcIso"); - print(" id_conversation: $idConversation"); - print(" type : gif"); - print("🔒 Longueur message : ${encryptedMessage.length}"); + try { final response = await http.post( Uri.parse('https://nexuschat.derickexm.be/messages/send_message/'), @@ -388,30 +563,16 @@ class _ChatScreenState extends State { ); if (response.statusCode != 200) { - print('Erreur envoi GIF : ${response.body}'); + print('Erreur envoi fichier : ${response.body}'); } else { - print('✅ GIF envoyé avec succès.'); + print('✅ Fichier envoyé avec succès.'); } } catch (e) { - print('Erreur réseau envoi GIF : $e'); + print('Erreur réseau envoi fichier : $e'); } } - void _scrollToBottom() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - }); - } - - void _selectPhoto() { - print('Sélectionner une photo'); - } + /// Future _selectGif() async { GiphyLocale? fr; @@ -441,198 +602,256 @@ class _ChatScreenState extends State { ), title: Text(destinataire), ), - body: _isInitialLoading - ? const Center(child: CircularProgressIndicator()) - : Column( - children: [ - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: messages.length, - itemBuilder: (context, index) { - final message = messages[index]; - final isMe = message['sender'] == expediteur; - - // Formatage de l'heure - final time = - DateTime.tryParse(message['timestamp'] ?? ''); - final formattedTime = time != null - ? "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}" - : ''; - - return Container( - alignment: - isMe ? Alignment.centerRight : Alignment.centerLeft, - margin: const EdgeInsets.symmetric( - vertical: 4, horizontal: 8), - child: Column( - crossAxisAlignment: isMe - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Text( - message['sender'] ?? '', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.grey[700], - ), - ), - Row( - mainAxisSize: MainAxisSize.min, + body: Stack( + children: [ + _isInitialLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: messages.length, + itemBuilder: (context, index) { + final message = messages[index]; + final isMe = message['sender'] == expediteur; + return Container( + alignment: isMe + ? Alignment.centerRight + : Alignment.centerLeft, + margin: const EdgeInsets.symmetric( + vertical: 4, horizontal: 8), + child: Column( + crossAxisAlignment: isMe + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ - Flexible( - child: GestureDetector( - onLongPress: isMe - ? () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text( - "Supprimer le message ?"), - actions: [ - TextButton( - child: Text("Annuler"), - onPressed: () => - Navigator.of(context) - .pop(), - ), - TextButton( - child: Text("Supprimer"), - onPressed: () { - Navigator.of(context) - .pop(); - _supprimerMessage( - message); - }, - ), - ], + Text( + message['sender'] ?? '', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.grey[700], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: GestureDetector( + onLongPress: isMe + ? () { + showDialog( + context: context, + builder: + (BuildContext context) { + return AlertDialog( + title: Text( + "Supprimer le message ?"), + actions: [ + TextButton( + child: + Text("Annuler"), + onPressed: () => + Navigator.of( + context) + .pop(), + ), + TextButton( + child: + Text("Supprimer"), + onPressed: () { + Navigator.of( + context) + .pop(); + _supprimerMessage( + message); + }, + ), + ], + ); + }, ); - }, - ); - } - : null, - child: Container( - margin: const EdgeInsets.only(top: 2), - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: isMe - ? Colors.orange - : Colors.grey[300], - borderRadius: BorderRadius.circular(10), + } + : null, + child: Container( + margin: const EdgeInsets.only(top: 2), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: isMe + ? Colors.orange + : Colors.grey[300], + borderRadius: + BorderRadius.circular(10), + ), + child: message['type'] == 'gif' + ? Image.network( + message['text'] ?? '', + errorBuilder: (context, error, + stackTrace) { + return Text( + "Erreur de chargement du GIF"); + }) + : message['text'] + .toString() + .startsWith('http') + ? GestureDetector( + onTap: () => launchUrl( + Uri.parse( + message['text'])), + child: Text( + '📎 Fichier joint', + style: TextStyle( + decoration: + TextDecoration + .underline, + color: + Colors.blueAccent, + ), + ), + ) + : Text( + message['owner'] != null + ? "Message de ${message['owner']}" + : (message['text'] ?? + ''), + style: TextStyle( + color: isMe + ? Colors.white + : Colors.black, + ), + ), + ), ), - child: message['type'] == 'gif' - ? Image.network(message['text'] ?? '') - : Text( - message['text'] ?? '', - style: TextStyle( - color: isMe - ? Colors.white - : Colors.black, - ), - ), ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 2), + child: Builder( + builder: (context) { + final rawDateStr = message['sent_at'] ?? + message['timestamp']; + DateTime? parsedDate; + try { + parsedDate = + DateTime.tryParse(rawDateStr ?? ''); + } catch (_) {} + final display = parsedDate != null + ? DateFormat("d MMMM yyyy à HH:mm", + "fr_FR") + .format(parsedDate) + : ''; + return Text( + display, + style: TextStyle( + fontSize: 10, + color: Colors.grey[600]), + ); + }, ), ), ], ), - if (formattedTime.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 2), - child: Text( - formattedTime, - style: TextStyle( - fontSize: 10, color: Colors.grey[600]), - ), - ), - ], - ), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: TextField( - controller: _controller, - focusNode: _focusNode, - style: TextStyle(fontSize: 16), - cursorColor: Colors.orange, - decoration: InputDecoration( - hintText: 'Entrez votre message...', - border: OutlineInputBorder(), - ), - minLines: 1, - maxLines: 5, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - onChanged: (text) => _updateButtonState(), - onEditingComplete: () {}, - inputFormatters: [ - _EnterKeyFormatter( - onEnter: () { - if (_controller.text.trim().isNotEmpty) { - sendMessage(_controller.text); - _controller - .clear(); // 👈 Ajout explicite du clear ici - } - }, + Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + focusNode: _focusNode, + style: TextStyle(fontSize: 16), + cursorColor: Colors.orange, + decoration: InputDecoration( + hintText: 'Entrez votre message...', + border: OutlineInputBorder(), + ), + minLines: 1, + maxLines: 5, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + onChanged: (text) => _updateButtonState(), + onEditingComplete: () {}, + inputFormatters: [ + _EnterKeyFormatter( + onEnter: () { + if (_controller.text + .trim() + .isNotEmpty) { + sendMessage(_controller.text); + _controller.clear(); + } + }, + ), + ], ), - ], + ), + SizedBox(width: 5), + Container( + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: Icon(Icons.gif_box, + color: Colors.deepOrange, size: 28), + onPressed: _selectGif, + tooltip: 'GIF', + ), + ), + SizedBox(width: 5), + Container( + decoration: BoxDecoration( + color: Colors.orange, + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon(Icons.send, color: Colors.black), + onPressed: _isButtonEnabled + ? () { + sendMessage(_controller.text); + _controller.clear(); + _updateButtonState(); + } + : null, + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 4, top: 2), + child: Text( + "🔒 Messages chiffrés de bout en bout", + style: + TextStyle(fontSize: 12, color: Colors.grey), ), ), - SizedBox(width: 8), - Container( - decoration: BoxDecoration( - color: Colors.orange.shade100, // plus doux - borderRadius: BorderRadius.circular(12), - ), - child: IconButton( - icon: Icon(Icons.gif_box, - color: Colors.deepOrange, size: 28), - onPressed: _selectGif, - tooltip: 'GIF', - ), - ), - SizedBox(width: 5), - Container( - decoration: BoxDecoration( - color: Colors.orange, - shape: BoxShape.circle, - ), - child: IconButton( - icon: Icon(Icons.send, color: Colors.black), - onPressed: _isButtonEnabled - ? () { - sendMessage(_controller.text); - _controller.clear(); - _updateButtonState(); - } - : null, - ), - ), - SizedBox(width: 8), ], ), - Padding( - padding: const EdgeInsets.only(bottom: 4, top: 2), - child: Text( - "🔒 Messages chiffrés de bout en bout", - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - ), - ], - ), + ), + ], ), - ], + if (_showScrollToBottom) + Positioned( + bottom: 100, + right: 16, + child: FloatingActionButton( + mini: true, + backgroundColor: Colors.orange, + child: Icon(Icons.arrow_downward), + onPressed: () => _scrollToBottom(force: true), + ), ), + ], + ), ); } } diff --git a/nexuschat/lib/contacts.dart b/nexuschat/lib/contacts.dart index 9544b85..0db6ca4 100644 --- a/nexuschat/lib/contacts.dart +++ b/nexuschat/lib/contacts.dart @@ -106,9 +106,11 @@ class _ContactsState extends State ); if (response.statusCode == 200) { final data = json.decode(response.body); + final users = data['users']; + users.shuffle(); setState(() { - _users = data['users']; - _filteredUsers = _users; + _users = users; + _filteredUsers = users.length > 3 ? users.take(3).toList() : users; }); } } catch (e) { @@ -119,9 +121,13 @@ class _ContactsState extends State void _filterUsers() { final query = _searchController.text.toLowerCase(); setState(() { - _filteredUsers = _users - .where((user) => user['username'].toLowerCase().contains(query)) - .toList(); + if (query.isEmpty) { + _filteredUsers = []; + } else { + _filteredUsers = _users + .where((user) => user['username'].toLowerCase().contains(query)) + .toList(); + } }); } @@ -134,6 +140,32 @@ class _ContactsState extends State }); } + Future _sendnotification( + String destinataire, String messageText) async { + if (destinataire.isEmpty || currentUser == null) return; + + final url = + Uri.parse("https://nexuschat.derickexm.be/users/send_notifications"); + final body = jsonEncode({ + 'destinataire': destinataire, + 'type': 'text', + 'contenu': messageText, + 'owner': currentUser + }); + + try { + final response = await http.post(url, + headers: {'Content-Type': 'application/json'}, body: body); + if (response.statusCode == 200) { + print("✅ Notification envoyée."); + } else { + print("❌ Erreur envoi notification: ${response.body}"); + } + } catch (e) { + print("❌ Exception envoi notification: $e"); + } + } + Future _envoyerDemandeContact(String destinataire) async { if (destinataire == currentUser) { ScaffoldMessenger.of(context).showSnackBar( @@ -173,6 +205,8 @@ class _ContactsState extends State ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Demande envoyée à $destinataire")), ); + await _sendnotification( + destinataire, "$currentUser vous a envoyé une demande de contact."); await _loadDemandes(); } } catch (e) { @@ -380,54 +414,72 @@ class _ContactsState extends State Expanded( child: _filteredUsers.isEmpty ? Center(child: Text("Aucun utilisateur trouvé")) - : ListView.builder( - itemCount: _filteredUsers.length, - itemBuilder: (context, index) { - final user = _filteredUsers[index]['username']; - if (user == currentUser) return SizedBox(); + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_searchController.text.isEmpty) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Text( + "Personnes que tu pourrais connaître", + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: ListView.builder( + itemCount: _filteredUsers.length, + itemBuilder: (context, index) { + final user = _filteredUsers[index]['username']; + if (user == currentUser) return SizedBox(); - return FutureBuilder( - future: _getEtatRelation(user), - builder: (context, snapshot) { - String? etat = snapshot.data; + return FutureBuilder( + future: _getEtatRelation(user), + builder: (context, snapshot) { + String? etat = snapshot.data; - if (!snapshot.hasData) { - return ListTile( - title: Text(user), - subtitle: Text("Chargement..."), + if (!snapshot.hasData) { + return ListTile( + title: Text(user), + subtitle: Text("Chargement..."), + ); + } + + String label = ""; + VoidCallback? action; + + if (etat == "aucune_relation") { + label = "Envoyer demande"; + action = + () => _envoyerDemandeContact(user); + } else if (etat == "pending_envoyee" || + etat == "pending_recue") { + label = "Demande en attente"; + action = null; + } else if (etat == "ami") { + return SizedBox(); // cacher les amis + } else { + label = "Erreur"; + action = null; + } + + return Card( + child: ListTile( + title: Text(user), + leading: Icon(Icons.person_outline), + trailing: ElevatedButton( + onPressed: action, + child: Text(label), + ), + ), + ); + }, ); - } - - String label = ""; - VoidCallback? action; - - if (etat == "aucune_relation") { - label = "Envoyer demande"; - action = () => _envoyerDemandeContact(user); - } else if (etat == "pending_envoyee" || - etat == "pending_recue") { - label = "Demande en attente"; - action = null; - } else if (etat == "ami") { - return SizedBox(); // cacher les amis - } else { - label = "Erreur"; - action = null; - } - - return Card( - child: ListTile( - title: Text(user), - leading: Icon(Icons.person_outline), - trailing: ElevatedButton( - onPressed: action, - child: Text(label), - ), - ), - ); - }, - ); - }, + }, + ), + ), + ], ), ), ], diff --git a/nexuschat/lib/forgotpassword.dart b/nexuschat/lib/forgotpassword.dart new file mode 100644 index 0000000..2c1ac71 --- /dev/null +++ b/nexuschat/lib/forgotpassword.dart @@ -0,0 +1,108 @@ +// ignore_for_file: use_super_parameters + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +class ForgotPasswordScreen extends StatefulWidget { + const ForgotPasswordScreen({Key? key}) : super(key: key); + + @override + State createState() => _ForgotPasswordScreenState(); +} + +class _ForgotPasswordScreenState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + Future _resetPassword(BuildContext context, String email) async { + final url = Uri.parse( + 'https://nexuschat.derickexm.be/email/change_password?email=$email'); + try { + final response = await http.post( + url, + headers: {'Accept': 'application/json'}, + ); + + if (response.statusCode == 200) { + _showErrorDialog(context, "Email de vérification envoyé avec succès!"); + } else { + _showErrorDialog(context, + "Impossible d'envoyer l'email de vérification. (${response.statusCode})"); + print("Réponse serveur : ${response.body}"); + } + } catch (e) { + _showErrorDialog(context, "Erreur de connexion à l'API."); + print("Erreur d'envoi email : $e"); + } + } + + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text(''), + content: Text(message), + actions: [ + TextButton( + child: const Text('Fermer'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('rénitialisation de mot de passe'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email', + prefixIcon: Icon(Icons.email), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Entrer votre email'; + } + if (!value.contains('@')) { + return 'Erreur : entrer un email valide'; + } + return null; + }, + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + _resetPassword(context, _emailController.text); + } + }, + child: const Text('Rénitialiser votre mot de passe'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/nexuschat/lib/login.dart b/nexuschat/lib/login.dart index c7c4d39..f19d51e 100644 --- a/nexuschat/lib/login.dart +++ b/nexuschat/lib/login.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:nexuschat/forgotpassword.dart'; import 'package:nexuschat/inscription.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'menu.dart'; @@ -93,6 +94,42 @@ class Login extends StatelessWidget { } } + Future _ForgotPassword(String email, String password) async { + final url = + Uri.parse('https://nexuschat.derickexm.be/users/check_credentials'); + + final body = jsonEncode({ + 'email': email, + 'password': password, + }); + + try { + final response = await http.post( + url, + headers: { + 'Content-Type': 'application/json', + }, + body: body, + ); + + if (response.statusCode == 200) { + print("Connexion réussie"); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('user_email', email); + + Navigator.pushReplacement( + context, MaterialPageRoute(builder: (context) => Menu())); + await sendAttemptLogin(email); + } else { + _showErrorDialog( + context, "Nom d'utilisateur ou mot de passe incorrect"); + } + } catch (e) { + _showErrorDialog(context, "Impossible de se connecter à l'API"); + } + } + return Scaffold( body: Center( child: Padding( @@ -184,6 +221,19 @@ class Login extends StatelessWidget { child: const Text("S'inscrire"), ), ), + TextButton( + onPressed: () { + // Tu pourras plus tard rediriger vers une page dédiée + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ForgotPasswordScreen())); + }, + child: const Text( + "Mot de passe oublié ?", + style: TextStyle(color: Colors.red), + ), + ), ], ), ), diff --git a/nexuschat/lib/main.dart b/nexuschat/lib/main.dart index 750eda5..8b5775b 100644 --- a/nexuschat/lib/main.dart +++ b/nexuschat/lib/main.dart @@ -4,8 +4,12 @@ import 'package:flutter/material.dart'; import 'package:nexuschat/menu.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'login.dart'; +import 'package:intl/date_symbol_data_local.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await initializeDateFormatting('fr_FR', null); -void main() { runApp(const MyApp()); } diff --git a/nexuschat/lib/menu.dart b/nexuschat/lib/menu.dart index 01a3148..b79a9a7 100644 --- a/nexuschat/lib/menu.dart +++ b/nexuschat/lib/menu.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:nexuschat/notifications.dart'; import 'package:nexuschat/profil.dart'; import 'settings.dart'; import 'listechat.dart'; @@ -24,6 +25,7 @@ class _MenuState extends State { _widgetOptions = [ Listechat(), + Notif(), Setting(), ]; } @@ -112,6 +114,10 @@ class _MenuState extends State { icon: Icon(Icons.chat), label: "Chat", ), + BottomNavigationBarItem( + icon: Icon(Icons.notifications), + label: "Notifications", + ), BottomNavigationBarItem( icon: Icon(Icons.settings), label: "Paramètres", diff --git a/nexuschat/lib/notifications.dart b/nexuschat/lib/notifications.dart new file mode 100644 index 0000000..7c64acd --- /dev/null +++ b/nexuschat/lib/notifications.dart @@ -0,0 +1,327 @@ +import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; +import 'dart:convert'; +import 'package:intl/intl.dart'; +import 'package:http/http.dart' as http; +import 'package:nexuschat/profil.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class Notif extends StatefulWidget { + const Notif({ + Key? key, + }) : super(key: key); + + @override + _NotifState createState() => _NotifState(); +} + +class _NotifState extends State { + String? username; + String? userId; + Map? _lastNotification; + bool isUsernameReady = false; + late SharedPreferences prefs; + int? selectedNotificationId; + + @override + void initState() { + super.initState(); + _initializeNotifications(); + _initPrefs(); + } + + Future _initPrefs() async { + prefs = await SharedPreferences.getInstance(); + final email = prefs.getString('user_email') ?? ''; + + if (email.isNotEmpty) { + await _getUsername(email); + } else { + print("❌ Aucun email trouvé dans les préférences."); + } + } + + Future _deleteNotification(String notificationId) async { + final url = + Uri.parse('https://nexuschat.derickexm.be/users/delete_notifications/'); + + try { + final response = await http.post( + url, + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: {'id': notificationId}, + ); + + if (response.statusCode == 200) { + if (mounted) { + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Notification supprimée')), + ); + } + } else { + print('Erreur lors de la suppression: ${response.body}'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Échec de suppression de la notification')), + ); + } + } catch (e) { + print('Erreur lors de la suppression: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur lors de la suppression')), + ); + } + } + + Future deleteAllNotifications() async { + final url = Uri.parse( + 'https://nexuschat.derickexm.be/users/delete_all_notifications/'); + + try { + var formData = FormData.fromMap({ + 'destinataire': username, + }); + + final response = await Dio().post( + url.toString(), + data: formData, + options: Options( + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + ), + ); + + if (response.statusCode == 200) { + final data = response.data; + print('Successfully deleted ${data['count']} notifications'); + return true; + } else { + print('Failed to delete notifications: ${response.statusCode}'); + return false; + } + } catch (e) { + print('Error when deleting all notifications: $e'); + return false; + } + } + + Future _getUsername(String email) async { + final url = Uri.parse( + 'https://nexuschat.derickexm.be/users/get_username/?email=$email'); + try { + final response = + await http.get(url, headers: {'Accept': 'application/json'}); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (mounted) { + setState(() { + username = data['username']; + prefs.setString('username', username!); + isUsernameReady = true; + }); + } + } else { + print("❌ Impossible de récupérer le nom d'utilisateur"); + } + } catch (e) { + print("❌ Erreur de connexion à l'API : $e"); + } + } + + void _initializeNotifications() {} + + Future>> fetchNotifications( + String? username) async { + if (username == null || username.isEmpty) { + return []; + } + + final response = await http.get(Uri.parse( + 'https://nexuschat.derickexm.be/users/get_notifications?username=$username')); + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + final List notifications = decoded['notifications']; + return notifications.cast>(); + } else { + throw Exception('Erreur de chargement des notifications'); + } + } + + Future _showConfirmationDialog(BuildContext context) async { + return await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Confirmation'), + content: Text( + 'Voulez-vous vraiment supprimer toutes les notifications ?'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: Text('Annuler'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: Text('Supprimer'), + ), + ], + ); + }, + ) ?? + false; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Notifications'), + actions: [ + IconButton( + icon: Icon(Icons.delete_sweep), + tooltip: 'Supprimer toutes les notifications', + onPressed: () async { + bool confirm = await _showConfirmationDialog(context); + if (confirm) { + bool success = await deleteAllNotifications(); + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Toutes les notifications ont été supprimées')), + ); + if (mounted) { + setState(() {}); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Échec de la suppression des notifications')), + ); + } + } + }, + ), + ], + ), + body: isUsernameReady + ? FutureBuilder>>( + future: fetchNotifications(username), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center( + child: Text('Erreur: ${snapshot.error}'), + ); + } + + final notifications = snapshot.data ?? []; + + if (notifications.isEmpty) { + return Center( + child: Text('Aucune notification pour le moment'), + ); + } + + return ListView.builder( + itemCount: notifications.length, + itemBuilder: (context, index) { + final notification = notifications[index]; + final username = notification['owner'] ?? 'inconnu'; + + final message = notification['contenu'] ?? 'Pas de contenu'; + + final timestamp = notification.containsKey('date_creation') + ? DateTime.parse(notification['date_creation']) + : DateTime.now(); + + var formattedTimestamp = + DateFormat('dd MMM yyyy, HH:mm').format(timestamp); + + return Container( + decoration: BoxDecoration(), + margin: + EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + child: ListTile( + trailing: IconButton( + icon: Icon(Icons.delete), + onPressed: () async { + await _deleteNotification( + notification['id'].toString()); + if (mounted) { + setState(() {}); + } + }, + ), + leading: CircleAvatar( + child: Icon( + Icons.notifications, + size: 30, + color: Colors.black, + ), + backgroundColor: Colors.white38, + ), + title: Text( + username ?? '', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message, + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 4.0), + Text( + formattedTimestamp, + style: + TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + onTap: () { + setState(() { + selectedNotificationId = notification['id']; + print( + "Notification sélectionnée : $selectedNotificationId"); + }); + }, + ), + ); + }, + ); + }, + ) + : Center(child: CircularProgressIndicator()), + ); + } + + void _showNotification( + BuildContext context, Map? notificationData) async { + var source = + notificationData != null && notificationData.containsKey('type') + ? notificationData['type'] + : "divers"; + var message = + notificationData != null && notificationData.containsKey('contenu') + ? notificationData['contenu'] + : "Pas de message"; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("🔔 $source : $message"), + duration: Duration(seconds: 3), + ), + ); + } +} diff --git a/nexuschat/lib/profil.dart b/nexuschat/lib/profil.dart index 06dab70..7ed4a56 100644 --- a/nexuschat/lib/profil.dart +++ b/nexuschat/lib/profil.dart @@ -402,15 +402,6 @@ class _ProfilState extends State { child: const Text("Envoyer l'email de vérification"), ), const SizedBox(height: 20), - const Text("Statut", - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), - const SizedBox(height: 3), - Text( - "Dernière activité : Non spécifié", - style: const TextStyle(fontSize: 20), - ), - const SizedBox(height: 10), ElevatedButton( onPressed: () { showDialog( diff --git a/nexuschat/pubspec.lock b/nexuschat/pubspec.lock index 567a6e4..acad71e 100644 --- a/nexuschat/pubspec.lock +++ b/nexuschat/pubspec.lock @@ -97,6 +97,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: diff --git a/nexuschat/pubspec.yaml b/nexuschat/pubspec.yaml index 5131720..a080b88 100644 --- a/nexuschat/pubspec.yaml +++ b/nexuschat/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + flutter_giphy_picker: ^1.0.5 http: ^1.4.0 adaptive_theme: ^3.6.0 @@ -44,6 +45,8 @@ dependencies: file_picker: ^5.0.0 image: ^3.0.1 shared_preferences: ^2.2.2 + dio: ^5.8.0+1 +