mirror of
https://github.com/Linloir/Simple-TCP-Client.git
synced 2025-12-17 00:38:11 +08:00
Core Function
- Chat page available - File transfer available
This commit is contained in:
parent
2a78af4885
commit
c3d0a91c48
@ -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<UserRepository>.value(
|
||||
value: userRepository,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: UserNameText(
|
||||
userid: userID,
|
||||
fontWeight: FontWeight.bold,
|
||||
)
|
||||
child: BlocProvider<ChatCubit>(
|
||||
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<ChatCubit, ChatState>(
|
||||
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<ChatCubit>().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()
|
||||
]
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<ChatState> {
|
||||
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<String, StreamSubscription> messageSendSubscriptionMap = {};
|
||||
final Map<String, StreamSubscription> fileFetchSubscriptionMap = {};
|
||||
|
||||
void addMessage(Message message) {
|
||||
Future<void> 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<void> 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<void> 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<void> 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<ChatHistory> 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<void> close() {
|
||||
subscription.cancel();
|
||||
for(var sub in messageSendSubscriptionMap.values) {
|
||||
sub.cancel();
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Message> chatHistory;
|
||||
final List<ChatHistory> chatHistory;
|
||||
|
||||
const ChatState({required this.chatHistory, required this.status});
|
||||
|
||||
static ChatState empty() => const ChatState(chatHistory: [], status: ChatStatus.fetching);
|
||||
|
||||
ChatState copyWith({
|
||||
ChatStatus? status,
|
||||
List<ChatHistory>? chatHistory
|
||||
}) {
|
||||
return ChatState(
|
||||
status: status ?? this.status,
|
||||
chatHistory: chatHistory ?? this.chatHistory
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [chatHistory];
|
||||
List<Object> get props => [...chatHistory, status];
|
||||
}
|
||||
|
||||
39
lib/chat/model/chat_history.dart
Normal file
39
lib/chat/model/chat_history.dart
Normal file
@ -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<Object> get props => [message.contentmd5, type, status];
|
||||
}
|
||||
78
lib/chat/view/common/file_box.dart
Normal file
78
lib/chat/view/common/file_box.dart
Normal file
@ -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<ChatCubit>().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
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/chat/view/common/image_box.dart
Normal file
28
lib/chat/view/common/image_box.dart
Normal file
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/chat/view/common/text_box.dart
Normal file
94
lib/chat/view/common/text_box.dart
Normal file
@ -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<ChatCubit>().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
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
],
|
||||
)
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
91
lib/chat/view/in_message_box.dart
Normal file
91
lib/chat/view/in_message_box.dart
Normal file
@ -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')}';
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-14 13:49:47
|
||||
* @LastEditTime : 2022-10-14 13:49:47
|
||||
* @Description :
|
||||
*/
|
||||
45
lib/chat/view/input_box/cubit/input_cubit.dart
Normal file
45
lib/chat/view/input_box/cubit/input_cubit.dart
Normal file
@ -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<MessageInputState> {
|
||||
MessageInputCubit({
|
||||
required this.chatCubit,
|
||||
}): super(const MessageInputState());
|
||||
|
||||
final ChatCubit chatCubit;
|
||||
|
||||
void onInputChange(MessageInput input) {
|
||||
emit(state.copyWith(
|
||||
status: Formz.validate([input]),
|
||||
input: input
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> 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()
|
||||
));
|
||||
}
|
||||
}
|
||||
34
lib/chat/view/input_box/cubit/input_state.dart
Normal file
34
lib/chat/view/input_box/cubit/input_state.dart
Normal file
@ -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<Object> get props => [status, input];
|
||||
}
|
||||
86
lib/chat/view/input_box/input_box.dart
Normal file
86
lib/chat/view/input_box/input_box.dart
Normal file
@ -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<MessageInputCubit>(
|
||||
create:(context) => MessageInputCubit(
|
||||
chatCubit: context.read<ChatCubit>()
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 64,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BlocListener<MessageInputCubit, MessageInputState>(
|
||||
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<MessageInputCubit>().onInputChange(MessageInput.dirty(value));
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
var chatCubit = context.read<ChatCubit>();
|
||||
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<MessageInputCubit, MessageInputState>(
|
||||
builder:(context, state) {
|
||||
return IconButton(
|
||||
onPressed: state.status == FormzStatus.valid ? () {
|
||||
context.read<MessageInputCubit>().onSubmission();
|
||||
} : null,
|
||||
icon: const Icon(Icons.send_rounded),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/chat/view/input_box/model/input.dart
Normal file
21
lib/chat/view/input_box/model/input.dart
Normal file
@ -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<String, MessageInputValidationError> {
|
||||
const MessageInput.pure() : super.pure('');
|
||||
const MessageInput.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
@override
|
||||
MessageInputValidationError? validator(String? value) {
|
||||
return value?.isNotEmpty == true ? null : MessageInputValidationError.empty;
|
||||
}
|
||||
}
|
||||
|
||||
91
lib/chat/view/out_message_box.dart
Normal file
91
lib/chat/view/out_message_box.dart
Normal file
@ -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')}';
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
/*
|
||||
* @Author : Linloir
|
||||
* @Date : 2022-10-14 13:49:28
|
||||
* @LastEditTime : 2022-10-14 13:49:28
|
||||
* @Description :
|
||||
*/
|
||||
@ -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;
|
||||
|
||||
@ -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<ContactState> {
|
||||
}): 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;
|
||||
|
||||
@ -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<MessageListState> {
|
||||
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<MessageListState> {
|
||||
|
||||
Future<void> _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<int> addedUserSet = {};
|
||||
@ -82,4 +98,10 @@ class MessageListCubit extends Cubit<MessageListState> {
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
subscription.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Object> get props => [messageList];
|
||||
List<Object> get props => [...messageList];
|
||||
}
|
||||
|
||||
@ -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<MessageListCubit, MessageListState>(
|
||||
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
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<Object?> get props => [message?.contentmd5, targetUser];
|
||||
List<Object> get props => [message?.contentmd5 ?? '', targetUser];
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<InitializationState> {
|
||||
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(
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<void> storeMessages(List<Message> 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<List<Message>> 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<UserInfo> _userInfoChangeStreamController = StreamController();
|
||||
@ -273,4 +278,20 @@ class LocalServiceRepository {
|
||||
Future<UserInfo?> fetchUserInfoViaUsername({required String username}) async {
|
||||
|
||||
}
|
||||
|
||||
Future<Message?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<String, Object?> jsonObject
|
||||
}): super(jsonObject: jsonObject);
|
||||
}): super(jsonObject: jsonObject) {
|
||||
_md5encoded = jsonObject['body'] == null ? null : (jsonObject['body'] as Map<String, Object?>)['md5encoded'] as String?;
|
||||
}
|
||||
|
||||
String? get md5encoded => _md5encoded;
|
||||
}
|
||||
|
||||
class ForwardMessageResponse extends TCPResponse {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user