mirror of
https://github.com/Linloir/Simple-TCP-Client.git
synced 2025-12-17 08:48: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
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-13 14:03:16
|
* @Date : 2022-10-13 14:03:16
|
||||||
* @LastEditTime : 2022-10-14 11:58:34
|
* @LastEditTime : 2022-10-15 10:54:53
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/common/username/username.dart';
|
||||||
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
|
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
|
||||||
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
|
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
|
||||||
@ -42,13 +46,66 @@ class ChatPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RepositoryProvider<UserRepository>.value(
|
return RepositoryProvider<UserRepository>.value(
|
||||||
value: userRepository,
|
value: userRepository,
|
||||||
child: Scaffold(
|
child: BlocProvider<ChatCubit>(
|
||||||
appBar: AppBar(
|
create: (context) => ChatCubit(
|
||||||
title: UserNameText(
|
userID: userID,
|
||||||
userid: userID,
|
localServiceRepository: localServiceRepository,
|
||||||
fontWeight: FontWeight.bold,
|
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
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-13 14:03:56
|
* @Date : 2022-10-13 14:03:56
|
||||||
* @LastEditTime : 2022-10-14 13:47:33
|
* @LastEditTime : 2022-10-15 11:45:04
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
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/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/common_models/message.dart';
|
||||||
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.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/models/tcp_response.dart';
|
||||||
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
|
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
|
||||||
|
|
||||||
class ChatCubit extends Cubit<ChatState> {
|
class ChatCubit extends Cubit<ChatState> {
|
||||||
ChatCubit({
|
ChatCubit({
|
||||||
|
required this.userID,
|
||||||
required this.localServiceRepository,
|
required this.localServiceRepository,
|
||||||
required this.tcpRepository
|
required this.tcpRepository
|
||||||
}): super(ChatState.empty()) {
|
}): super(ChatState.empty()) {
|
||||||
subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse);
|
subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final int userID;
|
||||||
final LocalServiceRepository localServiceRepository;
|
final LocalServiceRepository localServiceRepository;
|
||||||
final TCPRepository tcpRepository;
|
final TCPRepository tcpRepository;
|
||||||
late final StreamSubscription subscription;
|
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
|
//Store locally
|
||||||
|
localServiceRepository.storeMessages([message]);
|
||||||
//Send to server
|
//Send to server
|
||||||
|
tcpRepository.pushRequest(SendMessageRequest(
|
||||||
|
message: message,
|
||||||
|
token: (await SharedPreferences.getInstance()).getInt('token')
|
||||||
|
));
|
||||||
//Emit new state
|
//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) {
|
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
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
subscription.cancel();
|
subscription.cancel();
|
||||||
|
for(var sub in messageSendSubscriptionMap.values) {
|
||||||
|
sub.cancel();
|
||||||
|
}
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,33 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-13 14:03:52
|
* @Date : 2022-10-13 14:03:52
|
||||||
* @LastEditTime : 2022-10-14 13:42:46
|
* @LastEditTime : 2022-10-14 23:04:07
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
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 }
|
enum ChatStatus { fetching, partial, full }
|
||||||
|
|
||||||
class ChatState extends Equatable {
|
class ChatState extends Equatable {
|
||||||
final ChatStatus status;
|
final ChatStatus status;
|
||||||
final List<Message> chatHistory;
|
final List<ChatHistory> chatHistory;
|
||||||
|
|
||||||
const ChatState({required this.chatHistory, required this.status});
|
const ChatState({required this.chatHistory, required this.status});
|
||||||
|
|
||||||
static ChatState empty() => const ChatState(chatHistory: [], status: ChatStatus.fetching);
|
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
|
@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
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-13 14:03:45
|
* @Date : 2022-10-13 14:03:45
|
||||||
* @LastEditTime : 2022-10-13 14:03:46
|
* @LastEditTime : 2022-10-15 10:52:30
|
||||||
* @Description :
|
* @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
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 11:05:08
|
* @Date : 2022-10-11 11:05:08
|
||||||
* @LastEditTime : 2022-10-14 10:52:26
|
* @LastEditTime : 2022-10-14 15:57:30
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -26,6 +26,7 @@ class HomePage extends StatelessWidget {
|
|||||||
required this.tcpRepository,
|
required this.tcpRepository,
|
||||||
super.key
|
super.key
|
||||||
});
|
});
|
||||||
|
//TODO: listen to file storage
|
||||||
|
|
||||||
final int userID;
|
final int userID;
|
||||||
final LocalServiceRepository localServiceRepository;
|
final LocalServiceRepository localServiceRepository;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-13 14:01:45
|
* @Date : 2022-10-13 14:01:45
|
||||||
* @LastEditTime : 2022-10-14 11:49:50
|
* @LastEditTime : 2022-10-14 23:03:02
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ class ContactCubit extends Cubit<ContactState> {
|
|||||||
}): super(ContactState.empty()) {
|
}): super(ContactState.empty()) {
|
||||||
subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse);
|
subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse);
|
||||||
updateContacts();
|
updateContacts();
|
||||||
timer = Timer.periodic(const Duration(seconds: 5), (timer) {updateContacts();});
|
timer = Timer.periodic(const Duration(seconds: 10), (timer) {updateContacts();});
|
||||||
}
|
}
|
||||||
|
|
||||||
final LocalServiceRepository localServiceRepository;
|
final LocalServiceRepository localServiceRepository;
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-12 23:38:31
|
* @Date : 2022-10-12 23:38:31
|
||||||
* @LastEditTime : 2022-10-13 22:27:29
|
* @LastEditTime : 2022-10-15 10:29:05
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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/cubit/msg_list_state.dart';
|
||||||
import 'package:tcp_client/home/view/message_page/models/message_info.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/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/models/tcp_response.dart';
|
||||||
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.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.localServiceRepository,
|
||||||
required this.tcpRepository
|
required this.tcpRepository
|
||||||
}): super(MessageListState.empty()) {
|
}): super(MessageListState.empty()) {
|
||||||
tcpRepository.responseStreamBroadcast.listen(_onResponse);
|
subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
final LocalServiceRepository localServiceRepository;
|
final LocalServiceRepository localServiceRepository;
|
||||||
final TCPRepository tcpRepository;
|
final TCPRepository tcpRepository;
|
||||||
|
late final StreamSubscription subscription;
|
||||||
|
|
||||||
void addEmptyMessageOf({required int targetUser}) {
|
void addEmptyMessageOf({required int targetUser}) {
|
||||||
if(state.messageList.any((element) => element.targetUser == targetUser)) {
|
if(state.messageList.any((element) => element.targetUser == targetUser)) {
|
||||||
@ -35,6 +37,20 @@ class MessageListCubit extends Cubit<MessageListState> {
|
|||||||
|
|
||||||
Future<void> _onResponse(TCPResponse response) async {
|
Future<void> _onResponse(TCPResponse response) async {
|
||||||
switch(response.type) {
|
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: {
|
case TCPResponseType.fetchMessage: {
|
||||||
response as FetchMessageResponse;
|
response as FetchMessageResponse;
|
||||||
Set<int> addedUserSet = {};
|
Set<int> addedUserSet = {};
|
||||||
@ -82,4 +98,10 @@ class MessageListCubit extends Cubit<MessageListState> {
|
|||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
subscription.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-12 23:37:49
|
* @Date : 2022-10-12 23:37:49
|
||||||
* @LastEditTime : 2022-10-13 22:24:22
|
* @LastEditTime : 2022-10-15 00:55:49
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -104,5 +104,5 @@ class MessageListState extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [messageList];
|
List<Object> get props => [...messageList];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 11:05:18
|
* @Date : 2022-10-11 11:05:18
|
||||||
* @LastEditTime : 2022-10-14 11:45:45
|
* @LastEditTime : 2022-10-15 10:20:43
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -18,8 +18,7 @@ class MessagePage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<MessageListCubit, MessageListState>(
|
return BlocBuilder<MessageListCubit, MessageListState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Container(
|
return ListView.separated(
|
||||||
child: ListView.separated(
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return MessageTile(
|
return MessageTile(
|
||||||
userID: state.messageList[index].targetUser,
|
userID: state.messageList[index].targetUser,
|
||||||
@ -32,8 +31,7 @@ class MessagePage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: state.messageList.length
|
itemCount: state.messageList.length
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-12 23:48:54
|
* @Date : 2022-10-12 23:48:54
|
||||||
* @LastEditTime : 2022-10-13 22:24:01
|
* @LastEditTime : 2022-10-15 01:01:48
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -19,5 +19,5 @@ class MessageInfo extends Equatable {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [message?.contentmd5, targetUser];
|
List<Object> get props => [message?.contentmd5 ?? '', targetUser];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-13 13:17:52
|
* @Date : 2022-10-13 13:17:52
|
||||||
* @LastEditTime : 2022-10-14 12:07:32
|
* @LastEditTime : 2022-10-15 01:05:11
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -88,18 +88,20 @@ class MessageTile extends StatelessWidget {
|
|||||||
const SizedBox(width: 16,),
|
const SizedBox(width: 16,),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 6,),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8.0,
|
vertical: 2.0,
|
||||||
horizontal: 0
|
horizontal: 0
|
||||||
),
|
),
|
||||||
child: UserNameText(userid: userID, fontWeight: FontWeight.bold,)
|
child: UserNameText(userid: userID, fontWeight: FontWeight.bold,)
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 4.0
|
vertical: 2.0
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
message?.contentDecoded ?? '',
|
message?.contentDecoded ?? '',
|
||||||
@ -107,7 +109,8 @@ class MessageTile extends StatelessWidget {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
const SizedBox(height: 6,),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -138,7 +141,7 @@ class MessageTile extends StatelessWidget {
|
|||||||
var weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
var weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
//If date is today, return time
|
//If date is today, return time
|
||||||
if(date.day == DateTime.now().day) {
|
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 is yesterday, return 'yesterday'
|
||||||
if(date.day == DateTime.now().day - 1) {
|
if(date.day == DateTime.now().day - 1) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-12 09:56:04
|
* @Date : 2022-10-12 09:56:04
|
||||||
* @LastEditTime : 2022-10-14 14:24:11
|
* @LastEditTime : 2022-10-14 16:47:04
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -22,7 +22,6 @@ class InitializationCubit extends Cubit<InitializationState> {
|
|||||||
TCPRepository? tcpRepository;
|
TCPRepository? tcpRepository;
|
||||||
LocalServiceRepository? localServiceRepository;
|
LocalServiceRepository? localServiceRepository;
|
||||||
Future(() async {
|
Future(() async {
|
||||||
print('${(await getApplicationDocumentsDirectory()).path}/.data/database.db');
|
|
||||||
localServiceRepository = await LocalServiceRepository.create(databaseFilePath: '${(await getApplicationDocumentsDirectory()).path}/.data/database.db');
|
localServiceRepository = await LocalServiceRepository.create(databaseFilePath: '${(await getApplicationDocumentsDirectory()).path}/.data/database.db');
|
||||||
}).then((_) {
|
}).then((_) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 10:30:05
|
* @Date : 2022-10-11 10:30:05
|
||||||
* @LastEditTime : 2022-10-11 23:35:45
|
* @LastEditTime : 2022-10-14 23:53:18
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class Message extends JSONEncodable {
|
|||||||
_timestamp = DateTime.now().millisecondsSinceEpoch,
|
_timestamp = DateTime.now().millisecondsSinceEpoch,
|
||||||
_payload = payload {
|
_payload = payload {
|
||||||
_contentmd5 = md5.convert(
|
_contentmd5 = md5.convert(
|
||||||
utf8.encode(content)
|
utf8.encode(content).toList()
|
||||||
..addAll(Uint8List(4)..buffer.asInt32List()[0] = userid)
|
..addAll(Uint8List(4)..buffer.asInt32List()[0] = userid)
|
||||||
..addAll(Uint8List(4)..buffer.asInt32List()[0] = targetid)
|
..addAll(Uint8List(4)..buffer.asInt32List()[0] = targetid)
|
||||||
..addAll(Uint8List(4)..buffer.asInt32List()[0] = _timestamp)
|
..addAll(Uint8List(4)..buffer.asInt32List()[0] = _timestamp)
|
||||||
@ -68,7 +68,7 @@ class Message extends JSONEncodable {
|
|||||||
_content = jsonObject['content'] as String,
|
_content = jsonObject['content'] as String,
|
||||||
_timestamp = jsonObject['timestamp'] as int,
|
_timestamp = jsonObject['timestamp'] as int,
|
||||||
_contentmd5 = jsonObject['md5encoded'] as String,
|
_contentmd5 = jsonObject['md5encoded'] as String,
|
||||||
_filemd5 = jsonObject['filemd5'] as String,
|
_filemd5 = jsonObject['filemd5'] as String?,
|
||||||
_payload = payload;
|
_payload = payload;
|
||||||
|
|
||||||
int get senderID => _userid;
|
int get senderID => _userid;
|
||||||
@ -88,7 +88,7 @@ class Message extends JSONEncodable {
|
|||||||
'contenttype': _contenttype.literal,
|
'contenttype': _contenttype.literal,
|
||||||
'content': _content,
|
'content': _content,
|
||||||
'timestamp': _timestamp,
|
'timestamp': _timestamp,
|
||||||
'md5Encoded': _contentmd5,
|
'md5encoded': _contentmd5,
|
||||||
'filemd5': payload?.filemd5
|
'filemd5': payload?.filemd5
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 10:56:02
|
* @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
|
* @Description : Local Service Repository
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ class LocalServiceRepository {
|
|||||||
content text not null,
|
content text not null,
|
||||||
timestamp int not null,
|
timestamp int not null,
|
||||||
md5encoded text primary key,
|
md5encoded text primary key,
|
||||||
filemd5 text not null
|
filemd5 text
|
||||||
);
|
);
|
||||||
create table files (
|
create table files (
|
||||||
filemd5 text primary key,
|
filemd5 text primary key,
|
||||||
@ -79,17 +79,15 @@ class LocalServiceRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> storeMessages(List<Message> messages) async {
|
Future<void> storeMessages(List<Message> messages) async {
|
||||||
for(var message in messages) {
|
await _database.transaction((txn) async {
|
||||||
try {
|
for(var message in messages) {
|
||||||
await _database.insert(
|
await txn.insert(
|
||||||
'msgs',
|
'msgs',
|
||||||
message.jsonObject,
|
message.jsonObject,
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace
|
conflictAlgorithm: ConflictAlgorithm.replace
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
//TODO: do something
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Message>> findMessages({required String pattern}) async {
|
Future<List<Message>> findMessages({required String pattern}) async {
|
||||||
@ -216,13 +214,20 @@ class LocalServiceRepository {
|
|||||||
await Directory('$documentPath/files/.lib').create();
|
await Directory('$documentPath/files/.lib').create();
|
||||||
var permanentFilePath = '$documentPath/files/.lib/${tempFile.filemd5}';
|
var permanentFilePath = '$documentPath/files/.lib/${tempFile.filemd5}';
|
||||||
await tempFile.file.copy(permanentFilePath);
|
await tempFile.file.copy(permanentFilePath);
|
||||||
await _database.insert(
|
try{
|
||||||
'files',
|
await _database.insert(
|
||||||
{
|
'files',
|
||||||
'filemd5': tempFile.filemd5,
|
{
|
||||||
'dir': permanentFilePath
|
'filemd5': tempFile.filemd5,
|
||||||
}
|
'dir': permanentFilePath
|
||||||
);
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
print(err);
|
||||||
|
}
|
||||||
|
//Clear temp file
|
||||||
|
tempFile.file.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
final StreamController<UserInfo> _userInfoChangeStreamController = StreamController();
|
final StreamController<UserInfo> _userInfoChangeStreamController = StreamController();
|
||||||
@ -273,4 +278,20 @@ class LocalServiceRepository {
|
|||||||
Future<UserInfo?> fetchUserInfoViaUsername({required String username}) async {
|
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
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 09:44:03
|
* @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
|
* @Description : Abstract TCP request class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ class SendMessageRequest extends TCPRequest {
|
|||||||
var jsonString = toJSON();
|
var jsonString = toJSON();
|
||||||
var requestLength = jsonString.length;
|
var requestLength = jsonString.length;
|
||||||
yield Uint8List(4)..buffer.asInt32List()[0] = requestLength;
|
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);
|
yield Uint8List.fromList(jsonString.codeUnits);
|
||||||
if(_message.payload != null) {
|
if(_message.payload != null) {
|
||||||
var fileStream = _message.payload!.file.openRead();
|
var fileStream = _message.payload!.file.openRead();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 11:02:19
|
* @Date : 2022-10-11 11:02:19
|
||||||
* @LastEditTime : 2022-10-14 12:16:04
|
* @LastEditTime : 2022-10-14 15:12:02
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -156,9 +156,15 @@ class ModifyProfileResponse extends TCPResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SendMessageResponse extends TCPResponse {
|
class SendMessageResponse extends TCPResponse {
|
||||||
|
late final String? _md5encoded;
|
||||||
|
|
||||||
SendMessageResponse({
|
SendMessageResponse({
|
||||||
required Map<String, Object?> jsonObject
|
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 {
|
class ForwardMessageResponse extends TCPResponse {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 09:42:05
|
* @Date : 2022-10-11 09:42:05
|
||||||
* @LastEditTime : 2022-10-14 09:15:47
|
* @LastEditTime : 2022-10-15 11:06:05
|
||||||
* @Description : TCP repository
|
* @Description : TCP repository
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ class TCPRepository {
|
|||||||
remotePort: _remotePort
|
remotePort: _remotePort
|
||||||
);
|
);
|
||||||
duplicatedRepository._requestStreamController.add(request);
|
duplicatedRepository._requestStreamController.add(request);
|
||||||
await for(var response in duplicatedRepository.responseStream) {
|
await for(var response in duplicatedRepository.responseStreamBroadcast) {
|
||||||
if(response.type == TCPResponseType.sendMessage) {
|
if(response.type == TCPResponseType.sendMessage) {
|
||||||
_responseStreamController.add(response);
|
_responseStreamController.add(response);
|
||||||
break;
|
break;
|
||||||
@ -115,6 +115,9 @@ class TCPRepository {
|
|||||||
duplicatedRepository.dispose();
|
duplicatedRepository.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
_requestStreamController.add(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
_requestStreamController.add(request);
|
_requestStreamController.add(request);
|
||||||
@ -333,8 +336,9 @@ class TCPRepository {
|
|||||||
return hasFile;
|
return hasFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() async {
|
||||||
_socket.close();
|
await _socket.flush();
|
||||||
|
await _socket.close();
|
||||||
_responseRawStreamController.close();
|
_responseRawStreamController.close();
|
||||||
_payloadPullStreamController.close();
|
_payloadPullStreamController.close();
|
||||||
_payloadRawStreamController.close();
|
_payloadRawStreamController.close();
|
||||||
|
|||||||
@ -229,7 +229,7 @@ packages:
|
|||||||
source: git
|
source: git
|
||||||
version: "3.2.2"
|
version: "3.2.2"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
|
|||||||
@ -54,6 +54,7 @@ dependencies:
|
|||||||
azlistview: ^2.0.0
|
azlistview: ^2.0.0
|
||||||
lpinyin: ^2.0.3
|
lpinyin: ^2.0.3
|
||||||
easy_debounce: ^2.0.2+1
|
easy_debounce: ^2.0.2+1
|
||||||
|
path: ^1.8.2
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user