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) {
return ThemeData(
useMaterial3: true,
primaryColor: kPrimaryColor,
scaffoldBackgroundColor: Colors.white,
appBarTheme: appBarThemeLight,
@ -32,6 +33,7 @@ ThemeData lightThemeData(BuildContext context) {
ThemeData darkThemeData(BuildContext context) {
return ThemeData(
useMaterial3: true,
primaryColor: kPrimaryColor,
scaffoldBackgroundColor: Colors.black,
appBarTheme: appBarThemeDark,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,16 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:together_mobile/database/box_type.dart';
import 'package:together_mobile/models/apply_list_model.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:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
@ -51,6 +54,8 @@ class WebSocketManager extends ChangeNotifier {
receiveFriendAdded(data);
case 'friend-deleted':
receiveFriendDeleted(data);
case 'chat-image':
receiveImages(data);
}
}
@ -130,6 +135,9 @@ enum SocketStatus {
}
void receiveFriendMsg(Map<String, dynamic> msg) async {
print('=================收到了好友信息事件==================');
print(msg);
print('=======================================');
String senderId = msg['senderId'] as String;
late Box<MessageT> messageTBox;
try {
@ -151,11 +159,13 @@ void receiveFriendMsg(Map<String, dynamic> msg) async {
true,
false,
dateTime,
1,
),
);
} else {
chatSetting.isOpen = true;
chatSetting.latestDateTime = dateTime;
chatSetting.unreadCount++;
chatSettingBox.put(senderId, chatSetting);
}
@ -174,6 +184,9 @@ void receiveFriendMsg(Map<String, dynamic> msg) async {
}
void receiveApplyFriend(Map<String, dynamic> msg) {
print('=================收到了申请好友事件==================');
print(msg);
print('=======================================');
getIt.get<ApplyList>().addJson(msg);
}
@ -194,3 +207,14 @@ void receiveFriendDeleted(Map<String, dynamic> msg) {
getIt.get<Contact>().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',
name: 'AddContact',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) => const SearchNewScreen(),
builder: (context, state) => const SearchNewScreen() ,
routes: [
GoRoute(
path: 'friend',

View File

@ -27,24 +27,25 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> {
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) {
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);
List<Map<String, dynamic>> res = await Future.wait([
@ -120,8 +121,14 @@ class _ChatScreenState extends State<ChatScreen> {
valueListenable:
Hive.box<ChatSetting>('chat_setting').listenable(),
builder: (context, Box<ChatSetting> box, _) {
final openedChat =
final List<ChatSetting> openedChat =
box.values.where((element) => element.isOpen).toList();
// latestMsg on the top
openedChat.sort(
(a, b) => b.latestDateTime.compareTo(a.latestDateTime),
);
if (openedChat.isEmpty) {
return const Center(
child: Text(
@ -141,8 +148,9 @@ class _ChatScreenState extends State<ChatScreen> {
itemBuilder: (BuildContext context, int index) {
String contactId = openedChat[index].contactId;
String showedTime = formatTileDateTime(
openedChat[index].latestDateTime!,
openedChat[index].latestDateTime,
);
int unreadCount = openedChat[index].unreadCount;
return ValueListenableBuilder(
valueListenable:
@ -162,6 +170,7 @@ class _ChatScreenState extends State<ChatScreen> {
attachments: messageT.attachments,
dateTime: showedTime,
isShowTime: messageT.isShowTime,
unreadCount: unreadCount,
);
} else {
return ChatTile(
@ -173,6 +182,7 @@ class _ChatScreenState extends State<ChatScreen> {
attachments: const [],
dateTime: showedTime,
isShowTime: false,
unreadCount: 0,
);
}
},

View File

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

View File

@ -23,9 +23,10 @@ class ChatTile extends StatefulWidget {
required this.attachments,
required this.dateTime,
required this.isShowTime,
required this.unreadCount,
});
final int index;
final int index, unreadCount;
final String contactId, senderId, type, text, dateTime;
final List<String> attachments;
final bool isShowTime;
@ -62,7 +63,7 @@ class _ChatTileState extends State<ChatTile> {
foregroundColor: kContentColorDark,
backgroundColor: kPrimaryColor,
icon: Icons.remove_red_eye,
label: '隐藏',
label: '隐藏',
flex: 1,
),
],
@ -78,13 +79,13 @@ class _ChatTileState extends State<ChatTile> {
.friends[widget.contactId]!
.avatar
.isEmpty
? const BadgeAvatar(
count: 99,
? BadgeAvatar(
unreadCount: widget.unreadCount,
radius: 25,
backgroundImage: AssetImage('assets/images/user_3.png'),
backgroundImage: const AssetImage('assets/images/user_3.png'),
)
: BadgeAvatar(
count: 99,
unreadCount: widget.unreadCount,
radius: 25,
backgroundImage: CachedNetworkImageProvider(
'$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: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/request/server.dart';
class MessageBubble extends StatelessWidget {
class MessageBubble extends StatefulWidget {
const MessageBubble({
super.key,
required this.contactId,
@ -23,6 +26,70 @@ class MessageBubble extends StatelessWidget {
final bool isShowTime;
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
Widget build(BuildContext context) {
return Container(
@ -33,25 +100,26 @@ class MessageBubble extends StatelessWidget {
child: Column(
children: [
// message date time
if (isShowTime)
if (widget.isShowTime)
SizedBox(
height: 35.0,
child: Text(
dateTime,
widget.dateTime,
style: const TextStyle(
color: kUnActivatedColor,
),
),
),
Row(
textDirection:
senderId == contactId ? TextDirection.ltr : TextDirection.rtl,
textDirection: widget.senderId == widget.contactId
? TextDirection.ltr
: TextDirection.rtl,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
senderId == contactId
widget.senderId == widget.contactId
? getIt
.get<ContactAccountProfile>()
.friends[contactId]!
.friends[widget.contactId]!
.avatar
.isEmpty
? CircleAvatar(
@ -59,7 +127,7 @@ class MessageBubble extends StatelessWidget {
)
: CircleAvatar(
backgroundImage: CachedNetworkImageProvider(
'$avatarsUrl/${getIt.get<ContactAccountProfile>().friends[contactId]!.avatar}',
'$avatarsUrl/${getIt.get<ContactAccountProfile>().friends[widget.contactId]!.avatar}',
),
)
: CircleAvatar(
@ -72,7 +140,7 @@ class MessageBubble extends StatelessWidget {
),
Expanded(
child: Column(
crossAxisAlignment: senderId == contactId
crossAxisAlignment: widget.senderId == widget.contactId
? CrossAxisAlignment.start
: CrossAxisAlignment.end,
children: [
@ -83,12 +151,12 @@ class MessageBubble extends StatelessWidget {
// height: 5,
// ),
if (type == 'text/multipart')
if (widget.type == 'text/multipart')
// message box
Container(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
decoration: BoxDecoration(
color: senderId == contactId
color: widget.senderId == widget.contactId
? const Color.fromARGB(255, 241, 241, 241)
: kPrimaryColor,
borderRadius: BorderRadius.circular(10.0),
@ -97,31 +165,46 @@ class MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// text message content
Text(
text,
textWidthBasis: TextWidthBasis.longestLine,
),
if (widget.text.isNotEmpty)
Text(
widget.text,
textWidthBasis: TextWidthBasis.longestLine,
),
const SizedBox(
height: 10,
),
// image content if has
if (attachments.isNotEmpty)
...List.filled(
attachments.length,
Container(
margin: const EdgeInsets.only(
bottom: 10,
),
constraints: const BoxConstraints(
maxHeight: 100,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
),
child: Image.asset(
'assets/images/Logo_dark.png',
),
),
// image content if have
if (widget.attachments.isNotEmpty)
...List.generate(
widget.attachments.length,
(int index) {
return Container(
margin: const EdgeInsets.only(
bottom: 15,
),
constraints: const BoxConstraints(
maxHeight: 120,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
),
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:io';
import 'dart:typed_data';
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/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 MessageInputBox extends StatefulWidget {
@ -27,11 +33,14 @@ class MessageInputBox extends StatefulWidget {
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();
@ -85,9 +94,11 @@ class _MessageInputBoxState extends State<MessageInputBox> {
_hasMsg = true;
});
} else {
setState(() {
_hasMsg = false;
});
if (_imageFileList.isEmpty) {
setState(() {
_hasMsg = false;
});
}
}
},
minLines: null,
@ -132,15 +143,26 @@ class _MessageInputBoxState extends State<MessageInputBox> {
children: [
InputIconButton(
onPressed: () {},
icon: const Icon(Icons.insert_photo),
icon: const Icon(Icons.mic),
),
InputIconButton(
onPressed: () {},
icon: const Icon(Icons.call),
),
InputIconButton(
onPressed: () {},
icon: const Icon(Icons.mic),
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: () {},
@ -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) {
return;
}
DateTime now = DateTime.now();
late bool isShowTime;
List<String> attachments = [];
int messageCount = _messageTBox.length;
if (messageCount == 0) {
@ -174,28 +220,25 @@ class _MessageInputBoxState extends State<MessageInputBox> {
var differenceInMinutes = now.difference(lastTime).inMinutes;
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 = {
'event': 'one-to-one-chat',
'type': 'text/multipart',
'senderId': getIt.get<UserAccount>().id,
'receiverId': widget.contactId,
'text': _controller.text,
'attachments': [],
'attachments': attachments,
'dateTime': now.toString(),
'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(
MessageT(
msg['senderId']! as String,
@ -203,17 +246,50 @@ class _MessageInputBoxState extends State<MessageInputBox> {
msg['type']! as String,
now,
isShowTime,
[],
attachments,
),
);
Future.delayed(
const Duration(milliseconds: 50),
() => widget.scrollController.animateTo(
widget.scrollController.position.maxScrollExtent,
0,
duration: const Duration(milliseconds: 500),
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> {
ScrollController _controller = ScrollController();
@override
void initState() {
super.initState();
Future.delayed(
const Duration(microseconds: 500),
() => _controller.jumpTo(_controller.position.maxScrollExtent),
);
}
final ScrollController _controller = ScrollController();
final Box<ChatSetting> _chatSettingBox =
Hive.box<ChatSetting>('chat_setting');
@override
void dispose() {
@ -77,46 +70,64 @@ class _MessageScreenState extends State<MessageScreen> {
body: Column(
children: [
Expanded(
child: ValueListenableBuilder(
valueListenable: Hive.box<MessageT>('message_${widget.contactId}')
.listenable(),
builder: (context, value, _) {
Future.delayed(
const Duration(
milliseconds: 50,
),
() => _controller.animateTo(
_controller.position.maxScrollExtent,
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
),
);
child: Align(
alignment: Alignment.topCenter,
child: ValueListenableBuilder(
valueListenable:
Hive.box<MessageT>('message_${widget.contactId}')
.listenable(),
builder: (context, value, _) {
// Set unreadCount to 0 when at message screen
ChatSetting? chatSetting =
_chatSettingBox.get(widget.contactId);
return ListView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
controller: _controller,
itemCount:
Hive.box<MessageT>('message_${widget.contactId}').length,
itemBuilder: (context, index) {
Box<MessageT> messageTBox =
Hive.box<MessageT>('message_${widget.contactId}');
// whethe it is a brand new chat
if (chatSetting != null) {
chatSetting.unreadCount = 0;
_chatSettingBox.put(widget.contactId, chatSetting);
}
MessageT messageT = messageTBox.getAt(index)!;
Future.delayed(
const Duration(
milliseconds: 100,
),
() => _controller.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
),
);
return MessageBubble(
contactId: widget.contactId,
senderId: messageT.senderId,
dateTime: formatMessageDateTime(messageT.dateTime),
isShowTime: messageT.isShowTime,
type: messageT.type,
text: messageT.text,
attachments: messageT.attachments,
);
},
);
},
return ListView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
controller: _controller,
shrinkWrap: true,
reverse: true,
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(

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:path_provider/path_provider.dart';
@ -15,7 +16,22 @@ Future<String> getAvatarDir() async {
return '${appDirectory.path}/${getIt.get<UserAccount>().id}/images/avatars';
}
Future<String> getBoxPath() async {
Future<String> getBoxDir() async {
Directory appDirectory = await getApplicationDocumentsDirectory();
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';
}
}
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