From c3d0a91c48217d5aca83234467115646ff7b7cc1 Mon Sep 17 00:00:00 2001 From: Linloir <3145078758@qq.com> Date: Sat, 15 Oct 2022 15:30:09 +0800 Subject: [PATCH] Core Function - Chat page available - File transfer available --- lib/chat/chat_page.dart | 71 ++++++- lib/chat/cubit/chat_cubit.dart | 193 +++++++++++++++++- lib/chat/cubit/chat_state.dart | 18 +- lib/chat/model/chat_history.dart | 39 ++++ lib/chat/view/common/file_box.dart | 78 +++++++ lib/chat/view/common/image_box.dart | 28 +++ lib/chat/view/common/text_box.dart | 94 +++++++++ lib/chat/view/history_tile.dart | 65 +++++- lib/chat/view/in_message_box.dart | 91 +++++++++ .../view/in_message_box/in_message_box.dart | 6 - .../view/input_box/cubit/input_cubit.dart | 45 ++++ .../view/input_box/cubit/input_state.dart | 34 +++ lib/chat/view/input_box/input_box.dart | 86 ++++++++ lib/chat/view/input_box/model/input.dart | 21 ++ lib/chat/view/out_message_box.dart | 91 +++++++++ .../view/out_message_box/out_message_box.dart | 6 - lib/home/home_page.dart | 3 +- .../contact_page/cubit/contact_cubit.dart | 4 +- .../message_page/cubit/msg_list_cubit.dart | 28 ++- .../message_page/cubit/msg_list_state.dart | 4 +- lib/home/view/message_page/mesage_page.dart | 8 +- .../message_page/models/message_info.dart | 4 +- .../view/message_page/view/message_tile.dart | 15 +- .../cubit/initialization_cubit.dart | 3 +- lib/repositories/common_models/message.dart | 8 +- .../local_service_repository.dart | 51 +++-- .../tcp_repository/models/tcp_request.dart | 4 +- .../tcp_repository/models/tcp_response.dart | 10 +- .../tcp_repository/tcp_repository.dart | 12 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 31 files changed, 1046 insertions(+), 77 deletions(-) create mode 100644 lib/chat/model/chat_history.dart create mode 100644 lib/chat/view/common/file_box.dart create mode 100644 lib/chat/view/common/image_box.dart create mode 100644 lib/chat/view/common/text_box.dart create mode 100644 lib/chat/view/in_message_box.dart delete mode 100644 lib/chat/view/in_message_box/in_message_box.dart create mode 100644 lib/chat/view/input_box/cubit/input_cubit.dart create mode 100644 lib/chat/view/input_box/cubit/input_state.dart create mode 100644 lib/chat/view/input_box/input_box.dart create mode 100644 lib/chat/view/input_box/model/input.dart create mode 100644 lib/chat/view/out_message_box.dart delete mode 100644 lib/chat/view/out_message_box/out_message_box.dart diff --git a/lib/chat/chat_page.dart b/lib/chat/chat_page.dart index e483ea4..ce16307 100644 --- a/lib/chat/chat_page.dart +++ b/lib/chat/chat_page.dart @@ -1,12 +1,16 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:03:16 - * @LastEditTime : 2022-10-14 11:58:34 + * @LastEditTime : 2022-10-15 10:54:53 * @Description : */ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tcp_client/chat/cubit/chat_cubit.dart'; +import 'package:tcp_client/chat/cubit/chat_state.dart'; +import 'package:tcp_client/chat/view/history_tile.dart'; +import 'package:tcp_client/chat/view/input_box/input_box.dart'; import 'package:tcp_client/common/username/username.dart'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; @@ -42,13 +46,66 @@ class ChatPage extends StatelessWidget { Widget build(BuildContext context) { return RepositoryProvider.value( value: userRepository, - child: Scaffold( - appBar: AppBar( - title: UserNameText( - userid: userID, - fontWeight: FontWeight.bold, - ) + child: BlocProvider( + create: (context) => ChatCubit( + userID: userID, + localServiceRepository: localServiceRepository, + tcpRepository: tcpRepository ), + child: Scaffold( + appBar: AppBar( + title: UserNameText( + userid: userID, + fontWeight: FontWeight.bold, + ) + ), + body: Column( + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) { + return ListView.builder( + physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + reverse: true, + itemBuilder: (context, index) { + if(index == state.chatHistory.length) { + //Load more + context.read().fetchHistory(); + //Show loading indicator + return const Align( + alignment: Alignment.center, + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.blue, + strokeWidth: 2.0, + ), + ), + ); + } + else { + //Return history tile + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8 + ), + child: HistoryTile( + history: state.chatHistory[index], + ), + ); + } + }, + itemCount: state.status == ChatStatus.full ? state.chatHistory.length : state.chatHistory.length + 1, + ); + }, + ), + ), + InputBox() + ] + ), + ) ), ); } diff --git a/lib/chat/cubit/chat_cubit.dart b/lib/chat/cubit/chat_cubit.dart index ce145ef..845fa1f 100644 --- a/lib/chat/cubit/chat_cubit.dart +++ b/lib/chat/cubit/chat_cubit.dart @@ -1,44 +1,233 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:03:56 - * @LastEditTime : 2022-10-14 13:47:33 + * @LastEditTime : 2022-10-15 11:45:04 * @Description : */ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:open_file/open_file.dart'; +import 'package:path/path.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tcp_client/chat/cubit/chat_state.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; import 'package:tcp_client/repositories/common_models/message.dart'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart'; import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart'; import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; class ChatCubit extends Cubit { ChatCubit({ + required this.userID, required this.localServiceRepository, required this.tcpRepository }): super(ChatState.empty()) { subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse); } + final int userID; final LocalServiceRepository localServiceRepository; final TCPRepository tcpRepository; late final StreamSubscription subscription; + final Map messageSendSubscriptionMap = {}; + final Map fileFetchSubscriptionMap = {}; - void addMessage(Message message) { + Future addMessage(Message message) async { //Store locally + localServiceRepository.storeMessages([message]); //Send to server + tcpRepository.pushRequest(SendMessageRequest( + message: message, + token: (await SharedPreferences.getInstance()).getInt('token') + )); //Emit new state + var newHistory = ChatHistory( + message: message, + type: ChatHistoryType.outcome, + status: ChatHistoryStatus.sending + ); + var newHistoryList = [newHistory, ...state.chatHistory]; + emit(state.copyWith(chatHistory: newHistoryList)); + _bindSubscriptionForSending(messageMd5: message.contentmd5); + } + + Future fetchHistory() async { + emit(state.copyWith(status: ChatStatus.fetching)); + //Pull 20 histories from database + var fetchedMessages = await localServiceRepository.fetchMessageHistory( + userID: userID, + position: state.chatHistory.length + ); + var newHistories = []; + for(var message in fetchedMessages) { + var history = ChatHistory( + message: message, + type: message.senderID == userID ? ChatHistoryType.income : ChatHistoryType.outcome, + status: ChatHistoryStatus.done + ); + newHistories.add(history); + } + emit(state.copyWith( + status: fetchedMessages.length == 20 ? ChatStatus.partial : ChatStatus.full, + chatHistory: [...state.chatHistory, ...newHistories] + )); + } + + Future openFile({required Message message}) async { + if(message.type != MessageType.file) { + return; + } + var file = await localServiceRepository.findFile( + filemd5: message.filemd5!, + fileName: message.contentDecoded + ); + if(file != null) { + var newHistory = [...state.chatHistory]; + var index = newHistory.indexWhere((e) => e.message.contentmd5 == message.contentmd5); + if(index == -1) { + return; + } + newHistory[index] = newHistory[index].copyWith(status: ChatHistoryStatus.done); + emit(state.copyWith(chatHistory: newHistory)); + OpenFile.open(file.path); + } + else { + var newHistory = [...state.chatHistory]; + var index = newHistory.indexWhere((e) => e.message.contentmd5 == message.contentmd5); + if(index == -1) { + return; + } + newHistory[index] = newHistory[index].copyWith(status: ChatHistoryStatus.downloading); + emit(state.copyWith(chatHistory: newHistory)); + fetchFile(messageMd5: message.contentmd5); + } + } + + Future fetchFile({required String messageMd5}) async { + var newHistory = [...state.chatHistory]; + var index = newHistory.indexWhere((e) => e.message.contentmd5 == messageMd5); + if(index != -1) { + newHistory[index] = newHistory[index].copyWith( + status: ChatHistoryStatus.done + ); + } + var clonedTCPRepository = await tcpRepository.clone(); + clonedTCPRepository.pushRequest(FetchFileRequest( + msgmd5: messageMd5, + token: (await SharedPreferences.getInstance()).getInt('token') + )); + var subscription = clonedTCPRepository.responseStreamBroadcast.listen((response) { + if(response.type == TCPResponseType.fetchFile) { + response as FetchFileResponse; + if(response.status == TCPResponseStatus.ok) { + fileFetchSubscriptionMap[messageMd5]?.cancel(); + fileFetchSubscriptionMap.remove(messageMd5); + var newHistory = [...state.chatHistory]; + var index = newHistory.indexWhere((e) => e.message.contentmd5 == messageMd5); + if(index != -1) { + newHistory[index] = newHistory[index].copyWith( + status: ChatHistoryStatus.done + ); + } + localServiceRepository.storeFile(tempFile: response.payload); + emit(state.copyWith(chatHistory: newHistory)); + } + else { + fileFetchSubscriptionMap[messageMd5]?.cancel(); + fileFetchSubscriptionMap.remove(messageMd5); + var newHistory = [...state.chatHistory]; + var index = newHistory.indexWhere((e) => e.message.contentmd5 == messageMd5); + if(index != -1) { + newHistory[index] = newHistory[index].copyWith( + status: ChatHistoryStatus.failed + ); + } + emit(state.copyWith(chatHistory: newHistory)); + } + } + }); + fileFetchSubscriptionMap.addEntries([MapEntry(messageMd5, subscription)]); } void _onResponse(TCPResponse response) { + if(response.type == TCPResponseType.forwardMessage) { + response as ForwardMessageResponse; + if(response.message.senderID == userID || response.message.recieverID == userID) { + // Message storage will be handled by home bloc listener + // addMessage(response.message); + //Emit new state + var newHistory = ChatHistory( + message: response.message, + type: response.message.senderID == userID ? ChatHistoryType.income : ChatHistoryType.outcome, + status: ChatHistoryStatus.done + ); + var newHistoryList = [newHistory, ...state.chatHistory]; + emit(state.copyWith(chatHistory: newHistoryList)); + } + } + else if(response.type == TCPResponseType.fetchMessage) { + response as FetchMessageResponse; + List fetchedHistories = []; + for(var message in response.messages) { + if(message.senderID == userID || message.recieverID == userID) { + // addMessage(message); + var newHistory = ChatHistory( + message: message, + type: message.senderID == userID ? ChatHistoryType.income : ChatHistoryType.outcome, + status: ChatHistoryStatus.done + ); + fetchedHistories.insert(0, newHistory); + } + } + var newHistoryList = [...fetchedHistories, ...state.chatHistory]; + emit(state.copyWith(chatHistory: newHistoryList)); + } + } + void _bindSubscriptionForSending({ + required String messageMd5 + }) async { + var subscription = tcpRepository.responseStreamBroadcast.listen((response) { + if(response.type == TCPResponseType.sendMessage) { + response as SendMessageResponse; + if(response.md5encoded == messageMd5) { + messageSendSubscriptionMap[messageMd5]?.cancel(); + messageSendSubscriptionMap.remove(messageMd5); + if(response.status == TCPResponseStatus.ok) { + var newHistory = [...state.chatHistory]; + var index = newHistory.indexWhere((e) => e.message.contentmd5 == messageMd5); + if(index != -1) { + newHistory[index] = newHistory[index].copyWith( + status: ChatHistoryStatus.done + ); + } + emit(state.copyWith(chatHistory: newHistory)); + } + else { + var newHistory = [...state.chatHistory]; + var index = newHistory.indexWhere((e) => e.message.contentmd5 == messageMd5); + if(index != -1) { + newHistory[index] = newHistory[index].copyWith( + status: ChatHistoryStatus.failed + ); + } + emit(state.copyWith(chatHistory: newHistory)); + } + } + } + }); + messageSendSubscriptionMap.addEntries([MapEntry(messageMd5, subscription)]); } @override Future close() { subscription.cancel(); + for(var sub in messageSendSubscriptionMap.values) { + sub.cancel(); + } return super.close(); } } diff --git a/lib/chat/cubit/chat_state.dart b/lib/chat/cubit/chat_state.dart index 201c7fb..58db71c 100644 --- a/lib/chat/cubit/chat_state.dart +++ b/lib/chat/cubit/chat_state.dart @@ -1,23 +1,33 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:03:52 - * @LastEditTime : 2022-10-14 13:42:46 + * @LastEditTime : 2022-10-14 23:04:07 * @Description : */ import 'package:equatable/equatable.dart'; -import 'package:tcp_client/repositories/common_models/message.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; enum ChatStatus { fetching, partial, full } class ChatState extends Equatable { final ChatStatus status; - final List chatHistory; + final List chatHistory; const ChatState({required this.chatHistory, required this.status}); static ChatState empty() => const ChatState(chatHistory: [], status: ChatStatus.fetching); + ChatState copyWith({ + ChatStatus? status, + List? chatHistory + }) { + return ChatState( + status: status ?? this.status, + chatHistory: chatHistory ?? this.chatHistory + ); + } + @override - List get props => [chatHistory]; + List get props => [...chatHistory, status]; } diff --git a/lib/chat/model/chat_history.dart b/lib/chat/model/chat_history.dart new file mode 100644 index 0000000..ac3e02c --- /dev/null +++ b/lib/chat/model/chat_history.dart @@ -0,0 +1,39 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 14:55:20 + * @LastEditTime : 2022-10-14 15:26:25 + * @Description : + */ + +import 'package:equatable/equatable.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; + +enum ChatHistoryType { outcome, income } +enum ChatHistoryStatus { none, sending, downloading, done, failed } + +class ChatHistory extends Equatable { + final Message message; + final ChatHistoryType type; + final ChatHistoryStatus status; + + const ChatHistory({ + required this.message, + required this.type, + required this.status + }); + + ChatHistory copyWith({ + Message? message, + ChatHistoryType? type, + ChatHistoryStatus? status + }) { + return ChatHistory( + message: message ?? this.message, + type: type ?? this.type, + status: status ?? this.status + ); + } + + @override + List get props => [message.contentmd5, type, status]; +} diff --git a/lib/chat/view/common/file_box.dart b/lib/chat/view/common/file_box.dart new file mode 100644 index 0000000..97e7f1c --- /dev/null +++ b/lib/chat/view/common/file_box.dart @@ -0,0 +1,78 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 17:07:13 + * @LastEditTime : 2022-10-15 11:40:47 + * @Description : + */ + +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tcp_client/chat/cubit/chat_cubit.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; + +class FileBox extends StatelessWidget { + const FileBox({ + required this.history, + super.key + }); + + final ChatHistory history; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: (history.status == ChatHistoryStatus.downloading || history.status == ChatHistoryStatus.sending) ? null : () { + EasyDebounce.debounce( + 'findfile${history.message.contentmd5}', + const Duration(milliseconds: 500), + () { + context.read().openFile( + message: history.message + ); + } + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + child: history.status == ChatHistoryStatus.none || history.status == ChatHistoryStatus.done ? + Icon( + Icons.file_present_rounded, + size: 24, + color: history.type == ChatHistoryType.income ? Colors.blue[800] : Colors.white.withOpacity(0.8), + ) : history.status == ChatHistoryStatus.failed ? + Icon( + Icons.refresh_rounded, + size: 24, + color: history.type == ChatHistoryType.income ? Colors.red[800] : Colors.white.withOpacity(0.8), + ) : + SizedBox( + height: 18.0, + width: 18.0, + child: CircularProgressIndicator( + color: history.type == ChatHistoryType.income ? Colors.blue[800] : Colors.white.withOpacity(0.8), + strokeWidth: 3, + ), + ) + ), + const SizedBox(width: 18.0,), + Flexible( + child: Text( + history.message.contentDecoded, + softWrap: true, + style: TextStyle( + fontSize: 20.0, + color: history.type == ChatHistoryType.income ? Colors.grey[900] : Colors.white + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/chat/view/common/image_box.dart b/lib/chat/view/common/image_box.dart new file mode 100644 index 0000000..0a1c92d --- /dev/null +++ b/lib/chat/view/common/image_box.dart @@ -0,0 +1,28 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 17:04:20 + * @LastEditTime : 2022-10-14 17:34:12 + * @Description : + */ + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; + +class ImageBox extends StatelessWidget { + const ImageBox({ + required this.history, + super.key + }); + + final ChatHistory history; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: (){}, + child: Image.memory(base64Decode(history.message.contentDecoded)) + ); + } +} diff --git a/lib/chat/view/common/text_box.dart b/lib/chat/view/common/text_box.dart new file mode 100644 index 0000000..ebb52fc --- /dev/null +++ b/lib/chat/view/common/text_box.dart @@ -0,0 +1,94 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 17:04:12 + * @LastEditTime : 2022-10-15 10:53:28 + * @Description : + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tcp_client/chat/cubit/chat_cubit.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart'; + +class TextBox extends StatelessWidget { + const TextBox({ + required this.history, + super.key + }); + + final ChatHistory history; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: (){}, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if(history.type == ChatHistoryType.outcome) + ...[ + if(history.status == ChatHistoryStatus.sending) + ...[ + SizedBox( + height: 15.0, + width: 15.0, + child: CircularProgressIndicator( + color: Colors.white.withOpacity(0.5), + strokeWidth: 2.0, + ), + ), + const SizedBox(width: 16.0,), + ], + if(history.status == ChatHistoryStatus.failed) + ...[ + ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + child: Icon( + Icons.error_rounded, + color: Colors.white.withOpacity(0.5), + size: 20, + ), + onTap: () async { + context.read().tcpRepository.pushRequest(SendMessageRequest( + message: history.message, + token: (await SharedPreferences.getInstance()).getInt('token') + )); + }, + ), + ), + ), + const SizedBox(width: 8.0,), + ], + if(history.status == ChatHistoryStatus.done) + ...[ + Icon( + Icons.check_rounded, + color: Colors.white.withOpacity(0.5), + size: 20, + ), + const SizedBox(width: 12.0,), + ], + ], + Flexible( + child: Text( + history.message.contentDecoded, + softWrap: true, + style: TextStyle( + fontSize: 18, + color: history.type == ChatHistoryType.income ? Colors.grey[900] : Colors.white + ), + ), + ), + ] + ) + ), + ); + } +} diff --git a/lib/chat/view/history_tile.dart b/lib/chat/view/history_tile.dart index bbce8f4..8ac3554 100644 --- a/lib/chat/view/history_tile.dart +++ b/lib/chat/view/history_tile.dart @@ -1,6 +1,69 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:03:45 - * @LastEditTime : 2022-10-13 14:03:46 + * @LastEditTime : 2022-10-15 10:52:30 * @Description : */ + +import 'package:flutter/material.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; +import 'package:tcp_client/chat/view/in_message_box.dart'; +import 'package:tcp_client/chat/view/out_message_box.dart'; +import 'package:tcp_client/common/avatar/avatar.dart'; + +class HistoryTile extends StatelessWidget { + const HistoryTile({ + required this.history, + super.key + }); + + final ChatHistory history; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: history.type == ChatHistoryType.income ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + if(history.type == ChatHistoryType.income) + ...[ + Expanded( + flex: 5, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + UserAvatar(userid: history.message.senderID), + const SizedBox(width: 16.0,), + Flexible( + child: InMessageBox(history: history) + ), + ], + ) + ), + const Spacer(flex: 1,), + ], + if(history.type == ChatHistoryType.outcome) + ...[ + const Spacer(flex: 1,), + Expanded( + flex: 5, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: OutMessageBox(history: history), + ), + const SizedBox(width: 16.0,), + UserAvatar(userid: history.message.senderID), + ], + ) + ), + ] + ], + ); + } +} diff --git a/lib/chat/view/in_message_box.dart b/lib/chat/view/in_message_box.dart new file mode 100644 index 0000000..b2bffd9 --- /dev/null +++ b/lib/chat/view/in_message_box.dart @@ -0,0 +1,91 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 13:49:47 + * @LastEditTime : 2022-10-15 10:24:35 + * @Description : + */ + +import 'package:flutter/material.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; +import 'package:tcp_client/chat/view/common/file_box.dart'; +import 'package:tcp_client/chat/view/common/image_box.dart'; +import 'package:tcp_client/chat/view/common/text_box.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; + +class InMessageBox extends StatelessWidget { + const InMessageBox({ + required this.history, + super.key + }); + + final ChatHistory history; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + key: ValueKey(history.message.contentmd5), + duration: const Duration(milliseconds: 375), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8 + ), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + bottomLeft: Radius.zero, + bottomRight: Radius.circular(8.0) + ), + boxShadow: [BoxShadow(blurRadius: 5.0, color: Colors.grey.withOpacity(0.3))] + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + bottomLeft: Radius.zero, + bottomRight: Radius.circular(8.0) + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if(history.message.type == MessageType.file) + FileBox(history: history), + if(history.message.type == MessageType.image) + ImageBox(history: history), + if(history.message.type == MessageType.plaintext) + TextBox(history: history), + const SizedBox(height: 4.0,), + Text( + _getTimeStamp(history.message.timeStamp), + style: TextStyle( + fontSize: 12, + color: Colors.grey[400], + ), + ) + ], + ), + ) + ); + } + + String _getTimeStamp(int timeStamp) { + var date = DateTime.fromMillisecondsSinceEpoch(timeStamp); + var weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + //If date is today, return time + if(date.day == DateTime.now().day) { + return '${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } + //If date is yesterday, return 'yesterday' + if(date.day == DateTime.now().day - 1) { + return 'yesterday ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } + //If date is within this week, return the weekday in english + if(date.weekday < DateTime.now().weekday) { + return '${weekdays[date.weekday - 1]} ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } + //Otherwise return the date in english + return '${date.month}/${date.day} ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/chat/view/in_message_box/in_message_box.dart b/lib/chat/view/in_message_box/in_message_box.dart deleted file mode 100644 index 759f99d..0000000 --- a/lib/chat/view/in_message_box/in_message_box.dart +++ /dev/null @@ -1,6 +0,0 @@ -/* - * @Author : Linloir - * @Date : 2022-10-14 13:49:47 - * @LastEditTime : 2022-10-14 13:49:47 - * @Description : - */ diff --git a/lib/chat/view/input_box/cubit/input_cubit.dart b/lib/chat/view/input_box/cubit/input_cubit.dart new file mode 100644 index 0000000..a19b162 --- /dev/null +++ b/lib/chat/view/input_box/cubit/input_cubit.dart @@ -0,0 +1,45 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 21:57:05 + * @LastEditTime : 2022-10-14 22:55:16 + * @Description : + */ + +import 'package:bloc/bloc.dart'; +import 'package:formz/formz.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tcp_client/chat/cubit/chat_cubit.dart'; +import 'package:tcp_client/chat/view/input_box/cubit/input_state.dart'; +import 'package:tcp_client/chat/view/input_box/model/input.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; +import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; +import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; + +class MessageInputCubit extends Cubit { + MessageInputCubit({ + required this.chatCubit, + }): super(const MessageInputState()); + + final ChatCubit chatCubit; + + void onInputChange(MessageInput input) { + emit(state.copyWith( + status: Formz.validate([input]), + input: input + )); + } + + Future onSubmission() async { + chatCubit.addMessage(Message( + userid: (await SharedPreferences.getInstance()).getInt('userid')!, + targetid: chatCubit.userID, + contenttype: MessageType.plaintext, + content: state.input.value, + token: (await SharedPreferences.getInstance()).getInt('token')! + )); + emit(state.copyWith( + status: FormzStatus.pure, + input: const MessageInput.pure() + )); + } +} diff --git a/lib/chat/view/input_box/cubit/input_state.dart b/lib/chat/view/input_box/cubit/input_state.dart new file mode 100644 index 0000000..43fb81d --- /dev/null +++ b/lib/chat/view/input_box/cubit/input_state.dart @@ -0,0 +1,34 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 21:56:53 + * @LastEditTime : 2022-10-14 21:59:51 + * @Description : + */ + +import 'package:equatable/equatable.dart'; +import 'package:formz/formz.dart'; +import 'package:tcp_client/chat/view/input_box/model/input.dart'; + +class MessageInputState extends Equatable { + final MessageInput input; + + final FormzStatus status; + + const MessageInputState({ + this.status = FormzStatus.pure, + this.input = const MessageInput.pure() + }); + + MessageInputState copyWith({ + FormzStatus? status, + MessageInput? input + }) { + return MessageInputState( + input: input ?? this.input, + status: status ?? this.status + ); + } + + @override + List get props => [status, input]; +} \ No newline at end of file diff --git a/lib/chat/view/input_box/input_box.dart b/lib/chat/view/input_box/input_box.dart new file mode 100644 index 0000000..711138f --- /dev/null +++ b/lib/chat/view/input_box/input_box.dart @@ -0,0 +1,86 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 17:54:30 + * @LastEditTime : 2022-10-15 00:27:39 + * @Description : + */ + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; +import 'package:path/path.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tcp_client/chat/cubit/chat_cubit.dart'; +import 'package:tcp_client/chat/view/input_box/cubit/input_cubit.dart'; +import 'package:tcp_client/chat/view/input_box/cubit/input_state.dart'; +import 'package:tcp_client/chat/view/input_box/model/input.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart'; + +class InputBox extends StatelessWidget { + InputBox({super.key}); + + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create:(context) => MessageInputCubit( + chatCubit: context.read() + ), + child: SizedBox( + height: 64, + child: Row( + children: [ + Expanded( + child: BlocListener( + listenWhen: (previous, current) => previous.status != FormzStatus.pure && current.status == FormzStatus.pure, + listener: (context, state) { + _controller.clear(); + }, + child: Builder( + builder: (context) => TextField( + controller: _controller, + onChanged: (value) { + context.read().onInputChange(MessageInput.dirty(value)); + }, + ), + ) + ), + ), + IconButton( + onPressed: () { + var chatCubit = context.read(); + chatCubit.localServiceRepository.pickFile(FileType.any).then((file) async { + if(file != null) { + var newMessage = Message( + userid: (await SharedPreferences.getInstance()).getInt('userid')!, + targetid: chatCubit.userID, + content: basename(file.file.path), + contenttype: MessageType.file, + payload: file, + token: (await SharedPreferences.getInstance()).getInt('token')! + ); + chatCubit.addMessage(newMessage); + } + }); + }, + icon: const Icon(Icons.attach_file_rounded) + ), + BlocBuilder( + builder:(context, state) { + return IconButton( + onPressed: state.status == FormzStatus.valid ? () { + context.read().onSubmission(); + } : null, + icon: const Icon(Icons.send_rounded), + ); + }, + ) + ], + ), + ), + ); + } +} diff --git a/lib/chat/view/input_box/model/input.dart b/lib/chat/view/input_box/model/input.dart new file mode 100644 index 0000000..ce14980 --- /dev/null +++ b/lib/chat/view/input_box/model/input.dart @@ -0,0 +1,21 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 21:57:44 + * @LastEditTime : 2022-10-14 21:57:44 + * @Description : + */ + +import 'package:formz/formz.dart'; + +enum MessageInputValidationError { empty } + +class MessageInput extends FormzInput { + const MessageInput.pure() : super.pure(''); + const MessageInput.dirty([super.value = '']) : super.dirty(); + + @override + MessageInputValidationError? validator(String? value) { + return value?.isNotEmpty == true ? null : MessageInputValidationError.empty; + } +} + diff --git a/lib/chat/view/out_message_box.dart b/lib/chat/view/out_message_box.dart new file mode 100644 index 0000000..a8bf91e --- /dev/null +++ b/lib/chat/view/out_message_box.dart @@ -0,0 +1,91 @@ +/* + * @Author : Linloir + * @Date : 2022-10-14 13:49:28 + * @LastEditTime : 2022-10-15 10:23:43 + * @Description : + */ + +import 'package:flutter/material.dart'; +import 'package:tcp_client/chat/model/chat_history.dart'; +import 'package:tcp_client/chat/view/common/file_box.dart'; +import 'package:tcp_client/chat/view/common/image_box.dart'; +import 'package:tcp_client/chat/view/common/text_box.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; + +class OutMessageBox extends StatelessWidget { + const OutMessageBox({ + required this.history, + super.key + }); + + final ChatHistory history; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + key: ValueKey(history.message.contentmd5), + duration: const Duration(milliseconds: 375), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8 + ), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + bottomLeft: Radius.circular(8.0), + bottomRight: Radius.zero + ), + boxShadow: [BoxShadow(blurRadius: 5.0, color: Colors.grey.withOpacity(0.2))] + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + bottomLeft: Radius.circular(8.0), + bottomRight: Radius.zero + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if(history.message.type == MessageType.file) + FileBox(history: history), + if(history.message.type == MessageType.image) + ImageBox(history: history), + if(history.message.type == MessageType.plaintext) + TextBox(history: history), + const SizedBox(height: 4.0,), + Text( + _getTimeStamp(history.message.timeStamp), + style: TextStyle( + fontSize: 12, + color: Colors.grey[200], + ), + ) + ], + ), + ) + ); + } + + String _getTimeStamp(int timeStamp) { + var date = DateTime.fromMillisecondsSinceEpoch(timeStamp); + var weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + //If date is today, return time + if(date.day == DateTime.now().day) { + return '${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } + //If date is yesterday, return 'yesterday' + if(date.day == DateTime.now().day - 1) { + return 'yesterday ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } + //If date is within this week, return the weekday in english + if(date.weekday < DateTime.now().weekday) { + return '${weekdays[date.weekday - 1]} ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } + //Otherwise return the date in english + return '${date.month}/${date.day} ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } +} \ No newline at end of file diff --git a/lib/chat/view/out_message_box/out_message_box.dart b/lib/chat/view/out_message_box/out_message_box.dart deleted file mode 100644 index 992e513..0000000 --- a/lib/chat/view/out_message_box/out_message_box.dart +++ /dev/null @@ -1,6 +0,0 @@ -/* - * @Author : Linloir - * @Date : 2022-10-14 13:49:28 - * @LastEditTime : 2022-10-14 13:49:28 - * @Description : - */ diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index 32c7b25..70d3aa7 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 11:05:08 - * @LastEditTime : 2022-10-14 10:52:26 + * @LastEditTime : 2022-10-14 15:57:30 * @Description : */ @@ -26,6 +26,7 @@ class HomePage extends StatelessWidget { required this.tcpRepository, super.key }); + //TODO: listen to file storage final int userID; final LocalServiceRepository localServiceRepository; diff --git a/lib/home/view/contact_page/cubit/contact_cubit.dart b/lib/home/view/contact_page/cubit/contact_cubit.dart index 5dc9037..6358371 100644 --- a/lib/home/view/contact_page/cubit/contact_cubit.dart +++ b/lib/home/view/contact_page/cubit/contact_cubit.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:01:45 - * @LastEditTime : 2022-10-14 11:49:50 + * @LastEditTime : 2022-10-14 23:03:02 * @Description : */ @@ -22,7 +22,7 @@ class ContactCubit extends Cubit { }): super(ContactState.empty()) { subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse); updateContacts(); - timer = Timer.periodic(const Duration(seconds: 5), (timer) {updateContacts();}); + timer = Timer.periodic(const Duration(seconds: 10), (timer) {updateContacts();}); } final LocalServiceRepository localServiceRepository; diff --git a/lib/home/view/message_page/cubit/msg_list_cubit.dart b/lib/home/view/message_page/cubit/msg_list_cubit.dart index 3902a66..3060161 100644 --- a/lib/home/view/message_page/cubit/msg_list_cubit.dart +++ b/lib/home/view/message_page/cubit/msg_list_cubit.dart @@ -1,15 +1,16 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:38:31 - * @LastEditTime : 2022-10-13 22:27:29 + * @LastEditTime : 2022-10-15 10:29:05 * @Description : */ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tcp_client/home/view/message_page/cubit/msg_list_state.dart'; import 'package:tcp_client/home/view/message_page/models/message_info.dart'; -import 'package:tcp_client/repositories/common_models/userinfo.dart'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart'; import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; @@ -19,11 +20,12 @@ class MessageListCubit extends Cubit { required this.localServiceRepository, required this.tcpRepository }): super(MessageListState.empty()) { - tcpRepository.responseStreamBroadcast.listen(_onResponse); + subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse); } final LocalServiceRepository localServiceRepository; final TCPRepository tcpRepository; + late final StreamSubscription subscription; void addEmptyMessageOf({required int targetUser}) { if(state.messageList.any((element) => element.targetUser == targetUser)) { @@ -35,6 +37,20 @@ class MessageListCubit extends Cubit { Future _onResponse(TCPResponse response) async { switch(response.type) { + case TCPResponseType.sendMessage: { + response as SendMessageResponse; + if(response.status == TCPResponseStatus.ok) { + var message = await localServiceRepository.fetchMessage(msgmd5: response.md5encoded!); + if(message != null) { + var curUser = (await SharedPreferences.getInstance()).getInt('userid'); + emit(state.updateWithSingle(messageInfo: MessageInfo( + message: message, + targetUser: message.senderID == curUser ? message.recieverID : message.senderID + ))); + } + } + break; + } case TCPResponseType.fetchMessage: { response as FetchMessageResponse; Set addedUserSet = {}; @@ -82,4 +98,10 @@ class MessageListCubit extends Cubit { default: break; } } + + @override + Future close() { + subscription.cancel(); + return super.close(); + } } diff --git a/lib/home/view/message_page/cubit/msg_list_state.dart b/lib/home/view/message_page/cubit/msg_list_state.dart index 9520000..0d3859b 100644 --- a/lib/home/view/message_page/cubit/msg_list_state.dart +++ b/lib/home/view/message_page/cubit/msg_list_state.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:37:49 - * @LastEditTime : 2022-10-13 22:24:22 + * @LastEditTime : 2022-10-15 00:55:49 * @Description : */ @@ -104,5 +104,5 @@ class MessageListState extends Equatable { } @override - List get props => [messageList]; + List get props => [...messageList]; } diff --git a/lib/home/view/message_page/mesage_page.dart b/lib/home/view/message_page/mesage_page.dart index aa43b96..6ab6633 100644 --- a/lib/home/view/message_page/mesage_page.dart +++ b/lib/home/view/message_page/mesage_page.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 11:05:18 - * @LastEditTime : 2022-10-14 11:45:45 + * @LastEditTime : 2022-10-15 10:20:43 * @Description : */ @@ -18,8 +18,7 @@ class MessagePage extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return Container( - child: ListView.separated( + return ListView.separated( itemBuilder: (context, index) { return MessageTile( userID: state.messageList[index].targetUser, @@ -32,8 +31,7 @@ class MessagePage extends StatelessWidget { ); }, itemCount: state.messageList.length - ), - ); + ); } ); } diff --git a/lib/home/view/message_page/models/message_info.dart b/lib/home/view/message_page/models/message_info.dart index 84d3fdc..a58e645 100644 --- a/lib/home/view/message_page/models/message_info.dart +++ b/lib/home/view/message_page/models/message_info.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:48:54 - * @LastEditTime : 2022-10-13 22:24:01 + * @LastEditTime : 2022-10-15 01:01:48 * @Description : */ @@ -19,5 +19,5 @@ class MessageInfo extends Equatable { }); @override - List get props => [message?.contentmd5, targetUser]; + List get props => [message?.contentmd5 ?? '', targetUser]; } diff --git a/lib/home/view/message_page/view/message_tile.dart b/lib/home/view/message_page/view/message_tile.dart index 432b52b..4723240 100644 --- a/lib/home/view/message_page/view/message_tile.dart +++ b/lib/home/view/message_page/view/message_tile.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-13 13:17:52 - * @LastEditTime : 2022-10-14 12:07:32 + * @LastEditTime : 2022-10-15 01:05:11 * @Description : */ @@ -88,18 +88,20 @@ class MessageTile extends StatelessWidget { const SizedBox(width: 16,), Expanded( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ + const SizedBox(height: 6,), Padding( padding: const EdgeInsets.symmetric( - vertical: 8.0, + vertical: 2.0, horizontal: 0 ), child: UserNameText(userid: userID, fontWeight: FontWeight.bold,) ), - const Spacer(), Padding( padding: const EdgeInsets.symmetric( - vertical: 4.0 + vertical: 2.0 ), child: Text( message?.contentDecoded ?? '', @@ -107,7 +109,8 @@ class MessageTile extends StatelessWidget { fontSize: 16, ), ), - ) + ), + const SizedBox(height: 6,), ], ), ), @@ -138,7 +141,7 @@ class MessageTile extends StatelessWidget { var weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; //If date is today, return time if(date.day == DateTime.now().day) { - return '${date.hour}:${date.minute}'; + return '${date.hour}:${date.minute.toString().padLeft(2, '0')}'; } //If date is yesterday, return 'yesterday' if(date.day == DateTime.now().day - 1) { diff --git a/lib/initialization/cubit/initialization_cubit.dart b/lib/initialization/cubit/initialization_cubit.dart index 76d297d..ec57075 100644 --- a/lib/initialization/cubit/initialization_cubit.dart +++ b/lib/initialization/cubit/initialization_cubit.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 09:56:04 - * @LastEditTime : 2022-10-14 14:24:11 + * @LastEditTime : 2022-10-14 16:47:04 * @Description : */ @@ -22,7 +22,6 @@ class InitializationCubit extends Cubit { TCPRepository? tcpRepository; LocalServiceRepository? localServiceRepository; Future(() async { - print('${(await getApplicationDocumentsDirectory()).path}/.data/database.db'); localServiceRepository = await LocalServiceRepository.create(databaseFilePath: '${(await getApplicationDocumentsDirectory()).path}/.data/database.db'); }).then((_) { emit(state.copyWith( diff --git a/lib/repositories/common_models/message.dart b/lib/repositories/common_models/message.dart index 4cd4c71..dfad63c 100644 --- a/lib/repositories/common_models/message.dart +++ b/lib/repositories/common_models/message.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 10:30:05 - * @LastEditTime : 2022-10-11 23:35:45 + * @LastEditTime : 2022-10-14 23:53:18 * @Description : */ @@ -50,7 +50,7 @@ class Message extends JSONEncodable { _timestamp = DateTime.now().millisecondsSinceEpoch, _payload = payload { _contentmd5 = md5.convert( - utf8.encode(content) + utf8.encode(content).toList() ..addAll(Uint8List(4)..buffer.asInt32List()[0] = userid) ..addAll(Uint8List(4)..buffer.asInt32List()[0] = targetid) ..addAll(Uint8List(4)..buffer.asInt32List()[0] = _timestamp) @@ -68,7 +68,7 @@ class Message extends JSONEncodable { _content = jsonObject['content'] as String, _timestamp = jsonObject['timestamp'] as int, _contentmd5 = jsonObject['md5encoded'] as String, - _filemd5 = jsonObject['filemd5'] as String, + _filemd5 = jsonObject['filemd5'] as String?, _payload = payload; int get senderID => _userid; @@ -88,7 +88,7 @@ class Message extends JSONEncodable { 'contenttype': _contenttype.literal, 'content': _content, 'timestamp': _timestamp, - 'md5Encoded': _contentmd5, + 'md5encoded': _contentmd5, 'filemd5': payload?.filemd5 }; } diff --git a/lib/repositories/local_service_repository/local_service_repository.dart b/lib/repositories/local_service_repository/local_service_repository.dart index be5d442..627e427 100644 --- a/lib/repositories/local_service_repository/local_service_repository.dart +++ b/lib/repositories/local_service_repository/local_service_repository.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 10:56:02 - * @LastEditTime : 2022-10-14 14:30:11 + * @LastEditTime : 2022-10-15 11:48:54 * @Description : Local Service Repository */ @@ -40,7 +40,7 @@ class LocalServiceRepository { content text not null, timestamp int not null, md5encoded text primary key, - filemd5 text not null + filemd5 text ); create table files ( filemd5 text primary key, @@ -79,17 +79,15 @@ class LocalServiceRepository { } Future storeMessages(List messages) async { - for(var message in messages) { - try { - await _database.insert( + await _database.transaction((txn) async { + for(var message in messages) { + await txn.insert( 'msgs', message.jsonObject, conflictAlgorithm: ConflictAlgorithm.replace ); - } catch (err) { - //TODO: do something } - } + }); } Future> findMessages({required String pattern}) async { @@ -216,13 +214,20 @@ class LocalServiceRepository { await Directory('$documentPath/files/.lib').create(); var permanentFilePath = '$documentPath/files/.lib/${tempFile.filemd5}'; await tempFile.file.copy(permanentFilePath); - await _database.insert( - 'files', - { - 'filemd5': tempFile.filemd5, - 'dir': permanentFilePath - } - ); + try{ + await _database.insert( + 'files', + { + 'filemd5': tempFile.filemd5, + 'dir': permanentFilePath + }, + conflictAlgorithm: ConflictAlgorithm.replace + ); + } catch (err) { + print(err); + } + //Clear temp file + tempFile.file.delete(); } final StreamController _userInfoChangeStreamController = StreamController(); @@ -273,4 +278,20 @@ class LocalServiceRepository { Future fetchUserInfoViaUsername({required String username}) async { } + + Future fetchMessage({required String msgmd5}) async { + var result = await _database.query( + 'msgs', + where: 'md5encoded = ?', + whereArgs: [ + msgmd5 + ] + ); + if(result.isNotEmpty) { + return Message.fromJSONObject(jsonObject: result[0]); + } + else { + return null; + } + } } diff --git a/lib/repositories/tcp_repository/models/tcp_request.dart b/lib/repositories/tcp_repository/models/tcp_request.dart index a308a05..c1ca30d 100644 --- a/lib/repositories/tcp_repository/models/tcp_request.dart +++ b/lib/repositories/tcp_repository/models/tcp_request.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 09:44:03 - * @LastEditTime : 2022-10-13 23:04:49 + * @LastEditTime : 2022-10-15 11:14:21 * @Description : Abstract TCP request class */ @@ -167,7 +167,7 @@ class SendMessageRequest extends TCPRequest { var jsonString = toJSON(); var requestLength = jsonString.length; yield Uint8List(4)..buffer.asInt32List()[0] = requestLength; - yield Uint8List(4)..buffer.asInt32List()[0] = 0; + yield Uint8List(4)..buffer.asInt32List()[0] = (await _message.payload?.file.length()) ?? 0; yield Uint8List.fromList(jsonString.codeUnits); if(_message.payload != null) { var fileStream = _message.payload!.file.openRead(); diff --git a/lib/repositories/tcp_repository/models/tcp_response.dart b/lib/repositories/tcp_repository/models/tcp_response.dart index 00d36b2..da21432 100644 --- a/lib/repositories/tcp_repository/models/tcp_response.dart +++ b/lib/repositories/tcp_repository/models/tcp_response.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 11:02:19 - * @LastEditTime : 2022-10-14 12:16:04 + * @LastEditTime : 2022-10-14 15:12:02 * @Description : */ @@ -156,9 +156,15 @@ class ModifyProfileResponse extends TCPResponse { } class SendMessageResponse extends TCPResponse { + late final String? _md5encoded; + SendMessageResponse({ required Map jsonObject - }): super(jsonObject: jsonObject); + }): super(jsonObject: jsonObject) { + _md5encoded = jsonObject['body'] == null ? null : (jsonObject['body'] as Map)['md5encoded'] as String?; + } + + String? get md5encoded => _md5encoded; } class ForwardMessageResponse extends TCPResponse { diff --git a/lib/repositories/tcp_repository/tcp_repository.dart b/lib/repositories/tcp_repository/tcp_repository.dart index 066f628..713d146 100644 --- a/lib/repositories/tcp_repository/tcp_repository.dart +++ b/lib/repositories/tcp_repository/tcp_repository.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 09:42:05 - * @LastEditTime : 2022-10-14 09:15:47 + * @LastEditTime : 2022-10-15 11:06:05 * @Description : TCP repository */ @@ -106,7 +106,7 @@ class TCPRepository { remotePort: _remotePort ); duplicatedRepository._requestStreamController.add(request); - await for(var response in duplicatedRepository.responseStream) { + await for(var response in duplicatedRepository.responseStreamBroadcast) { if(response.type == TCPResponseType.sendMessage) { _responseStreamController.add(response); break; @@ -115,6 +115,9 @@ class TCPRepository { duplicatedRepository.dispose(); }); } + else { + _requestStreamController.add(request); + } } else { _requestStreamController.add(request); @@ -333,8 +336,9 @@ class TCPRepository { return hasFile; } - void dispose() { - _socket.close(); + void dispose() async { + await _socket.flush(); + await _socket.close(); _responseRawStreamController.close(); _payloadPullStreamController.close(); _payloadRawStreamController.close(); diff --git a/pubspec.lock b/pubspec.lock index 583f7b5..e906a39 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -229,7 +229,7 @@ packages: source: git version: "3.2.2" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.flutter-io.cn" diff --git a/pubspec.yaml b/pubspec.yaml index 1891b63..adb85ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: azlistview: ^2.0.0 lpinyin: ^2.0.3 easy_debounce: ^2.0.2+1 + path: ^1.8.2 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.