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) {
|
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,
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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']);
|
||||||
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}',
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
354
pubspec.lock
354
pubspec.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue