416 lines
11 KiB
Dart
416 lines
11 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:badges/badges.dart' as badges;
|
|
|
|
import 'package:together_mobile/common/constants.dart';
|
|
import 'package:together_mobile/database/box_type.dart';
|
|
import 'package:together_mobile/models/contact_model.dart';
|
|
import 'package:together_mobile/models/init_get_it.dart';
|
|
import 'package:together_mobile/models/user_model.dart';
|
|
import 'package:together_mobile/models/websocket_model.dart';
|
|
import 'package:together_mobile/utils/app_dir.dart';
|
|
import 'package:together_mobile/utils/format_datetime.dart';
|
|
import 'input_icon_button.dart';
|
|
|
|
class SendMessage {
|
|
final int type;
|
|
final List<String> receivers;
|
|
final List<String> attachments;
|
|
final String dir;
|
|
|
|
SendMessage(this.type, this.receivers, this.attachments, this.dir);
|
|
|
|
SendMessage.fromJson(Map<String, dynamic> json)
|
|
: type = json['type'],
|
|
receivers = json['receivers'],
|
|
attachments = json['attachments'],
|
|
dir = json['dir'];
|
|
}
|
|
|
|
class MessageInputBox extends StatefulWidget {
|
|
const MessageInputBox({
|
|
super.key,
|
|
required this.chatType,
|
|
required this.contactId,
|
|
required this.scrollController,
|
|
});
|
|
|
|
final int chatType;
|
|
final String contactId;
|
|
final ScrollController scrollController;
|
|
|
|
@override
|
|
State<MessageInputBox> createState() => _MessageInputBoxState();
|
|
}
|
|
|
|
class _MessageInputBoxState extends State<MessageInputBox> {
|
|
final TextEditingController _controller = TextEditingController();
|
|
final ImagePicker _picker = ImagePicker();
|
|
bool _hasMsg = false;
|
|
|
|
late Box<ChatSetting> _chatBox;
|
|
late Box<MessageT> _messageTBox;
|
|
|
|
List<XFile> _imageFileList = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_chatBox = Hive.box<ChatSetting>('chat_setting');
|
|
_messageTBox = Hive.box<MessageT>('message_${widget.contactId}');
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
_controller.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: kPrimaryColor.withOpacity(0.5),
|
|
blurRadius: 30,
|
|
// Shadow spared from the edge of the container
|
|
blurStyle: BlurStyle.outer,
|
|
),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.only(top: 6),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
const SizedBox(
|
|
width: 10,
|
|
),
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(10),
|
|
constraints: const BoxConstraints(maxHeight: 120),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10),
|
|
// Container color will be imply to its child
|
|
color: Theme.of(context).brightness == Brightness.dark
|
|
? const Color.fromARGB(255, 61, 61, 61)
|
|
: kPrimaryColor.withOpacity(0.2),
|
|
),
|
|
child: TextField(
|
|
onChanged: (value) {
|
|
if (value.isNotEmpty) {
|
|
setState(() {
|
|
_hasMsg = true;
|
|
});
|
|
} else {
|
|
if (_imageFileList.isEmpty) {
|
|
setState(() {
|
|
_hasMsg = false;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
minLines: null,
|
|
maxLines: null,
|
|
controller: _controller,
|
|
decoration: const InputDecoration(
|
|
isCollapsed: true,
|
|
border: UnderlineInputBorder(
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 10,
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
_sendMsg();
|
|
},
|
|
style: FilledButton.styleFrom(
|
|
padding: const EdgeInsets.all(0),
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
backgroundColor:
|
|
_hasMsg ? kPrimaryColor : kPrimaryColor.withAlpha(50),
|
|
),
|
|
child: const Text(
|
|
'发送',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 10,
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
InputIconButton(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.mic),
|
|
),
|
|
InputIconButton(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.call),
|
|
),
|
|
badges.Badge(
|
|
showBadge: _imageFileList.isNotEmpty,
|
|
badgeStyle: const badges.BadgeStyle(
|
|
badgeColor: kSecondaryColor,
|
|
elevation: 12,
|
|
),
|
|
badgeContent: Text(_imageFileList.length.toString()),
|
|
position: badges.BadgePosition.topEnd(top: 0, end: 0),
|
|
child: InputIconButton(
|
|
onPressed: () {
|
|
_pickImages(context);
|
|
},
|
|
icon: const Icon(Icons.insert_photo),
|
|
),
|
|
),
|
|
InputIconButton(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.emoji_emotions),
|
|
),
|
|
InputIconButton(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add_box_rounded),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _pickImages(BuildContext context) async {
|
|
try {
|
|
List<XFile> pickedImages = await _picker.pickMultiImage();
|
|
if (pickedImages.isNotEmpty) {
|
|
setState(() {
|
|
_imageFileList = pickedImages;
|
|
_hasMsg = true;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_imageFileList = [];
|
|
});
|
|
if (_controller.text.isEmpty) {
|
|
setState(() {
|
|
_hasMsg = false;
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('Error when pick image: $e');
|
|
}
|
|
}
|
|
|
|
void _sendMsg() async {
|
|
if (!_hasMsg) {
|
|
return;
|
|
}
|
|
|
|
DateTime now = DateTime.now();
|
|
bool isShowTime = _isShowTime(now);
|
|
String senderId = getIt.get<UserAccount>().id;
|
|
String text = _controller.text;
|
|
String type = 'text/multipart';
|
|
List<String> attachments = [];
|
|
|
|
if (_imageFileList.isNotEmpty) {
|
|
String dirTime = formatDirTime(now);
|
|
for (var i = 0; i < _imageFileList.length; i++) {
|
|
attachments.add('$dirTime/${getRandomFilename()}');
|
|
}
|
|
}
|
|
|
|
late int msgId;
|
|
if (_messageTBox.length == 0) {
|
|
msgId = 0;
|
|
} else {
|
|
msgId = _messageTBox.length - 1;
|
|
}
|
|
|
|
_messageTBox.add(
|
|
MessageT(
|
|
msgId,
|
|
senderId,
|
|
text,
|
|
type,
|
|
now,
|
|
isShowTime,
|
|
attachments,
|
|
),
|
|
);
|
|
|
|
Future.delayed(
|
|
const Duration(milliseconds: 50),
|
|
() => widget.scrollController.animateTo(
|
|
0,
|
|
duration: const Duration(milliseconds: 500),
|
|
curve: Curves.linear,
|
|
),
|
|
);
|
|
|
|
final msg = {
|
|
'type': 'text/multipart',
|
|
'msgId': msgId,
|
|
'senderId': senderId,
|
|
'text': text,
|
|
'attachments': attachments,
|
|
'dateTime': now.toString(),
|
|
'isShowTime': isShowTime,
|
|
};
|
|
|
|
if (widget.chatType == 0) {
|
|
msg['event'] = 'friend-chat-msg';
|
|
msg['receiverId'] = widget.contactId;
|
|
getIt.get<WebSocketManager>().channel.sink.add(json.encode(msg));
|
|
if (attachments.isNotEmpty) {
|
|
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
|
List<String> encodedDatas = await compute(
|
|
bytes2json,
|
|
(
|
|
0,
|
|
[widget.contactId],
|
|
attachments,
|
|
_imageFileList,
|
|
baseImageDir,
|
|
),
|
|
);
|
|
|
|
for (final data in encodedDatas) {
|
|
getIt.get<WebSocketManager>().channel.sink.add(data);
|
|
}
|
|
}
|
|
} else {
|
|
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
|
List<String> receiverIds = getIt
|
|
.get<ContactAccountProfile>()
|
|
.groupChats[widget.contactId]!
|
|
.members;
|
|
receiverIds.remove(senderId);
|
|
msg['event'] = 'group-chat-msg';
|
|
msg['groupChatId'] = widget.contactId;
|
|
msg['receiverIds'] = receiverIds;
|
|
msg['nickname'] = getIt.get<UserProfile>().nickname;
|
|
msg['remarkInGroupChat'] =
|
|
getIt.get<Contact>().groupChats[widget.contactId]!.remarkInGroupChat;
|
|
msg['avatar'] = getIt.get<UserProfile>().avatar;
|
|
getIt.get<WebSocketManager>().channel.sink.add(json.encode(msg));
|
|
if (attachments.isNotEmpty) {
|
|
List<String> encodedDatas = await compute(
|
|
bytes2json,
|
|
(
|
|
1,
|
|
receiverIds,
|
|
attachments,
|
|
_imageFileList,
|
|
baseImageDir,
|
|
),
|
|
);
|
|
|
|
for (final data in encodedDatas) {
|
|
getIt.get<WebSocketManager>().channel.sink.add(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
_controller.text = '';
|
|
setState(() {
|
|
_imageFileList = [];
|
|
_hasMsg = false;
|
|
});
|
|
|
|
var chatSetting = _chatBox.get(widget.contactId);
|
|
if (chatSetting == null) {
|
|
_chatBox.put(
|
|
widget.contactId,
|
|
ChatSetting(
|
|
widget.contactId,
|
|
widget.chatType,
|
|
false,
|
|
true,
|
|
false,
|
|
now,
|
|
0,
|
|
),
|
|
);
|
|
} else {
|
|
chatSetting.latestDateTime = now;
|
|
chatSetting.unreadCount = 0;
|
|
chatSetting.isOpen = true;
|
|
_chatBox.put(widget.contactId, chatSetting);
|
|
}
|
|
}
|
|
|
|
bool _isShowTime(DateTime now) {
|
|
int messageCount = _messageTBox.length;
|
|
|
|
if (messageCount == 0) {
|
|
return true;
|
|
} else {
|
|
MessageT? message = _messageTBox.getAt(messageCount - 1);
|
|
DateTime lastTime = message!.dateTime;
|
|
var differenceInMinutes = now.difference(lastTime).inMinutes;
|
|
return differenceInMinutes > 8 ? true : false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<List<String>> bytes2json(
|
|
(
|
|
int,
|
|
List<String>,
|
|
List<String>,
|
|
List<XFile>,
|
|
String,
|
|
) args) async {
|
|
List<String> encodedJson = [];
|
|
for (var i = 0; i < args.$3.length; i++) {
|
|
Uint8List bytes = await args.$4[i].readAsBytes();
|
|
File file = File('${args.$5}/${args.$3[i]}');
|
|
await file.create(recursive: true);
|
|
await file.writeAsBytes(bytes);
|
|
if (args.$1 == 0) {
|
|
encodedJson.add(
|
|
json.encode(
|
|
{
|
|
'event': 'chat-image',
|
|
'receiverId': args.$2[0],
|
|
'filename': args.$3[i],
|
|
'bytes': bytes,
|
|
},
|
|
),
|
|
);
|
|
} else {
|
|
encodedJson.add(
|
|
json.encode(
|
|
{
|
|
'event': 'chat-image',
|
|
'receiverIds': args.$2,
|
|
'filename': args.$3[i],
|
|
'bytes': bytes,
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return encodedJson;
|
|
}
|