From 1348a30ecab1ea19eb6e62d8ea67c5a82822c148 Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Mon, 30 Dec 2024 15:28:07 +0530 Subject: [PATCH 1/3] done with change --- lib/services/communication_service.dart | 0 macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- .../MainActivity.kt | 0 pubspec.yaml | 1 + 4 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 lib/services/communication_service.dart rename {android/app/src/main/kotlin/com/nankai/openpeerchat_flutter => openpeerchat_flutter}/MainActivity.kt (100%) diff --git a/lib/services/communication_service.dart b/lib/services/communication_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 14cd431..9d4b458 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,7 @@ import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation import shared_preferences_foundation -import sqflite +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) diff --git a/android/app/src/main/kotlin/com/nankai/openpeerchat_flutter/MainActivity.kt b/openpeerchat_flutter/MainActivity.kt similarity index 100% rename from android/app/src/main/kotlin/com/nankai/openpeerchat_flutter/MainActivity.kt rename to openpeerchat_flutter/MainActivity.kt diff --git a/pubspec.yaml b/pubspec.yaml index b3040f7..70a4a2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: open_filex: ^4.5.0 permission_handler: ^11.3.1 path_provider: ^2.1.4 + web_socket_channel: ^3.0.1 dev_dependencies: flutter_lints: From 1ed7a2a3ab247769f78a0cd7c1577c043454e36c Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Mon, 30 Dec 2024 15:44:53 +0530 Subject: [PATCH 2/3] done --- lib/classes/global.dart | 2 + lib/pages/chat_page.dart | 147 +++++++++++++----------- lib/pages/profile.dart | 4 +- lib/services/communication_service.dart | 32 ++++++ openpeerchat_flutter/MainActivity.kt | 7 -- 5 files changed, 115 insertions(+), 77 deletions(-) delete mode 100644 openpeerchat_flutter/MainActivity.kt diff --git a/lib/classes/global.dart b/lib/classes/global.dart index 6af0a7b..fb4d39f 100644 --- a/lib/classes/global.dart +++ b/lib/classes/global.dart @@ -28,6 +28,8 @@ class Global extends ChangeNotifier { static Map cache = {}; static final GlobalKey scaffoldKey = GlobalKey(); + static var profileNameStream; + void sentToConversations(Msg msg, String converser, {bool addToTable = true}) { diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 33a2518..d2883ad 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -13,26 +13,31 @@ import '../components/view_file.dart'; import '../encyption/rsa.dart'; class ChatPage extends StatefulWidget { - const ChatPage({Key? key, required this.converser}) : super(key: key); + String converser; + ChatPage({Key? key, required this.converser}) : super(key: key); + - final String converser; @override - ChatPageState createState() => ChatPageState(); + _ChatPageState createState() => _ChatPageState(); } -class ChatPageState extends State { +class _ChatPageState extends State { List messageList = []; TextEditingController myController = TextEditingController(); @override void initState() { super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); + // Adding listener to listen for profile name updates + Global.profileNameStream.listen((updatedName) { + setState(() { + if (widget.converser == Global.myName) { + // Update the converser name if it matches the updated profile name + widget.converser = updatedName; + } + }); + }); } final ScrollController _scrollController = ScrollController(); @@ -74,72 +79,78 @@ class ChatPageState extends State { Expanded( child: messageList.isEmpty ? const Center( - child: Text('No messages yet'), - ) + child: Text('No messages yet'), + ) : ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(8), - itemCount: groupedMessages.keys.length, - itemBuilder: (BuildContext context, int index) { - String date = groupedMessages.keys.elementAt(index); - return Column( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - date, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ...groupedMessages[date]!.map((msg) { - String displayMessage = msg.message; - if (Global.myPrivateKey != null) { - RSAPrivateKey privateKey = Global.myPrivateKey!; - dynamic data = jsonDecode(msg.message); - if (data['type'] == 'text') { - Uint8List encryptedBytes = base64Decode(data['data']); - Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); - displayMessage = utf8.decode(decryptedBytes); - } - } + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: groupedMessages.keys.length, + itemBuilder: (BuildContext context, int index) { + String date = groupedMessages.keys.elementAt(index); return Column( - crossAxisAlignment: msg.msgtype == 'sent' ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - Align( - alignment: msg.msgtype == 'sent' ? Alignment.centerRight : Alignment.centerLeft, - child: Bubble( - padding: const BubbleEdges.all(12), - margin: const BubbleEdges.only(top: 10), - //add shadow - style: BubbleStyle( - elevation: 3, - shadowColor: Colors.black.withOpacity(0.5), - ), - // nip: msg.msgtype == 'sent' ? BubbleNip.rightTop : BubbleNip.leftTop, - radius: const Radius.circular(10), - color: msg.msgtype == 'sent' ? const Color(0xffd1c4e9) : const Color(0xff80DEEA), - child: msg.message.contains('file') ? _buildFileBubble(msg) : Text( - displayMessage, - style: const TextStyle(color: Colors.black87), + Center( + child: Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + date, + style: const TextStyle(fontWeight: FontWeight.bold), ), ), ), - Padding( - padding: const EdgeInsets.only(top: 2, bottom: 10), - child: Text( - dateFormatter(timeStamp: msg.timestamp), - style: const TextStyle(color: Colors.black54, fontSize: 10), - ), - ), + ...groupedMessages[date]!.map((msg) { + String displayMessage = msg.message; + if (Global.myPrivateKey != null) { + RSAPrivateKey privateKey = Global.myPrivateKey!; + dynamic data = jsonDecode(msg.message); + if (data['type'] == 'text') { + Uint8List encryptedBytes = base64Decode(data['data']); + Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); + displayMessage = utf8.decode(decryptedBytes); + } + } + return Column( + crossAxisAlignment: msg.msgtype == 'sent' + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Align( + alignment: msg.msgtype == 'sent' + ? Alignment.centerRight + : Alignment.centerLeft, + child: Bubble( + padding: const BubbleEdges.all(12), + margin: const BubbleEdges.only(top: 10), + style: BubbleStyle( + elevation: 3, + shadowColor: Colors.black.withOpacity(0.5), + ), + radius: const Radius.circular(10), + color: msg.msgtype == 'sent' + ? const Color(0xffd1c4e9) + : const Color(0xff80DEEA), + child: msg.message.contains('file') + ? _buildFileBubble(msg) + : Text( + displayMessage, + style: const TextStyle(color: Colors.black87), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 10), + child: Text( + dateFormatter(timeStamp: msg.timestamp), + style: const TextStyle(color: Colors.black54, fontSize: 10), + ), + ), + ], + ); + }), ], ); - }), - ], - ); - }, - ), + }, + ), ), MessagePanel(converser: widget.converser), ], @@ -161,7 +172,6 @@ class ChatPageState extends State { color: Colors.black87, ), overflow: TextOverflow.visible, - ), ), IconButton( @@ -180,3 +190,4 @@ String dateFormatter({required String timeStamp}) { String formattedTime = DateFormat('hh:mm aa').format(dateTime); return formattedTime; } + diff --git a/lib/pages/profile.dart b/lib/pages/profile.dart index 6e344e7..1dac65c 100644 --- a/lib/pages/profile.dart +++ b/lib/pages/profile.dart @@ -4,7 +4,7 @@ import 'home_screen.dart'; import 'package:nanoid/nanoid.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../classes/global.dart'; - +import '../services/communication_service.dart'; class Profile extends StatefulWidget { final bool onLogin; @@ -113,7 +113,7 @@ class _ProfileState extends State { // saving the name and id to shared preferences prefs.setString('p_name', myName.text); prefs.setString('p_id', customLengthId); - + CommunicationService.broadcastProfileUpdate(customLengthId, myName.text); // On pressing, move to the home screen navigateToHomeScreen(); }, diff --git a/lib/services/communication_service.dart b/lib/services/communication_service.dart index e69de29..3681a17 100644 --- a/lib/services/communication_service.dart +++ b/lib/services/communication_service.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +// ignore: depend_on_referenced_packages +import 'package:web_socket_channel/web_socket_channel.dart'; + +class CommunicationService { + static final WebSocketChannel _channel = + WebSocketChannel.connect(Uri.parse('ws://your-websocket-server-url')); + + /// Broadcasts a profile update to all connected peers + static void broadcastProfileUpdate(String userId, String newName) { + final message = { + 'type': 'profile_update', + 'userId': userId, + 'newName': newName, + }; + + _channel.sink.add(jsonEncode(message)); + } + + /// Listens for incoming messages + static void listen(void Function(Map) onMessage) { + _channel.stream.listen((data) { + final decodedData = jsonDecode(data); + onMessage(decodedData); + }); + } + + /// Closes the WebSocket connection + static void closeConnection() { + _channel.sink.close(); + } +} diff --git a/openpeerchat_flutter/MainActivity.kt b/openpeerchat_flutter/MainActivity.kt deleted file mode 100644 index d9d9e99..0000000 --- a/openpeerchat_flutter/MainActivity.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.nankai.openpeerchat_flutter - -import io.flutter.embedding.android.FlutterFragmentActivity - -class MainActivity: FlutterFragmentActivity() { - // ... -} \ No newline at end of file From c669a236a9c69f22165bf8f66a6ae6d4bb9aec77 Mon Sep 17 00:00:00 2001 From: dikshantbhangala Date: Fri, 3 Jan 2025 17:15:20 +0530 Subject: [PATCH 3/3] Added audio recording feature and refactored code --- lib/components/message_panel.dart | 135 +++++++++++++----- lib/encyption/rsa.dart | 69 ++++----- lib/pages/chat_page.dart | 227 +++++++++++++++--------------- pubspec.yaml | 1 + 4 files changed, 250 insertions(+), 182 deletions(-) diff --git a/lib/components/message_panel.dart b/lib/components/message_panel.dart index c5db219..637efbc 100644 --- a/lib/components/message_panel.dart +++ b/lib/components/message_panel.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_sound/flutter_sound.dart'; // ADDED import 'package:flutter/services.dart'; import 'package:nanoid/nanoid.dart'; import 'package:pointycastle/asymmetric/api.dart'; @@ -13,10 +14,6 @@ import '../database/database_helper.dart'; import '../encyption/rsa.dart'; import 'view_file.dart'; -/// This component is used in the ChatPage. -/// It is the message bar where the message is typed on and sent to -/// connected devices. - class MessagePanel extends StatefulWidget { const MessagePanel({Key? key, required this.converser}) : super(key: key); final String converser; @@ -28,19 +25,41 @@ class MessagePanel extends StatefulWidget { class _MessagePanelState extends State { TextEditingController myController = TextEditingController(); File _selectedFile = File(''); + FlutterSoundRecorder? _recorder; // ADDED + bool _isRecording = false; // ADDED + String? _recordedFilePath; // ADDED + + @override + void initState() { + super.initState(); + _recorder = FlutterSoundRecorder(); // ADDED + _initializeRecorder(); // ADDED + } + + @override + void dispose() { + _recorder?.closeRecorder(); // ADDED + _recorder = null; // ADDED + super.dispose(); + } + + // ADDED: Initialize audio recorder + Future _initializeRecorder() async { + await _recorder?.openRecorder(); + await _recorder?.setSubscriptionDuration(const Duration(milliseconds: 100)); + } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( - //multiline text field maxLines: null, controller: myController, decoration: InputDecoration( icon: const Icon(Icons.person), hintText: 'Send Message?', - labelText: 'Send Message ', + labelText: 'Send Message', suffixIcon: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, @@ -49,6 +68,13 @@ class _MessagePanelState extends State { onPressed: () => _navigateToFilePreviewPage(context), icon: const Icon(Icons.attach_file), ), + IconButton( + onPressed: _toggleRecording, // ADDED + icon: Icon( + _isRecording ? Icons.mic_off : Icons.mic, // ADDED + color: _isRecording ? Colors.red : null, // ADDED + ), + ), IconButton( onPressed: () => _sendMessage(context), icon: const Icon( @@ -67,7 +93,6 @@ class _MessagePanelState extends State { if (myController.text.isEmpty) { return; } - // Encode the message to base64 String data = jsonEncode({ "sender": Global.myName, @@ -95,7 +120,6 @@ class _MessagePanelState extends State { ); RSAPublicKey publicKey = Global.myPublicKey!; - // Encrypt the message Uint8List encryptedMessage = rsaEncrypt( publicKey, Uint8List.fromList(utf8.encode(myController.text))); @@ -110,21 +134,76 @@ class _MessagePanelState extends State { widget.converser, ); - // refreshMessages(); myController.clear(); } - /// This function is used to navigate to the file preview page and check the file size. + // ADDED: Start and stop audio recording + Future _toggleRecording() async { + if (_isRecording) { + final path = await _recorder?.stopRecorder(); + setState(() { + _isRecording = false; + _recordedFilePath = path; + }); + if (path != null) { + _sendAudioMessage(context, path); // Send the recorded audio + } + } else { + await _recorder?.startRecorder( + codec: Codec.aacMP4, + toFile: 'audio_${DateTime.now().millisecondsSinceEpoch}.m4a', + ); + setState(() { + _isRecording = true; + }); + } + } + + // ADDED: Send recorded audio as a message + void _sendAudioMessage(BuildContext context, String filePath) { + var msgId = nanoid(21); + String fileName = filePath.split('/').last; + + String data = jsonEncode({ + "sender": Global.myName, + "type": "audio", + "fileName": fileName, + "filePath": filePath, + }); + + String date = DateTime.now().toUtc().toString(); + Global.cache[msgId] = Payload( + msgId, + Global.myName, + widget.converser, + data, + date, + ); + insertIntoMessageTable( + Payload( + msgId, + Global.myName, + widget.converser, + data, + date, + ), + ); + + Provider.of(context, listen: false).sentToConversations( + Msg(data, "sent", date, msgId), + widget.converser, + ); + } + + // Existing file picker and sender logic void _navigateToFilePreviewPage(BuildContext context) async { - //max size of file is 30 MB double sizeKbs = 0; const int maxSizeKbs = 30 * 1024; FilePickerResult? result = await FilePicker.platform.pickFiles(); - if(result != null) { + if (result != null) { sizeKbs = result.files.single.size / 1024; } - if (sizeKbs > maxSizeKbs) { if (!context.mounted) return; showDialog( @@ -136,10 +215,8 @@ class _MessagePanelState extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - //file size in MB title: Text('File Size: ${(sizeKbs / 1024).ceil()} MB'), - subtitle: const Text( - 'File size should not exceed 30 MB'), + subtitle: const Text('File size should not exceed 30 MB'), ), ], ), @@ -157,7 +234,6 @@ class _MessagePanelState extends State { return; } -//this function is used to open the file preview dialog if (result != null) { setState(() { _selectedFile = File(result.files.single.path!); @@ -172,12 +248,11 @@ class _MessagePanelState extends State { mainAxisSize: MainAxisSize.min, children: [ ListTile( - - title: Text('File Name: ${_selectedFile.path - .split('/') - .last}', overflow: TextOverflow.ellipsis,), - subtitle: Text( - 'File Size: ${(sizeKbs / 1024).floor()} MB'), + title: Text( + 'File Name: ${_selectedFile.path.split('/').last}', + overflow: TextOverflow.ellipsis, + ), + subtitle: Text('File Size: ${(sizeKbs / 1024).floor()} MB'), ), ElevatedButton( onPressed: () => FilePreview.openFile(_selectedFile.path), @@ -186,7 +261,6 @@ class _MessagePanelState extends State { ], ), actions: [ - TextButton( onPressed: () { Navigator.of(context).pop(); @@ -195,10 +269,9 @@ class _MessagePanelState extends State { ), IconButton( onPressed: () { - Navigator.pop(context); - _sendFileMessage(context, _selectedFile); - - }, + Navigator.pop(context); + _sendFileMessage(context, _selectedFile); + }, icon: const Icon( Icons.send, ), @@ -210,9 +283,7 @@ class _MessagePanelState extends State { } } - -/// This function is used to send the file message. - void _sendFileMessage(BuildContext context, File file) async{ + void _sendFileMessage(BuildContext context, File file) async { var msgId = nanoid(21); String fileName = _selectedFile.path.split('/').last; @@ -247,7 +318,5 @@ class _MessagePanelState extends State { Msg(data, "sent", date, msgId), widget.converser, ); - } - } diff --git a/lib/encyption/rsa.dart b/lib/encyption/rsa.dart index 042b2f3..82ef802 100644 --- a/lib/encyption/rsa.dart +++ b/lib/encyption/rsa.dart @@ -1,19 +1,17 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:asn1lib/asn1lib.dart'; -import 'package:pointycastle/src/platform_check/platform_check.dart'; -import "package:pointycastle/export.dart"; +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/random/fortuna_random.dart'; +import 'dart:math'; -AsymmetricKeyPair generateRSAkeyPair( - SecureRandom secureRandom, - {int bitLength = 2048}) { +AsymmetricKeyPair generateRSAkeyPair(SecureRandom secureRandom, {int bitLength = 2048}) { final keyGen = RSAKeyGenerator() ..init(ParametersWithRandom( RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), secureRandom)); final pair = keyGen.generateKeyPair(); - final myPublic = pair.publicKey as RSAPublicKey; final myPrivate = pair.privateKey as RSAPrivateKey; @@ -21,53 +19,49 @@ AsymmetricKeyPair generateRSAkeyPair( } SecureRandom exampleSecureRandom() { - final secureRandom = FortunaRandom() - ..seed(KeyParameter( - Platform.instance.platformEntropySource().getBytes(32))); + final secureRandom = FortunaRandom(); + final seedSource = Random.secure(); + final seed = List.generate(32, (_) => seedSource.nextInt(256)); + secureRandom.seed(KeyParameter(Uint8List.fromList(seed))); return secureRandom; } - Uint8List rsaEncrypt(RSAPublicKey myPublic, Uint8List dataToEncrypt) { final encryptor = OAEPEncoding(RSAEngine()) - ..init(true, PublicKeyParameter(myPublic)); // true=encrypt + ..init(true, PublicKeyParameter(myPublic)); return _processInBlocks(encryptor, dataToEncrypt); } Uint8List rsaDecrypt(RSAPrivateKey myPrivate, Uint8List cipherText) { final decryptor = OAEPEncoding(RSAEngine()) - ..init(false, PrivateKeyParameter(myPrivate)); // false=decrypt + ..init(false, PrivateKeyParameter(myPrivate)); return _processInBlocks(decryptor, cipherText); } - Uint8List _processInBlocks(AsymmetricBlockCipher engine, Uint8List input) { - final numBlocks = input.length ~/ engine.inputBlockSize + - ((input.length % engine.inputBlockSize != 0) ? 1 : 0); - - final output = Uint8List(numBlocks * engine.outputBlockSize); + final inputBlockSize = engine.inputBlockSize; + final outputBlockSize = engine.outputBlockSize; + final numBlocks = (input.length / inputBlockSize).ceil(); + final output = Uint8List(numBlocks * outputBlockSize); var inputOffset = 0; var outputOffset = 0; + while (inputOffset < input.length) { - final chunkSize = (inputOffset + engine.inputBlockSize <= input.length) - ? engine.inputBlockSize + final chunkSize = (input.length - inputOffset > inputBlockSize) + ? inputBlockSize : input.length - inputOffset; outputOffset += engine.processBlock( input, inputOffset, chunkSize, output, outputOffset); - inputOffset += chunkSize; } - return (output.length == outputOffset) - ? output - : output.sublist(0, outputOffset); + return output.sublist(0, outputOffset); } - String encodePrivateKeyToPem(RSAPrivateKey privateKey) { final topLevel = ASN1Sequence(); topLevel.add(ASN1Integer(BigInt.from(0))); @@ -93,29 +87,26 @@ String encodePublicKeyToPem(RSAPublicKey publicKey) { return "-----BEGIN PUBLIC KEY-----\r\n$dataBase64\r\n-----END PUBLIC KEY-----"; } -//parsePrivateKeyFromPem RSAPrivateKey parsePrivateKeyFromPem(String pem) { - final data = pem.split(RegExp(r'\r?\n')); - final raw = base64.decode(data.sublist(1, data.length - 1).join('')); + final data = pem.split(RegExp(r'\r?\n')).where((line) => !line.contains('-----')).join(''); + final raw = base64.decode(data); final topLevel = ASN1Sequence.fromBytes(raw); - final n = topLevel.elements[1] as ASN1Integer; - final d = topLevel.elements[3] as ASN1Integer; - final p = topLevel.elements[4] as ASN1Integer; - final q = topLevel.elements[5] as ASN1Integer; + final n = (topLevel.elements[1] as ASN1Integer).valueAsBigInteger; + final d = (topLevel.elements[3] as ASN1Integer).valueAsBigInteger; + final p = (topLevel.elements[4] as ASN1Integer).valueAsBigInteger; + final q = (topLevel.elements[5] as ASN1Integer).valueAsBigInteger; - return RSAPrivateKey( - n.valueAsBigInteger, d.valueAsBigInteger, p.valueAsBigInteger, q.valueAsBigInteger); + return RSAPrivateKey(n, d, p, q); } RSAPublicKey parsePublicKeyFromPem(String pem) { - final data = pem.split(RegExp(r'\r?\n')); - final raw = base64.decode(data.sublist(1, data.length - 1).join('')); + final data = pem.split(RegExp(r'\r?\n')).where((line) => !line.contains('-----')).join(''); + final raw = base64.decode(data); final topLevel = ASN1Sequence.fromBytes(raw); - final modulus = topLevel.elements[0] as ASN1Integer; - final exponent = topLevel.elements[1] as ASN1Integer; + final modulus = (topLevel.elements[0] as ASN1Integer).valueAsBigInteger; + final exponent = (topLevel.elements[1] as ASN1Integer).valueAsBigInteger; - return RSAPublicKey(modulus.valueAsBigInteger, exponent.valueAsBigInteger); + return RSAPublicKey(modulus, exponent); } - diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index d2883ad..9eb4600 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -1,5 +1,4 @@ import 'dart:typed_data'; - import 'package:bubble/bubble.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -10,65 +9,53 @@ import '../classes/global.dart'; import 'dart:convert'; import 'package:pointycastle/asymmetric/api.dart'; import '../components/view_file.dart'; -import '../encyption/rsa.dart'; +import '../encryption/rsa.dart'; class ChatPage extends StatefulWidget { String converser; - ChatPage({Key? key, required this.converser}) : super(key: key); - - + ChatPage({Key? key, required this.converser}) : super(key: key); @override _ChatPageState createState() => _ChatPageState(); } class _ChatPageState extends State { + final ScrollController _scrollController = ScrollController(); List messageList = []; - TextEditingController myController = TextEditingController(); @override void initState() { super.initState(); - // Adding listener to listen for profile name updates + _subscribeToProfileUpdates(); + } + + void _subscribeToProfileUpdates() { Global.profileNameStream.listen((updatedName) { - setState(() { - if (widget.converser == Global.myName) { - // Update the converser name if it matches the updated profile name + if (widget.converser == Global.myName) { + setState(() { widget.converser = updatedName; - } - }); + }); + } }); } - final ScrollController _scrollController = ScrollController(); - - @override - Widget build(BuildContext context) { - if (Provider.of(context).conversations[widget.converser] != null) { - messageList = []; - Provider.of(context) - .conversations[widget.converser]! - .forEach((key, value) { - messageList.add(value); - }); - + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( - _scrollController.position.maxScrollExtent + 50, + _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } - } + }); + } - Map> groupedMessages = {}; - for (var msg in messageList) { - String date = DateFormat('dd/MM/yyyy').format(DateTime.parse(msg.timestamp)); - if (groupedMessages[date] == null) { - groupedMessages[date] = []; - } - groupedMessages[date]!.add(msg); - } + @override + Widget build(BuildContext context) { + messageList = _getMessageList(context); + + Map> groupedMessages = _groupMessagesByDate(messageList); return Scaffold( appBar: AppBar( @@ -78,77 +65,14 @@ class _ChatPageState extends State { children: [ Expanded( child: messageList.isEmpty - ? const Center( - child: Text('No messages yet'), - ) + ? const Center(child: Text('No messages yet')) : ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(8), itemCount: groupedMessages.keys.length, - itemBuilder: (BuildContext context, int index) { + itemBuilder: (context, index) { String date = groupedMessages.keys.elementAt(index); - return Column( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - date, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ...groupedMessages[date]!.map((msg) { - String displayMessage = msg.message; - if (Global.myPrivateKey != null) { - RSAPrivateKey privateKey = Global.myPrivateKey!; - dynamic data = jsonDecode(msg.message); - if (data['type'] == 'text') { - Uint8List encryptedBytes = base64Decode(data['data']); - Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); - displayMessage = utf8.decode(decryptedBytes); - } - } - return Column( - crossAxisAlignment: msg.msgtype == 'sent' - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Align( - alignment: msg.msgtype == 'sent' - ? Alignment.centerRight - : Alignment.centerLeft, - child: Bubble( - padding: const BubbleEdges.all(12), - margin: const BubbleEdges.only(top: 10), - style: BubbleStyle( - elevation: 3, - shadowColor: Colors.black.withOpacity(0.5), - ), - radius: const Radius.circular(10), - color: msg.msgtype == 'sent' - ? const Color(0xffd1c4e9) - : const Color(0xff80DEEA), - child: msg.message.contains('file') - ? _buildFileBubble(msg) - : Text( - displayMessage, - style: const TextStyle(color: Colors.black87), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 2, bottom: 10), - child: Text( - dateFormatter(timeStamp: msg.timestamp), - style: const TextStyle(color: Colors.black54, fontSize: 10), - ), - ), - ], - ); - }), - ], - ); + return _buildMessageGroup(date, groupedMessages[date]!); }, ), ), @@ -158,6 +82,83 @@ class _ChatPageState extends State { ); } + List _getMessageList(BuildContext context) { + var conversation = Provider.of(context).conversations[widget.converser]; + if (conversation == null) return []; + return conversation.values.toList(); + } + + Map> _groupMessagesByDate(List messages) { + Map> groupedMessages = {}; + for (var msg in messages) { + String date = DateFormat('dd/MM/yyyy').format(DateTime.parse(msg.timestamp)); + groupedMessages.putIfAbsent(date, () => []).add(msg); + } + return groupedMessages; + } + + Widget _buildMessageGroup(String date, List messages) { + return Column( + children: [ + _buildDateHeader(date), + ...messages.map((msg) => _buildMessageBubble(msg)), + ], + ); + } + + Widget _buildDateHeader(String date) { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + date, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ); + } + + Widget _buildMessageBubble(Msg msg) { + String displayMessage = msg.message; + if (msg.msgtype == 'text' && Global.myPrivateKey != null) { + displayMessage = _decryptMessage(msg.message); + } + return Column( + crossAxisAlignment: msg.msgtype == 'sent' + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Align( + alignment: msg.msgtype == 'sent' + ? Alignment.centerRight + : Alignment.centerLeft, + child: Bubble( + padding: const BubbleEdges.all(12), + margin: const BubbleEdges.only(top: 10), + style: BubbleStyle( + elevation: 3, + shadowColor: Colors.black.withOpacity(0.5), + ), + radius: const Radius.circular(10), + color: msg.msgtype == 'sent' + ? const Color(0xffd1c4e9) + : const Color(0xff80DEEA), + child: msg.message.contains('file') + ? _buildFileBubble(msg) + : Text(displayMessage, style: const TextStyle(color: Colors.black87)), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 10), + child: Text( + dateFormatter(timeStamp: msg.timestamp), + style: const TextStyle(color: Colors.black54, fontSize: 10), + ), + ), + ], + ); + } + Widget _buildFileBubble(Msg msg) { dynamic data = jsonDecode(msg.message); String fileName = data['fileName']; @@ -168,26 +169,32 @@ class _ChatPageState extends State { Flexible( child: Text( fileName, - style: const TextStyle( - color: Colors.black87, - ), - overflow: TextOverflow.visible, + style: const TextStyle(color: Colors.black87), + overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.file_open, color: Colors.black87), - onPressed: () { - FilePreview.openFile(filePath); - }, + onPressed: () => FilePreview.openFile(filePath), ), ], ); } + + String _decryptMessage(String message) { + try { + RSAPrivateKey privateKey = Global.myPrivateKey!; + dynamic data = jsonDecode(message); + Uint8List encryptedBytes = base64Decode(data['data']); + Uint8List decryptedBytes = rsaDecrypt(privateKey, encryptedBytes); + return utf8.decode(decryptedBytes); + } catch (e) { + return "[Error decrypting message]"; + } + } } String dateFormatter({required String timeStamp}) { DateTime dateTime = DateTime.parse(timeStamp); - String formattedTime = DateFormat('hh:mm aa').format(dateTime); - return formattedTime; + return DateFormat('hh:mm aa').format(dateTime); } - diff --git a/pubspec.yaml b/pubspec.yaml index 70a4a2f..b67840c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: permission_handler: ^11.3.1 path_provider: ^2.1.4 web_socket_channel: ^3.0.1 + flutter_sound: ^9.6.0 dev_dependencies: flutter_lints: