diff --git a/android/app/src/main/kotlin/com/nankai/openpeerchat_flutter/MainActivity.kt b/android/app/src/main/kotlin/com/nankai/openpeerchat_flutter/MainActivity.kt deleted file mode 100644 index d9d9e99..0000000 --- a/android/app/src/main/kotlin/com/nankai/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 diff --git a/android/build.gradle b/android/build.gradle index 70a1de5..9bb29bc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,16 @@ buildscript { ext.kotlin_version = '2.0.10' + + repositories { + google() + mavenCentral() + } + + dependencies { + // Add the Android Gradle Plugin classpath + classpath 'com.android.tools.build:gradle:8.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } } allprojects { diff --git a/android/gradle.properties b/android/gradle.properties index 3b5b324..5a505fa 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e1ca574..9f7d524 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip + diff --git a/lib/classes/global.dart b/lib/classes/global.dart index 332078a..bf78be9 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/components/message_panel.dart b/lib/components/message_panel.dart index c9aef41..beb22bc 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:permission_handler/permission_handler.dart'; @@ -15,10 +16,6 @@ import '../encyption/rsa.dart'; import 'audio_service.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, this.onMessageSent}) : super(key: key); final String converser;final VoidCallback? onMessageSent; @@ -30,13 +27,35 @@ class MessagePanel extends StatefulWidget { class _MessagePanelState extends State { TextEditingController myController = TextEditingController(); File _selectedFile = File(''); + + FlutterSoundRecorder? _recorder; // ADDED + bool _isRecording = false; // ADDED + String? _recordedFilePath; // ADDED late final AudioService _audioService; bool _isRecording = false; String? _currentRecordingPath; + @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)); + } + _audioService = AudioService(); _initializeAudio(); } @@ -102,10 +121,39 @@ class _MessagePanelState extends State { } + @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(8.0), + + child: TextFormField( + maxLines: null, + controller: myController, + decoration: InputDecoration( + icon: const Icon(Icons.person), + hintText: 'Send Message?', + labelText: 'Send Message', + suffixIcon: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + 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( + Icons.send, + ), decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, boxShadow: [ @@ -140,6 +188,7 @@ class _MessagePanelState extends State { decoration: BoxDecoration( color: _isRecording ? Colors.red.withOpacity(0.1) : null, shape: BoxShape.circle, + ), child: Icon( _isRecording ? Icons.mic : Icons.mic_none, @@ -161,7 +210,6 @@ class _MessagePanelState extends State { if (myController.text.isEmpty) { return; } - // Encode the message to base64 String data = jsonEncode({ "sender": Global.myName, @@ -189,7 +237,6 @@ class _MessagePanelState extends State { ); RSAPublicKey publicKey = Global.myPublicKey!; - // Encrypt the message Uint8List encryptedMessage = rsaEncrypt( publicKey, Uint8List.fromList(utf8.encode(myController.text))); @@ -204,21 +251,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( @@ -230,10 +332,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'), ), ], ), @@ -251,7 +351,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!); @@ -266,12 +365,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), @@ -280,7 +378,6 @@ class _MessagePanelState extends State { ], ), actions: [ - TextButton( onPressed: () { Navigator.of(context).pop(); @@ -289,10 +386,9 @@ class _MessagePanelState extends State { ), IconButton( onPressed: () { - Navigator.pop(context); - _sendFileMessage(context, _selectedFile); - - }, + Navigator.pop(context); + _sendFileMessage(context, _selectedFile); + }, icon: const Icon( Icons.send, ), @@ -304,6 +400,8 @@ class _MessagePanelState extends State { } } + void _sendFileMessage(BuildContext context, File file) async { + void _sendVoiceMessage(File audioFile) async { final String msgId = nanoid(21); @@ -331,6 +429,7 @@ class _MessagePanelState extends State { /// This function is used to send the file message. void _sendFileMessage(BuildContext context, File file) async{ + var msgId = nanoid(21); String fileName = _selectedFile.path.split('/').last; @@ -365,7 +464,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..bac4556 100644 --- a/lib/encyption/rsa.dart +++ b/lib/encyption/rsa.dart @@ -1,19 +1,24 @@ 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"; - -AsymmetricKeyPair generateRSAkeyPair( - SecureRandom secureRandom, - {int bitLength = 2048}) { +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/random/fortuna_random.dart'; +import 'dart:math'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:sqflite/sqflite.dart'; +import 'package:path/path.dart'; + +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 +26,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 +94,78 @@ 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); } +Future initDatabase() async { + final databasePath = await getDatabasesPath(); + final path = join(databasePath, 'chat_database.db'); + + return openDatabase( + path, + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT, mediaPath TEXT)', + ); + }, + version: 1, + ); +} + +Future saveMessage(String message, {String? mediaPath}) async { + final db = await initDatabase(); + await db.insert( + 'messages', + {'content': message, 'mediaPath': mediaPath}, + conflictAlgorithm: ConflictAlgorithm.replace, + ); +} + +Future>> retrieveMessages() async { + final db = await initDatabase(); + return db.query('messages'); +} + +Future exportChatHistory() async { + final pdf = pw.Document(); + final messages = await retrieveMessages(); + + for (var message in messages) { + pdf.addPage(pw.Page(build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text(message['content']), + if (message['mediaPath'] != null) + pw.Text('Media: ${message['mediaPath']}'), + ], + ); + })); + } + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/chat_history.pdf'; + final file = File(filePath); + await file.writeAsBytes(await pdf.save()); + print('Chat history exported to: $filePath'); +} diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index e3cce21..705e005 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'; @@ -14,16 +13,17 @@ import '../encyption/rsa.dart'; import 'package:audioplayers/audioplayers.dart'; class ChatPage extends StatefulWidget { - const ChatPage({Key? key, required this.converser}) : super(key: key); - - final String converser; + String converser; + ChatPage({Key? key, required this.converser}) : super(key: key); @override - ChatPageState createState() => ChatPageState(); + _ChatPageState createState() => _ChatPageState(); } -class ChatPageState extends State { +class _ChatPageState extends State { + final ScrollController _scrollController = ScrollController(); List messageList = []; + TextEditingController myController = TextEditingController(); final AudioPlayer _audioPlayer = AudioPlayer(); String? _currentlyPlayingId; @@ -31,6 +31,7 @@ class ChatPageState extends State { final ScrollController _scrollController = ScrollController(); bool _isFirstBuild = true; // Add this flag + String _formatDuration(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, '0'); String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); @@ -40,6 +41,28 @@ class ChatPageState extends State { @override void initState() { super.initState(); + + _subscribeToProfileUpdates(); + } + + void _subscribeToProfileUpdates() { + Global.profileNameStream.listen((updatedName) { + if (widget.converser == Global.myName) { + setState(() { + widget.converser = updatedName; + }); + } + }); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); _audioPlayer.setReleaseMode(ReleaseMode.stop); // Stop when completed _audioPlayer.onPlayerComplete.listen((event) { if (mounted) { @@ -91,30 +114,48 @@ class ChatPageState extends State { _scrollToBottom(); _isFirstBuild = false; }); - } - } - 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( title: Text(widget.converser), + actions: [ + IconButton( + icon: Icon(Icons.download), + onPressed: () async { + await exportChatHistory(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Chat history exported successfully!')), + ); + }, + ), + ], ), body: Column( 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: (context, index) { + String date = groupedMessages.keys.elementAt(index); + return _buildMessageGroup(date, groupedMessages[date]!); + }, + ), + controller: _scrollController, padding: const EdgeInsets.all(8), itemCount: groupedMessages.keys.length, @@ -294,12 +335,91 @@ 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), + ), + ), + ], + ); + } + + Future _handleVoicePlayback(Msg msg) async { try { final data = jsonDecode(msg.message); @@ -328,6 +448,7 @@ class ChatPageState extends State { ); } } + Widget _buildFileBubble(Msg msg) { dynamic data = jsonDecode(msg.message); if (data['type'] == 'voice') { @@ -342,26 +463,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/lib/pages/profile.dart b/lib/pages/profile.dart index d6863eb..a3b2480 100644 --- a/lib/pages/profile.dart +++ b/lib/pages/profile.dart @@ -4,9 +4,12 @@ import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:nanoid/nanoid.dart'; import '../classes/global.dart'; +import feature/chat-history-export +import '../services/communication_service.dart'; + import '../providers/theme_provider.dart'; import 'home_screen.dart'; - + main class Profile extends StatefulWidget { final bool onLogin; const Profile({Key? key, required this.onLogin}) : super(key: key); @@ -133,6 +136,50 @@ class _ProfileState extends State with SingleTickerProviderStateMixin { color: colorScheme.primary, ), ), + + ), + feature/chat-history-export + ElevatedButton( + onPressed: () async { + final prefs = await SharedPreferences.getInstance(); + // 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(); + }, + child: const Text("Save"), + ) + + Container( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: ThemeProvider.availableThemes.length, + itemBuilder: (context, index) { + String themeName = ThemeProvider.availableThemes.keys.elementAt(index); + bool isSelected = themeName == themeProvider.currentTheme; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + elevation: isSelected ? 8 : 2, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: () => themeProvider.setTheme(themeName), + borderRadius: BorderRadius.circular(12), + child: Container( + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: isSelected + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ) + : null, + const SizedBox(height: 24), TextFormField( controller: _nameController, @@ -166,6 +213,7 @@ class _ProfileState extends State with SingleTickerProviderStateMixin { 'Your Unique ID', style: TextStyle( color: colorScheme.onSurfaceVariant, + ), ), Text( @@ -180,10 +228,18 @@ class _ProfileState extends State with SingleTickerProviderStateMixin { ], ), ), + + ), + + ], + ); + }, + ], ), ), ), + ); } diff --git a/lib/services/communication_service.dart b/lib/services/communication_service.dart new file mode 100644 index 0000000..3681a17 --- /dev/null +++ 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/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1515165..cfc77e8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,8 +6,10 @@ import FlutterMacOS import Foundation import audio_session + import audioplayers_darwin import device_info_plus + import flutter_secure_storage_macos import just_audio import local_auth_darwin @@ -18,8 +20,10 @@ import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) diff --git a/pubspec.yaml b/pubspec.yaml index 70cb06c..be88ac6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,6 @@ dependencies: flutter_nearby_connections: ^1.1.2 cupertino_icons: ^1.0.8 bubble: ^1.2.1 - nanoid: ^1.0.0 sqflite: ^2.3.3+1 intl: ^0.19.0 pointycastle: ^3.9.1 @@ -31,17 +30,26 @@ dependencies: file_picker: ^8.1.2 open_filex: ^4.5.0 permission_handler: ^11.3.1 - path_provider: ^2.1.4 + path_provider: ^2.1.4 web_socket_channel: ^2.2.0 + nanoid: ^1.0.0 + flutter_sound: any + just_audio: ^0.9.14 + pdf: ^3.11.1 + just_audio: ^0.9.42 record: ^4.4.4 audioplayers: ^6.1.0 device_info_plus: ^11.2.0 + dev_dependencies: flutter_lints: flutter_test: sdk: flutter + + + flutter: uses-material-design: true \ No newline at end of file