order chat tile by time, image message can be sent and showed on message screen

main
htylight 2023-08-15 10:53:30 +08:00
parent 277edd7f0e
commit a4daec8f10
17 changed files with 562 additions and 327 deletions

View File

@ -5,6 +5,7 @@ import './constants.dart';
ThemeData lightThemeData(BuildContext context) { ThemeData lightThemeData(BuildContext context) {
return ThemeData( return ThemeData(
useMaterial3: true,
primaryColor: kPrimaryColor, primaryColor: kPrimaryColor,
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: Colors.white,
appBarTheme: appBarThemeLight, appBarTheme: appBarThemeLight,
@ -32,6 +33,7 @@ ThemeData lightThemeData(BuildContext context) {
ThemeData darkThemeData(BuildContext context) { ThemeData darkThemeData(BuildContext context) {
return ThemeData( return ThemeData(
useMaterial3: true,
primaryColor: kPrimaryColor, primaryColor: kPrimaryColor,
scaffoldBackgroundColor: Colors.black, scaffoldBackgroundColor: Colors.black,
appBarTheme: appBarThemeDark, appBarTheme: appBarThemeDark,

View File

@ -17,7 +17,10 @@ class ChatSetting {
bool isHideMsg; bool isHideMsg;
@HiveField(4) @HiveField(4)
DateTime? latestDateTime; DateTime latestDateTime;
@HiveField(5)
int unreadCount;
ChatSetting( ChatSetting(
this.contactId, this.contactId,
@ -25,6 +28,7 @@ class ChatSetting {
this.isOpen, this.isOpen,
this.isHideMsg, this.isHideMsg,
this.latestDateTime, this.latestDateTime,
this.unreadCount,
); );
} }

View File

@ -21,14 +21,15 @@ class ChatSettingAdapter extends TypeAdapter<ChatSetting> {
fields[1] == null ? false : fields[1] as bool, fields[1] == null ? false : fields[1] as bool,
fields[2] == null ? true : fields[2] as bool, fields[2] == null ? true : fields[2] as bool,
fields[3] == null ? false : fields[3] as bool, fields[3] == null ? false : fields[3] as bool,
fields[4] as DateTime?, fields[4] as DateTime,
fields[5] as int,
); );
} }
@override @override
void write(BinaryWriter writer, ChatSetting obj) { void write(BinaryWriter writer, ChatSetting obj) {
writer writer
..writeByte(5) ..writeByte(6)
..writeByte(0) ..writeByte(0)
..write(obj.contactId) ..write(obj.contactId)
..writeByte(1) ..writeByte(1)
@ -38,7 +39,9 @@ class ChatSettingAdapter extends TypeAdapter<ChatSetting> {
..writeByte(3) ..writeByte(3)
..write(obj.isHideMsg) ..write(obj.isHideMsg)
..writeByte(4) ..writeByte(4)
..write(obj.latestDateTime); ..write(obj.latestDateTime)
..writeByte(5)
..write(obj.unreadCount);
} }
@override @override

View File

@ -11,7 +11,7 @@ import 'package:together_mobile/utils/app_dir.dart';
void initDatabase() async { void initDatabase() async {
List<int> encryptionKeyUint8List = await getEncryptKey(); List<int> encryptionKeyUint8List = await getEncryptKey();
await Hive.initFlutter(await getBoxPath()); await Hive.initFlutter(await getBoxDir());
Box<ChatSetting> chatSettingBox = Box<ChatSetting> chatSettingBox =
await Hive.openBox<ChatSetting>('chat_setting'); await Hive.openBox<ChatSetting>('chat_setting');
@ -54,7 +54,7 @@ void openNewMessageBox(String contactId) async {
var chatSettingBox = Hive.box<ChatSetting>('chat_setting'); var chatSettingBox = Hive.box<ChatSetting>('chat_setting');
chatSettingBox chatSettingBox
.add(ChatSetting(contactId, false, true, false, DateTime.now())); .add(ChatSetting(contactId, false, true, false, DateTime.now(), 0));
await Hive.openLazyBox( await Hive.openLazyBox(
'message_$contactId', 'message_$contactId',

View File

@ -44,7 +44,7 @@ class UserProfile extends ChangeNotifier {
String status = ''; String status = '';
String sign = ''; String sign = '';
String avatar = ''; String avatar = '';
String baseAvatarPath = ''; String baseImageDir = '';
bool isInitialised = false; bool isInitialised = false;
Future<void> init(Map<String, dynamic> json) async { Future<void> init(Map<String, dynamic> json) async {
@ -54,7 +54,7 @@ class UserProfile extends ChangeNotifier {
status = json['status'] ?? ''; status = json['status'] ?? '';
sign = json['sign'] ?? ''; sign = json['sign'] ?? '';
avatar = json['avatar'] ?? ''; avatar = json['avatar'] ?? '';
baseAvatarPath = await getAvatarDir(); baseImageDir = await getChatImageDir();
gender = _genderEn2Cn(json['gender'] ?? ''); gender = _genderEn2Cn(json['gender'] ?? '');
isInitialised = true; isInitialised = true;
} }
@ -94,7 +94,7 @@ class UserProfile extends ChangeNotifier {
status = ''; status = '';
sign = ''; sign = '';
avatar = ''; avatar = '';
baseAvatarPath = ''; baseImageDir = '';
isInitialised = false; isInitialised = false;
} }

View File

@ -1,13 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:together_mobile/database/box_type.dart'; import 'package:together_mobile/database/box_type.dart';
import 'package:together_mobile/models/apply_list_model.dart'; import 'package:together_mobile/models/apply_list_model.dart';
import 'package:together_mobile/models/contact_model.dart'; import 'package:together_mobile/models/contact_model.dart';
import 'package:together_mobile/models/init_get_it.dart'; import 'package:together_mobile/models/init_get_it.dart';
import 'package:together_mobile/models/user_model.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status; import 'package:web_socket_channel/status.dart' as status;
@ -51,6 +54,8 @@ class WebSocketManager extends ChangeNotifier {
receiveFriendAdded(data); receiveFriendAdded(data);
case 'friend-deleted': case 'friend-deleted':
receiveFriendDeleted(data); receiveFriendDeleted(data);
case 'chat-image':
receiveImages(data);
} }
} }
@ -130,6 +135,9 @@ enum SocketStatus {
} }
void receiveFriendMsg(Map<String, dynamic> msg) async { void receiveFriendMsg(Map<String, dynamic> msg) async {
print('=================收到了好友信息事件==================');
print(msg);
print('=======================================');
String senderId = msg['senderId'] as String; String senderId = msg['senderId'] as String;
late Box<MessageT> messageTBox; late Box<MessageT> messageTBox;
try { try {
@ -151,11 +159,13 @@ void receiveFriendMsg(Map<String, dynamic> msg) async {
true, true,
false, false,
dateTime, dateTime,
1,
), ),
); );
} else { } else {
chatSetting.isOpen = true; chatSetting.isOpen = true;
chatSetting.latestDateTime = dateTime; chatSetting.latestDateTime = dateTime;
chatSetting.unreadCount++;
chatSettingBox.put(senderId, chatSetting); chatSettingBox.put(senderId, chatSetting);
} }
@ -174,6 +184,9 @@ void receiveFriendMsg(Map<String, dynamic> msg) async {
} }
void receiveApplyFriend(Map<String, dynamic> msg) { void receiveApplyFriend(Map<String, dynamic> msg) {
print('=================收到了申请好友事件==================');
print(msg);
print('=======================================');
getIt.get<ApplyList>().addJson(msg); getIt.get<ApplyList>().addJson(msg);
} }
@ -194,3 +207,14 @@ void receiveFriendDeleted(Map<String, dynamic> msg) {
getIt.get<Contact>().removeFriend(msg['friendId']); getIt.get<Contact>().removeFriend(msg['friendId']);
getIt.get<ContactAccountProfile>().removeFriend(msg['friendId']); getIt.get<ContactAccountProfile>().removeFriend(msg['friendId']);
} }
void receiveImages(Map<String, dynamic> msg) async {
print('=================收到了聊天图片事件==================');
print(msg);
print('=======================================');
String chatImageDir = getIt.get<UserProfile>().baseImageDir;
File file = await File('$chatImageDir/${msg['filename']}').create(
recursive: true,
);
await file.writeAsBytes(msg['bytes']);
}

View File

@ -31,7 +31,7 @@ final contactRouter = GoRoute(
path: 'add', path: 'add',
name: 'AddContact', name: 'AddContact',
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
builder: (context, state) => const SearchNewScreen(), builder: (context, state) => const SearchNewScreen() ,
routes: [ routes: [
GoRoute( GoRoute(
path: 'friend', path: 'friend',

View File

@ -27,24 +27,25 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> { class _ChatScreenState extends State<ChatScreen> {
Future<bool> _initData() async { Future<bool> _initData() async {
List<int> encryptionKeyUint8List = await getEncryptKey();
await Hive.initFlutter(await getBoxPath());
Box<ChatSetting> chatSettingBox =
await Hive.openBox<ChatSetting>('chat_setting');
final openedChats =
chatSettingBox.values.where((element) => element.isOpen);
for (var chatBox in openedChats) {
Hive.openBox<MessageT>(
'message_${chatBox.contactId}',
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
compactionStrategy: (entries, deletedEntries) => entries > 200,
);
}
if (!getIt.get<UserProfile>().isInitialised) { if (!getIt.get<UserProfile>().isInitialised) {
List<int> encryptionKeyUint8List = await getEncryptKey();
await Hive.initFlutter(await getBoxDir());
Box<ChatSetting> chatSettingBox =
await Hive.openBox<ChatSetting>('chat_setting');
final openedChats =
chatSettingBox.values.where((element) => element.isOpen);
for (var chatBox in openedChats) {
await Hive.openBox<MessageT>(
'message_${chatBox.contactId}',
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
compactionStrategy: (entries, deletedEntries) => entries > 200,
);
}
getIt.get<WebSocketManager>().connect(getIt.get<UserAccount>().id); getIt.get<WebSocketManager>().connect(getIt.get<UserAccount>().id);
List<Map<String, dynamic>> res = await Future.wait([ List<Map<String, dynamic>> res = await Future.wait([
@ -120,8 +121,14 @@ class _ChatScreenState extends State<ChatScreen> {
valueListenable: valueListenable:
Hive.box<ChatSetting>('chat_setting').listenable(), Hive.box<ChatSetting>('chat_setting').listenable(),
builder: (context, Box<ChatSetting> box, _) { builder: (context, Box<ChatSetting> box, _) {
final openedChat = final List<ChatSetting> openedChat =
box.values.where((element) => element.isOpen).toList(); box.values.where((element) => element.isOpen).toList();
// latestMsg on the top
openedChat.sort(
(a, b) => b.latestDateTime.compareTo(a.latestDateTime),
);
if (openedChat.isEmpty) { if (openedChat.isEmpty) {
return const Center( return const Center(
child: Text( child: Text(
@ -141,8 +148,9 @@ class _ChatScreenState extends State<ChatScreen> {
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
String contactId = openedChat[index].contactId; String contactId = openedChat[index].contactId;
String showedTime = formatTileDateTime( String showedTime = formatTileDateTime(
openedChat[index].latestDateTime!, openedChat[index].latestDateTime,
); );
int unreadCount = openedChat[index].unreadCount;
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: valueListenable:
@ -162,6 +170,7 @@ class _ChatScreenState extends State<ChatScreen> {
attachments: messageT.attachments, attachments: messageT.attachments,
dateTime: showedTime, dateTime: showedTime,
isShowTime: messageT.isShowTime, isShowTime: messageT.isShowTime,
unreadCount: unreadCount,
); );
} else { } else {
return ChatTile( return ChatTile(
@ -173,6 +182,7 @@ class _ChatScreenState extends State<ChatScreen> {
attachments: const [], attachments: const [],
dateTime: showedTime, dateTime: showedTime,
isShowTime: false, isShowTime: false,
unreadCount: 0,
); );
} }
}, },

View File

@ -3,32 +3,28 @@ import 'package:flutter/material.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:together_mobile/common/constants.dart'; import 'package:together_mobile/common/constants.dart';
class BadgeAvatar extends StatefulWidget { class BadgeAvatar extends StatelessWidget {
const BadgeAvatar({ const BadgeAvatar({
super.key, super.key,
required this.count, required this.unreadCount,
required this.radius, required this.radius,
required this.backgroundImage, required this.backgroundImage,
}); });
final int count; final int unreadCount;
final double radius; final double radius;
final ImageProvider<Object> backgroundImage; final ImageProvider<Object> backgroundImage;
@override
State<BadgeAvatar> createState() => _BadgeAvatarState();
}
class _BadgeAvatarState extends State<BadgeAvatar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return badges.Badge( return badges.Badge(
showBadge: unreadCount > 0,
badgeStyle: const badges.BadgeStyle( badgeStyle: const badges.BadgeStyle(
badgeColor: kErrorColor, badgeColor: kErrorColor,
elevation: 12, elevation: 12,
), ),
badgeContent: Text( badgeContent: Text(
'${widget.count}+', unreadCount > 99 ? '$unreadCount+' : '$unreadCount',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).colorScheme.inversePrimary, color: Theme.of(context).colorScheme.inversePrimary,
@ -37,7 +33,7 @@ class _BadgeAvatarState extends State<BadgeAvatar> {
badgeAnimation: const badges.BadgeAnimation.scale(), badgeAnimation: const badges.BadgeAnimation.scale(),
position: badges.BadgePosition.topEnd(top: -12, end: -16), position: badges.BadgePosition.topEnd(top: -12, end: -16),
child: CircleAvatar( child: CircleAvatar(
backgroundImage: widget.backgroundImage, backgroundImage: backgroundImage,
), ),
); );
} }

View File

@ -23,9 +23,10 @@ class ChatTile extends StatefulWidget {
required this.attachments, required this.attachments,
required this.dateTime, required this.dateTime,
required this.isShowTime, required this.isShowTime,
required this.unreadCount,
}); });
final int index; final int index, unreadCount;
final String contactId, senderId, type, text, dateTime; final String contactId, senderId, type, text, dateTime;
final List<String> attachments; final List<String> attachments;
final bool isShowTime; final bool isShowTime;
@ -62,7 +63,7 @@ class _ChatTileState extends State<ChatTile> {
foregroundColor: kContentColorDark, foregroundColor: kContentColorDark,
backgroundColor: kPrimaryColor, backgroundColor: kPrimaryColor,
icon: Icons.remove_red_eye, icon: Icons.remove_red_eye,
label: '隐藏', label: '隐藏',
flex: 1, flex: 1,
), ),
], ],
@ -78,13 +79,13 @@ class _ChatTileState extends State<ChatTile> {
.friends[widget.contactId]! .friends[widget.contactId]!
.avatar .avatar
.isEmpty .isEmpty
? const BadgeAvatar( ? BadgeAvatar(
count: 99, unreadCount: widget.unreadCount,
radius: 25, radius: 25,
backgroundImage: AssetImage('assets/images/user_3.png'), backgroundImage: const AssetImage('assets/images/user_3.png'),
) )
: BadgeAvatar( : BadgeAvatar(
count: 99, unreadCount: widget.unreadCount,
radius: 25, radius: 25,
backgroundImage: CachedNetworkImageProvider( backgroundImage: CachedNetworkImageProvider(
'$avatarsUrl/${getIt.get<ContactAccountProfile>().friends[widget.contactId]!.avatar}', '$avatarsUrl/${getIt.get<ContactAccountProfile>().friends[widget.contactId]!.avatar}',

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,7 +10,7 @@ import 'package:together_mobile/models/init_get_it.dart';
import 'package:together_mobile/models/user_model.dart'; import 'package:together_mobile/models/user_model.dart';
import 'package:together_mobile/request/server.dart'; import 'package:together_mobile/request/server.dart';
class MessageBubble extends StatelessWidget { class MessageBubble extends StatefulWidget {
const MessageBubble({ const MessageBubble({
super.key, super.key,
required this.contactId, required this.contactId,
@ -23,6 +26,70 @@ class MessageBubble extends StatelessWidget {
final bool isShowTime; final bool isShowTime;
final List<String> attachments; final List<String> attachments;
@override
State<MessageBubble> createState() => _MessageBubbleState();
}
class _MessageBubbleState extends State<MessageBubble> {
// add late here so you can access widget instance
List<bool> _isImagesLoaded = [];
final List<Timer> _timerList = [];
@override
void initState() {
super.initState();
_isImagesLoaded = List.filled(widget.attachments.length, false);
for (var i = 0; i < widget.attachments.length; i++) {
String imagePath =
'${getIt.get<UserProfile>().baseImageDir}/${widget.attachments[i]}';
File file = File(imagePath);
if (file.existsSync()) {
_isImagesLoaded[i] = true;
} else {
_isImagesLoaded[i] = false;
_timerList.add(Timer.periodic(
const Duration(milliseconds: 200),
(timer) {
if ((file.existsSync())) {
setState(() {
_isImagesLoaded[i] = true;
});
timer.cancel();
}
},
));
}
}
// _isImagesLoaded = List.filled(widget.attachments.length, false);
// _timerList = List.generate(
// widget.attachments.length,
// (index) {
// return Timer.periodic(
// const Duration(milliseconds: 200),
// (timer) async {
// String imagePath =
// '${getIt.get<UserProfile>().baseImageDir}/${widget.attachments[index]}';
// File file = File(imagePath);
// if ((await file.exists())) {
// setState(() {
// _isImagesLoaded[index] = true;
// });
// timer.cancel();
// }
// },
// );
// },
// );
}
@override
void dispose() {
super.dispose();
for (var element in _timerList) {
element.cancel();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@ -33,25 +100,26 @@ class MessageBubble extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
// message date time // message date time
if (isShowTime) if (widget.isShowTime)
SizedBox( SizedBox(
height: 35.0, height: 35.0,
child: Text( child: Text(
dateTime, widget.dateTime,
style: const TextStyle( style: const TextStyle(
color: kUnActivatedColor, color: kUnActivatedColor,
), ),
), ),
), ),
Row( Row(
textDirection: textDirection: widget.senderId == widget.contactId
senderId == contactId ? TextDirection.ltr : TextDirection.rtl, ? TextDirection.ltr
: TextDirection.rtl,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
senderId == contactId widget.senderId == widget.contactId
? getIt ? getIt
.get<ContactAccountProfile>() .get<ContactAccountProfile>()
.friends[contactId]! .friends[widget.contactId]!
.avatar .avatar
.isEmpty .isEmpty
? CircleAvatar( ? CircleAvatar(
@ -59,7 +127,7 @@ class MessageBubble extends StatelessWidget {
) )
: CircleAvatar( : CircleAvatar(
backgroundImage: CachedNetworkImageProvider( backgroundImage: CachedNetworkImageProvider(
'$avatarsUrl/${getIt.get<ContactAccountProfile>().friends[contactId]!.avatar}', '$avatarsUrl/${getIt.get<ContactAccountProfile>().friends[widget.contactId]!.avatar}',
), ),
) )
: CircleAvatar( : CircleAvatar(
@ -72,7 +140,7 @@ class MessageBubble extends StatelessWidget {
), ),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: senderId == contactId crossAxisAlignment: widget.senderId == widget.contactId
? CrossAxisAlignment.start ? CrossAxisAlignment.start
: CrossAxisAlignment.end, : CrossAxisAlignment.end,
children: [ children: [
@ -83,12 +151,12 @@ class MessageBubble extends StatelessWidget {
// height: 5, // height: 5,
// ), // ),
if (type == 'text/multipart') if (widget.type == 'text/multipart')
// message box // message box
Container( Container(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 0), padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: senderId == contactId color: widget.senderId == widget.contactId
? const Color.fromARGB(255, 241, 241, 241) ? const Color.fromARGB(255, 241, 241, 241)
: kPrimaryColor, : kPrimaryColor,
borderRadius: BorderRadius.circular(10.0), borderRadius: BorderRadius.circular(10.0),
@ -97,31 +165,46 @@ class MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// text message content // text message content
Text( if (widget.text.isNotEmpty)
text, Text(
textWidthBasis: TextWidthBasis.longestLine, widget.text,
), textWidthBasis: TextWidthBasis.longestLine,
),
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
// image content if has // image content if have
if (attachments.isNotEmpty) if (widget.attachments.isNotEmpty)
...List.filled( ...List.generate(
attachments.length, widget.attachments.length,
Container( (int index) {
margin: const EdgeInsets.only( return Container(
bottom: 10, margin: const EdgeInsets.only(
), bottom: 15,
constraints: const BoxConstraints( ),
maxHeight: 100, constraints: const BoxConstraints(
), maxHeight: 120,
decoration: BoxDecoration( ),
borderRadius: BorderRadius.circular(10.0), decoration: BoxDecoration(
), borderRadius: BorderRadius.circular(10.0),
child: Image.asset( ),
'assets/images/Logo_dark.png', child: _isImagesLoaded[index]
), ? Image.file(File(
), '${getIt.get<UserProfile>().baseImageDir}/${widget.attachments[index]}',
))
: Container(
width: 20,
height: 40,
padding: const EdgeInsets.symmetric(
vertical: 10),
child:
const CircularProgressIndicator(
color: kSecondaryColor,
strokeWidth: 3.0,
),
),
);
},
), ),
], ],
), ),

View File

@ -1,14 +1,20 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.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/common/constants.dart';
import 'package:together_mobile/database/box_type.dart'; import 'package:together_mobile/database/box_type.dart';
import 'package:together_mobile/models/init_get_it.dart'; import 'package:together_mobile/models/init_get_it.dart';
import 'package:together_mobile/models/user_model.dart'; import 'package:together_mobile/models/user_model.dart';
import 'package:together_mobile/models/websocket_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'; import 'input_icon_button.dart';
class MessageInputBox extends StatefulWidget { class MessageInputBox extends StatefulWidget {
@ -27,11 +33,14 @@ class MessageInputBox extends StatefulWidget {
class _MessageInputBoxState extends State<MessageInputBox> { class _MessageInputBoxState extends State<MessageInputBox> {
final TextEditingController _controller = TextEditingController(); final TextEditingController _controller = TextEditingController();
final ImagePicker _picker = ImagePicker();
bool _hasMsg = false; bool _hasMsg = false;
late Box<ChatSetting> _chatBox; late Box<ChatSetting> _chatBox;
late Box<MessageT> _messageTBox; late Box<MessageT> _messageTBox;
List<XFile> _imageFileList = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -85,9 +94,11 @@ class _MessageInputBoxState extends State<MessageInputBox> {
_hasMsg = true; _hasMsg = true;
}); });
} else { } else {
setState(() { if (_imageFileList.isEmpty) {
_hasMsg = false; setState(() {
}); _hasMsg = false;
});
}
} }
}, },
minLines: null, minLines: null,
@ -132,15 +143,26 @@ class _MessageInputBoxState extends State<MessageInputBox> {
children: [ children: [
InputIconButton( InputIconButton(
onPressed: () {}, onPressed: () {},
icon: const Icon(Icons.insert_photo), icon: const Icon(Icons.mic),
), ),
InputIconButton( InputIconButton(
onPressed: () {}, onPressed: () {},
icon: const Icon(Icons.call), icon: const Icon(Icons.call),
), ),
InputIconButton( badges.Badge(
onPressed: () {}, showBadge: _imageFileList.isNotEmpty,
icon: const Icon(Icons.mic), 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( InputIconButton(
onPressed: () {}, onPressed: () {},
@ -157,13 +179,37 @@ class _MessageInputBoxState extends State<MessageInputBox> {
); );
} }
void _sendMsg() { 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(e);
}
}
void _sendMsg() async {
if (!_hasMsg) { if (!_hasMsg) {
return; return;
} }
DateTime now = DateTime.now(); DateTime now = DateTime.now();
late bool isShowTime; late bool isShowTime;
List<String> attachments = [];
int messageCount = _messageTBox.length; int messageCount = _messageTBox.length;
if (messageCount == 0) { if (messageCount == 0) {
@ -174,28 +220,25 @@ class _MessageInputBoxState extends State<MessageInputBox> {
var differenceInMinutes = now.difference(lastTime).inMinutes; var differenceInMinutes = now.difference(lastTime).inMinutes;
isShowTime = differenceInMinutes > 8 ? true : false; isShowTime = differenceInMinutes > 8 ? true : false;
} }
if (_imageFileList.isNotEmpty) {
String dirTime = formatDirTime(now);
for (var i = 0; i < _imageFileList.length; i++) {
attachments.add('$dirTime/${getRandomFilename()}');
}
}
final msg = { final msg = {
'event': 'one-to-one-chat', 'event': 'one-to-one-chat',
'type': 'text/multipart', 'type': 'text/multipart',
'senderId': getIt.get<UserAccount>().id, 'senderId': getIt.get<UserAccount>().id,
'receiverId': widget.contactId, 'receiverId': widget.contactId,
'text': _controller.text, 'text': _controller.text,
'attachments': [], 'attachments': attachments,
'dateTime': now.toString(), 'dateTime': now.toString(),
'isShowTime': isShowTime, 'isShowTime': isShowTime,
}; };
getIt.get<WebSocketManager>().channel.sink.add(json.encode(msg));
_controller.text = '';
var chatSetting = _chatBox.get(widget.contactId);
if (chatSetting == null) {
_chatBox.put(
widget.contactId,
ChatSetting(widget.contactId, false, true, false, now),
);
}
_messageTBox.add( _messageTBox.add(
MessageT( MessageT(
msg['senderId']! as String, msg['senderId']! as String,
@ -203,17 +246,50 @@ class _MessageInputBoxState extends State<MessageInputBox> {
msg['type']! as String, msg['type']! as String,
now, now,
isShowTime, isShowTime,
[], attachments,
), ),
); );
Future.delayed( Future.delayed(
const Duration(milliseconds: 50), const Duration(milliseconds: 50),
() => widget.scrollController.animateTo( () => widget.scrollController.animateTo(
widget.scrollController.position.maxScrollExtent, 0,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
curve: Curves.linear, curve: Curves.linear,
), ),
); );
getIt.get<WebSocketManager>().channel.sink.add(json.encode(msg));
if (attachments.isNotEmpty) {
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
for (var i = 0; i < attachments.length; i++) {
Uint8List bytes = await _imageFileList[i].readAsBytes();
File file = File('$baseImageDir/${attachments[i]}');
file.createSync(recursive: true);
file.writeAsBytes(bytes);
getIt.get<WebSocketManager>().channel.sink.add(json.encode(
{
'event': 'chat-image',
'receiverId': widget.contactId,
'filename': attachments[i],
'bytes': bytes,
},
));
}
}
_controller.text = '';
setState(() {
_imageFileList = [];
_hasMsg = false;
});
var chatSetting = _chatBox.get(widget.contactId);
if (chatSetting == null) {
_chatBox.put(
widget.contactId,
ChatSetting(widget.contactId, false, true, false, now, 0),
);
}
} }
} }

View File

@ -23,16 +23,9 @@ class MessageScreen extends StatefulWidget {
} }
class _MessageScreenState extends State<MessageScreen> { class _MessageScreenState extends State<MessageScreen> {
ScrollController _controller = ScrollController(); final ScrollController _controller = ScrollController();
final Box<ChatSetting> _chatSettingBox =
@override Hive.box<ChatSetting>('chat_setting');
void initState() {
super.initState();
Future.delayed(
const Duration(microseconds: 500),
() => _controller.jumpTo(_controller.position.maxScrollExtent),
);
}
@override @override
void dispose() { void dispose() {
@ -77,46 +70,64 @@ class _MessageScreenState extends State<MessageScreen> {
body: Column( body: Column(
children: [ children: [
Expanded( Expanded(
child: ValueListenableBuilder( child: Align(
valueListenable: Hive.box<MessageT>('message_${widget.contactId}') alignment: Alignment.topCenter,
.listenable(), child: ValueListenableBuilder(
builder: (context, value, _) { valueListenable:
Future.delayed( Hive.box<MessageT>('message_${widget.contactId}')
const Duration( .listenable(),
milliseconds: 50, builder: (context, value, _) {
), // Set unreadCount to 0 when at message screen
() => _controller.animateTo( ChatSetting? chatSetting =
_controller.position.maxScrollExtent, _chatSettingBox.get(widget.contactId);
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
),
);
return ListView.builder( // whethe it is a brand new chat
physics: const BouncingScrollPhysics( if (chatSetting != null) {
parent: AlwaysScrollableScrollPhysics(), chatSetting.unreadCount = 0;
), _chatSettingBox.put(widget.contactId, chatSetting);
controller: _controller, }
itemCount:
Hive.box<MessageT>('message_${widget.contactId}').length,
itemBuilder: (context, index) {
Box<MessageT> messageTBox =
Hive.box<MessageT>('message_${widget.contactId}');
MessageT messageT = messageTBox.getAt(index)!; Future.delayed(
const Duration(
milliseconds: 100,
),
() => _controller.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
),
);
return MessageBubble( return ListView.builder(
contactId: widget.contactId, physics: const BouncingScrollPhysics(
senderId: messageT.senderId, parent: AlwaysScrollableScrollPhysics(),
dateTime: formatMessageDateTime(messageT.dateTime), ),
isShowTime: messageT.isShowTime, controller: _controller,
type: messageT.type, shrinkWrap: true,
text: messageT.text, reverse: true,
attachments: messageT.attachments, itemCount: Hive.box<MessageT>('message_${widget.contactId}')
); .length,
}, itemBuilder: (context, index) {
); Box<MessageT> messageTBox =
}, Hive.box<MessageT>('message_${widget.contactId}');
int length = messageTBox.length;
int i = length - index - 1;
MessageT messageT = messageTBox.getAt(i)!;
return MessageBubble(
key: ValueKey(i),
contactId: widget.contactId,
senderId: messageT.senderId,
dateTime: formatMessageDateTime(messageT.dateTime),
isShowTime: messageT.isShowTime,
type: messageT.type,
text: messageT.text,
attachments: messageT.attachments,
);
},
);
},
),
), ),
), ),
MessageInputBox( MessageInputBox(

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -15,7 +16,22 @@ Future<String> getAvatarDir() async {
return '${appDirectory.path}/${getIt.get<UserAccount>().id}/images/avatars'; return '${appDirectory.path}/${getIt.get<UserAccount>().id}/images/avatars';
} }
Future<String> getBoxPath() async { Future<String> getBoxDir() async {
Directory appDirectory = await getApplicationDocumentsDirectory(); Directory appDirectory = await getApplicationDocumentsDirectory();
return '${appDirectory.path}/${getIt.get<UserAccount>().id}/ChatBox'; return '${appDirectory.path}/${getIt.get<UserAccount>().id}/ChatBox';
} }
Future<String> getChatImageDir() async {
Directory appDirectory = await getApplicationDocumentsDirectory();
return '${appDirectory.path}/${getIt.get<UserAccount>().id}/images';
}
String getRandomFilename() {
final random = Random();
const availableChars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz';
String randomString = List.generate(
11, (index) => availableChars[random.nextInt(availableChars.length)])
.join();
return '$randomString.png';
}

View File

@ -78,3 +78,12 @@ String formatMessageDateTime(DateTime dateTime) {
return '$year-$month-$day $hour:$minute'; return '$year-$month-$day $hour:$minute';
} }
} }
String formatDirTime(DateTime dateTime) {
int year = dateTime.year;
String month =
dateTime.month < 0 ? '0${dateTime.month}' : '${dateTime.month}';
String day = dateTime.day < 0 ? '0${dateTime.day}' : '${dateTime.day}';
return '$year$month$day';
}

File diff suppressed because it is too large Load Diff