order chat tile by time, image message can be sent and showed on message screen
parent
277edd7f0e
commit
a4daec8f10
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
354
pubspec.lock
354
pubspec.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue