implement slice transmitting of chat image
parent
849e7d5067
commit
830a16bd27
assets/images
lib
models
request
router
screens
chat
friend_profile/components
message
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -39,7 +39,7 @@ class ChatSetting {
|
|||
@HiveType(typeId: 1)
|
||||
class MessageT {
|
||||
@HiveField(0)
|
||||
int msgId;
|
||||
String msgId;
|
||||
|
||||
@HiveField(1)
|
||||
String senderId;
|
||||
|
@ -70,9 +70,37 @@ class MessageT {
|
|||
);
|
||||
}
|
||||
|
||||
@HiveType(typeId: 2)
|
||||
class AttachmentProgress {
|
||||
// denote the number of chunk has sent or recieved.
|
||||
// if 0, means no chunk has received or sent.
|
||||
@HiveField(0)
|
||||
int hasChunkNum;
|
||||
|
||||
@HiveField(1)
|
||||
int totalChunkNum;
|
||||
|
||||
@HiveField(2)
|
||||
double progress;
|
||||
|
||||
@HiveField(3)
|
||||
bool isValid;
|
||||
|
||||
@HiveField(4)
|
||||
bool isPause;
|
||||
|
||||
AttachmentProgress(
|
||||
this.hasChunkNum,
|
||||
this.totalChunkNum,
|
||||
this.progress,
|
||||
this.isValid,
|
||||
this.isPause,
|
||||
);
|
||||
}
|
||||
|
||||
/// message type:
|
||||
/// 1. text/multipart: text with/without images or videos
|
||||
/// 2. voice
|
||||
/// 3. video
|
||||
/// 4. voice-call
|
||||
/// 5. video-call
|
||||
/// 5. video-call
|
||||
|
|
|
@ -69,7 +69,7 @@ class MessageTAdapter extends TypeAdapter<MessageT> {
|
|||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return MessageT(
|
||||
fields[0] as int,
|
||||
fields[0] as String,
|
||||
fields[1] as String,
|
||||
fields[2] == null ? '' : fields[2] as String,
|
||||
fields[3] as String,
|
||||
|
@ -109,3 +109,49 @@ class MessageTAdapter extends TypeAdapter<MessageT> {
|
|||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class AttachmentProgressAdapter extends TypeAdapter<AttachmentProgress> {
|
||||
@override
|
||||
final int typeId = 2;
|
||||
|
||||
@override
|
||||
AttachmentProgress read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AttachmentProgress(
|
||||
fields[0] as int,
|
||||
fields[1] as int,
|
||||
fields[2] as double,
|
||||
fields[3] as bool,
|
||||
fields[4] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AttachmentProgress obj) {
|
||||
writer
|
||||
..writeByte(5)
|
||||
..writeByte(0)
|
||||
..write(obj.hasChunkNum)
|
||||
..writeByte(1)
|
||||
..write(obj.totalChunkNum)
|
||||
..writeByte(2)
|
||||
..write(obj.progress)
|
||||
..writeByte(3)
|
||||
..write(obj.isValid)
|
||||
..writeByte(4)
|
||||
..write(obj.isPause);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AttachmentProgressAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
|
|
@ -32,14 +32,19 @@ class HiveDatabase {
|
|||
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
|
||||
compactionStrategy: (entries, deletedEntries) => entries > 200,
|
||||
);
|
||||
await Hive.openBox<int>('msg_index_${chatBox.contactId}');
|
||||
}
|
||||
|
||||
await Hive.openBox<AttachmentProgress>('attachment_send');
|
||||
await Hive.openBox<AttachmentProgress>('attachment_receive');
|
||||
|
||||
_isInitialised = true;
|
||||
}
|
||||
|
||||
static void registerAdapter() {
|
||||
Hive.registerAdapter(ChatSettingAdapter());
|
||||
Hive.registerAdapter(MessageTAdapter());
|
||||
Hive.registerAdapter(AttachmentProgressAdapter());
|
||||
}
|
||||
|
||||
static Future<Box<MessageT>> openMessageBox(String contactId) async {
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:together_mobile/database/hive_database.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/status.dart' as status;
|
||||
|
||||
import 'package:together_mobile/models/route_state_model.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:together_mobile/notification_api.dart';
|
||||
import 'package:together_mobile/utils/ws_receive_callback.dart';
|
||||
|
||||
enum SocketStatus {
|
||||
connected,
|
||||
|
@ -23,6 +17,8 @@ enum SocketStatus {
|
|||
closed,
|
||||
}
|
||||
|
||||
// const chunkSize = 1024 * 1024;
|
||||
|
||||
class WebSocketManager extends ChangeNotifier {
|
||||
late Uri wsUrl;
|
||||
late WebSocketChannel channel;
|
||||
|
@ -35,6 +31,7 @@ class WebSocketManager extends ChangeNotifier {
|
|||
int reconnectTimes = 0;
|
||||
Duration heartBeatTimeout = const Duration(seconds: 4);
|
||||
Duration reconnectTimeout = const Duration(seconds: 3);
|
||||
Map<String, Timer> sendImageTimer = {};
|
||||
|
||||
void connect(String userId, bool isReconnect) {
|
||||
id = userId;
|
||||
|
@ -66,7 +63,7 @@ class WebSocketManager extends ChangeNotifier {
|
|||
reconnectTimes = 0;
|
||||
}
|
||||
|
||||
void onData(jsonData) {
|
||||
void onData(jsonData) async {
|
||||
// If socket can receive msg, that means connection is estabilished
|
||||
socketStatus = SocketStatus.connected;
|
||||
notifyListeners();
|
||||
|
@ -79,10 +76,12 @@ class WebSocketManager extends ChangeNotifier {
|
|||
|
||||
heartBeatInspect();
|
||||
|
||||
Map<String, dynamic> data = json.decode(jsonData);
|
||||
// Map<String, dynamic> data = json.decode(jsonData);
|
||||
Map<String, dynamic> data =
|
||||
await compute((message) => json.decode(message), jsonData);
|
||||
switch (data['event']) {
|
||||
case 'friend-chat-msg':
|
||||
receiveFriendMsg(data, true);
|
||||
await receiveFriendMsg(data, true);
|
||||
case 'apply-friend':
|
||||
receiveApplyFriend(data);
|
||||
case 'friend-added':
|
||||
|
@ -95,6 +94,10 @@ class WebSocketManager extends ChangeNotifier {
|
|||
receiveGroupChatCreation(data);
|
||||
case 'group-chat-msg':
|
||||
receiveGroupChatMsg(data, true);
|
||||
case 'pull-chat-image':
|
||||
receivePullChatImage(data);
|
||||
case 'chat-image-send-ok':
|
||||
receiveCheckChatImage(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,245 +201,50 @@ class WebSocketManager extends ChangeNotifier {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void receiveFriendMsg(Map<String, dynamic> msg, bool isShowNotification) async {
|
||||
print('=================收到了好友信息事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String senderId = msg['senderId'] as String;
|
||||
Box<ChatSetting> chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
||||
Box<MessageT> messageTBox = await HiveDatabase.openMessageBox(senderId);
|
||||
ChatSetting? chatSetting = chatSettingBox.get(senderId);
|
||||
DateTime dateTime = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
if (chatSetting == null) {
|
||||
chatSettingBox.put(
|
||||
senderId,
|
||||
ChatSetting(
|
||||
senderId,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
dateTime,
|
||||
1,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
chatSetting.isOpen = true;
|
||||
chatSetting.latestDateTime = dateTime;
|
||||
chatSetting.unreadCount++;
|
||||
chatSettingBox.put(senderId, chatSetting);
|
||||
}
|
||||
|
||||
List<String> attachments = List.from(msg['attachments']);
|
||||
final DateTime now = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
messageTBox.add(
|
||||
MessageT(
|
||||
msg['msgId'] as int,
|
||||
senderId,
|
||||
msg['text'],
|
||||
msg['type'],
|
||||
now,
|
||||
msg['isShowTime'],
|
||||
attachments,
|
||||
),
|
||||
);
|
||||
|
||||
if (!isShowNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
String name = getIt.get<Contact>().friends[senderId]!.friendRemark.isEmpty
|
||||
? getIt.get<ContactAccountProfile>().friends[senderId]!.nickname
|
||||
: getIt.get<Contact>().friends[senderId]!.friendRemark;
|
||||
|
||||
String routeName = getIt.get<RouteState>().currentPathName;
|
||||
|
||||
String avatar = getIt.get<ContactAccountProfile>().friends[senderId]!.avatar;
|
||||
|
||||
if (!getIt.get<RouteState>().isVisible) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
);
|
||||
} else if (routeName == 'Message') {
|
||||
if (!getIt.get<RouteState>().query.containsValue(senderId)) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
);
|
||||
void addSendImageTimer(String filename, int totalChunkNum) {
|
||||
if (sendImageTimer.containsKey(filename)) {
|
||||
sendImageTimer[filename]!.cancel();
|
||||
sendImageTimer.remove(filename);
|
||||
}
|
||||
} else if (routeName != 'Chat') {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
|
||||
sendImageTimer[filename] = Timer(
|
||||
heartBeatTimeout,
|
||||
() async {
|
||||
late Box<AttachmentProgress> attachmentLoadingBox;
|
||||
try {
|
||||
attachmentLoadingBox =
|
||||
Hive.box<AttachmentProgress>('attachment_receive');
|
||||
} catch (_) {
|
||||
attachmentLoadingBox =
|
||||
await Hive.openBox<AttachmentProgress>('attachment_receive');
|
||||
}
|
||||
|
||||
AttachmentProgress? at = attachmentLoadingBox.get(filename);
|
||||
|
||||
if (at == null) {
|
||||
attachmentLoadingBox.put(
|
||||
filename,
|
||||
AttachmentProgress(
|
||||
0,
|
||||
totalChunkNum,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
at.isPause = true;
|
||||
attachmentLoadingBox.put(filename, at);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void receiveApplyFriend(Map<String, dynamic> msg) {
|
||||
print('=================收到了申请好友事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
getIt.get<ApplyList>().addJson(msg);
|
||||
}
|
||||
|
||||
void receiveFriendAdded(Map<String, dynamic> msg) {
|
||||
print('=================收到了申请好友通过事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
getIt.get<Contact>().addFriend(msg['friendId'], msg['setting']);
|
||||
getIt
|
||||
.get<ContactAccountProfile>()
|
||||
.addFriendAccountProfile(msg['friendId'], msg['accountProfile']);
|
||||
}
|
||||
|
||||
void receiveFriendDeleted(Map<String, dynamic> msg) {
|
||||
print('=================收到了解除好友事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
getIt.get<Contact>().removeFriend(msg['friendId']);
|
||||
getIt.get<ContactAccountProfile>().removeFriend(msg['friendId']);
|
||||
}
|
||||
|
||||
void receiveChatImages(Map<String, dynamic> msg) async {
|
||||
print('=================收到了聊天图片事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String chatImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
File file = File('$chatImageDir/${msg['filename']}');
|
||||
if (await file.exists()) {
|
||||
return;
|
||||
} else {
|
||||
await file.create(recursive: true);
|
||||
await file.writeAsBytes(List<int>.from(msg['bytes']));
|
||||
}
|
||||
}
|
||||
|
||||
void receiveGroupChatCreation(Map<String, dynamic> msg) {
|
||||
print('=================收到了群聊邀请事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String groupChatId = msg['groupChat']['id'];
|
||||
getIt.get<Contact>().addGroupChat(groupChatId);
|
||||
getIt.get<ContactAccountProfile>().addGroupChatProfile(groupChatId, msg);
|
||||
}
|
||||
|
||||
void receiveGroupChatMsg(
|
||||
Map<String, dynamic> msg, bool isShowNotification) async {
|
||||
print('=================收到了群聊信息事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String senderId = msg['senderId'] as String;
|
||||
String groupChatId = msg['groupChatId'] as String;
|
||||
Box<ChatSetting> chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
||||
Box<MessageT> messageTBox = await HiveDatabase.openMessageBox(groupChatId);
|
||||
ChatSetting? chatSetting = chatSettingBox.get(groupChatId);
|
||||
DateTime dateTime = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
getIt.get<ContactAccountProfile>().addGroupChatMemberProfile(
|
||||
groupChatId,
|
||||
msg['senderId'],
|
||||
{
|
||||
'avatar': msg['avatar'],
|
||||
'nickname': msg['nickname'],
|
||||
'remarkInGroupChat': msg['remarkInGroupChat'],
|
||||
},
|
||||
);
|
||||
|
||||
if (chatSetting == null) {
|
||||
chatSettingBox.put(
|
||||
groupChatId,
|
||||
ChatSetting(
|
||||
groupChatId,
|
||||
1,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
dateTime,
|
||||
1,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
chatSetting.isOpen = true;
|
||||
chatSetting.latestDateTime = dateTime;
|
||||
chatSetting.unreadCount++;
|
||||
chatSettingBox.put(groupChatId, chatSetting);
|
||||
}
|
||||
|
||||
List<String> attachments = List.from(msg['attachments']);
|
||||
final DateTime now = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
messageTBox.add(
|
||||
MessageT(
|
||||
msg['msgId'] as int,
|
||||
senderId,
|
||||
msg['text'],
|
||||
msg['type'],
|
||||
now,
|
||||
msg['isShowTime'],
|
||||
attachments,
|
||||
),
|
||||
);
|
||||
|
||||
if (!isShowNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
String avatar =
|
||||
getIt.get<ContactAccountProfile>().groupChats[groupChatId]!.avatar;
|
||||
late String name;
|
||||
|
||||
if (getIt.get<Contact>().friends.containsKey(senderId)) {
|
||||
if (getIt.get<Contact>().friends[senderId]!.friendRemark.isNotEmpty) {
|
||||
name = getIt.get<Contact>().friends[senderId]!.friendRemark;
|
||||
} else if ((msg['remarkInGroupChat'] as String).isNotEmpty) {
|
||||
name = msg['remarkInGroupChat'];
|
||||
} else {
|
||||
name = msg['nickname'];
|
||||
void removeSendImageTimer(String filename) {
|
||||
if (sendImageTimer.containsKey(filename)) {
|
||||
sendImageTimer[filename]!.cancel();
|
||||
}
|
||||
} else {
|
||||
name = (msg['remarkInGroupChat'] as String).isNotEmpty
|
||||
? msg['remarkInGroupChat']
|
||||
: msg['nickname'];
|
||||
}
|
||||
|
||||
String routeName = getIt.get<RouteState>().currentPathName;
|
||||
|
||||
if (!getIt.get<RouteState>().isVisible) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
groupChatId: groupChatId,
|
||||
);
|
||||
} else if (routeName == 'Message') {
|
||||
if (!getIt.get<RouteState>().query.containsValue(senderId)) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
groupChatId: groupChatId,
|
||||
);
|
||||
}
|
||||
} else if (routeName != 'Chat') {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
groupChatId: groupChatId,
|
||||
);
|
||||
sendImageTimer.remove(filename);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,3 +12,14 @@ Future<Map<String, dynamic>> getUnreceivedMsg(String userId) async {
|
|||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> uploadChatAttachment(
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Response response = await request.post(
|
||||
'/message/attachment',
|
||||
data: data,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
|
|
@ -39,12 +39,9 @@ final chatRouter = GoRoute(
|
|||
parentNavigatorKey: rootNavigatorKey,
|
||||
builder: (context, state) {
|
||||
Map<String, Object> extra = state.extra as Map<String, Object>;
|
||||
for (var a in extra['attachmentItems']! as List<AttachmentItem>) {
|
||||
print(a.id);
|
||||
}
|
||||
return ImageViewScreen(
|
||||
attachmentItems:
|
||||
extra['attachmentItems']! as List<AttachmentItem>,
|
||||
attachments:
|
||||
extra['attachments']! as List<String>,
|
||||
initialIndex: extra['index']! as int,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:together_mobile/database/hive_database.dart';
|
|||
import 'package:together_mobile/request/message.dart';
|
||||
|
||||
import 'package:together_mobile/screens/chat/components/group_chat_chat_tile.dart';
|
||||
import 'package:together_mobile/utils/ws_receive_callback.dart';
|
||||
import 'components/friend_chat_tile.dart';
|
||||
import 'components/add_menu.dart';
|
||||
import 'package:together_mobile/database/box_type.dart';
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import 'package:together_mobile/common/constants.dart';
|
||||
import 'package:together_mobile/database/box_type.dart';
|
||||
import 'package:together_mobile/database/hive_database.dart';
|
||||
|
||||
class RowFloatingButtons extends StatelessWidget {
|
||||
|
@ -75,7 +73,7 @@ class RowFloatingButtons extends StatelessWidget {
|
|||
),
|
||||
FloatingActionButton(
|
||||
onPressed: () async {
|
||||
HiveDatabase.openMessageBox(friendId);
|
||||
await HiveDatabase.openMessageBox(friendId);
|
||||
// ignore: use_build_context_synchronously
|
||||
context.pushReplacementNamed(
|
||||
'Message',
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
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';
|
||||
|
||||
class AttachmentContainer extends StatefulWidget {
|
||||
const AttachmentContainer({
|
||||
super.key,
|
||||
required this.isMyself,
|
||||
required this.filename,
|
||||
required this.onTapImage,
|
||||
});
|
||||
|
||||
final bool isMyself;
|
||||
final String filename;
|
||||
final Function(String) onTapImage;
|
||||
|
||||
@override
|
||||
State<AttachmentContainer> createState() => _AttachmentContainerState();
|
||||
}
|
||||
|
||||
class _AttachmentContainerState extends State<AttachmentContainer>
|
||||
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this);
|
||||
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 15,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 150,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: widget.isMyself
|
||||
? Hive.box<AttachmentProgress>('attachment_send')
|
||||
.listenable(keys: [widget.filename])
|
||||
: Hive.box<AttachmentProgress>('attachment_receive')
|
||||
.listenable(keys: [widget.filename]),
|
||||
builder: (context, apBox, _) {
|
||||
double progress = apBox.get(widget.filename)!.progress;
|
||||
String filePath =
|
||||
'${getIt.get<UserProfile>().baseImageDir}/${widget.filename}';
|
||||
bool isFileExists = File(filePath).existsSync();
|
||||
|
||||
_controller.animateTo(
|
||||
progress,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.onTapImage(widget.filename);
|
||||
},
|
||||
child: progress == 1.0 || isFileExists
|
||||
? Image.file(File(filePath))
|
||||
: Image.asset('assets/images/loading.gif'),
|
||||
),
|
||||
if (progress < 1)
|
||||
Positioned.fill(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
constraints: const BoxConstraints.expand(),
|
||||
color: const Color.fromARGB(255, 32, 32, 32)
|
||||
.withOpacity(0.3),
|
||||
child: Text(
|
||||
'${(_animation.value * 100).toStringAsFixed(0)}%',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
color: kUnAvailableColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
CircularProgressIndicator(
|
||||
value: _animation.value,
|
||||
color: kSecondaryColor,
|
||||
strokeWidth: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
@ -12,7 +9,7 @@ import 'package:together_mobile/models/contact_model.dart';
|
|||
import 'package:together_mobile/models/init_get_it.dart';
|
||||
import 'package:together_mobile/models/user_model.dart';
|
||||
import 'package:together_mobile/request/server.dart';
|
||||
import 'package:together_mobile/screens/message/image_view_screen/image_view_screen.dart';
|
||||
import 'package:together_mobile/screens/message/components/attachment_container.dart';
|
||||
|
||||
class FriendMessageBubble extends StatefulWidget {
|
||||
const FriendMessageBubble({
|
||||
|
@ -26,14 +23,13 @@ class FriendMessageBubble extends StatefulWidget {
|
|||
required this.isShowTime,
|
||||
required this.text,
|
||||
required this.attachments,
|
||||
required this.attachmentItems,
|
||||
required this.allAttachments,
|
||||
});
|
||||
|
||||
final int index, length;
|
||||
final String contactId, senderId, type, dateTime, text;
|
||||
final bool isShowTime;
|
||||
final List<String> attachments;
|
||||
final List<AttachmentItem> attachmentItems;
|
||||
final List<String> attachments, allAttachments;
|
||||
|
||||
@override
|
||||
State<FriendMessageBubble> createState() => _FriendMessageBubbleState();
|
||||
|
@ -41,65 +37,14 @@ class FriendMessageBubble extends StatefulWidget {
|
|||
|
||||
class _FriendMessageBubbleState extends State<FriendMessageBubble> {
|
||||
// add late here so you can access widget instance
|
||||
List<bool> _isImagesLoaded = [];
|
||||
final List<Timer> _timerList = [];
|
||||
late bool _isHideMsg;
|
||||
|
||||
@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();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
||||
late final chatSetting = chatSettingBox.get(widget.contactId);
|
||||
_isHideMsg = chatSetting!.isHideMsg;
|
||||
|
||||
if (_isHideMsg) {
|
||||
if (widget.index + 1 == widget.length) {
|
||||
_isHideMsg = false;
|
||||
Timer(
|
||||
const Duration(milliseconds: 3500),
|
||||
() {
|
||||
setState(() {
|
||||
_isHideMsg = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var element in _timerList) {
|
||||
element.cancel();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -170,7 +115,6 @@ class _FriendMessageBubbleState extends State<FriendMessageBubble> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_isHideMsg) const Text('消息已隐藏'),
|
||||
|
||||
// text message content
|
||||
if (widget.text.isNotEmpty && !_isHideMsg)
|
||||
Text(
|
||||
|
@ -180,58 +124,16 @@ class _FriendMessageBubbleState extends State<FriendMessageBubble> {
|
|||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
// image content if have
|
||||
// image content if has
|
||||
if (widget.attachments.isNotEmpty && !_isHideMsg)
|
||||
...List.generate(
|
||||
widget.attachments.length,
|
||||
(int index) {
|
||||
int attachmentItemIndex =
|
||||
widget.attachmentItems.indexWhere(
|
||||
(element) =>
|
||||
element.resource ==
|
||||
widget.attachments[index],
|
||||
);
|
||||
int heroTagId = widget
|
||||
.attachmentItems[attachmentItemIndex].id;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 15,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 120,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: _isImagesLoaded[index]
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
_onTapImage(
|
||||
context,
|
||||
widget.attachments[index],
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: heroTagId,
|
||||
child: 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,
|
||||
),
|
||||
),
|
||||
String filename = widget.attachments[index];
|
||||
return AttachmentContainer(
|
||||
isMyself: !isFriend,
|
||||
filename: filename,
|
||||
onTapImage: _onTapImage,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -261,15 +163,15 @@ class _FriendMessageBubbleState extends State<FriendMessageBubble> {
|
|||
);
|
||||
}
|
||||
|
||||
void _onTapImage(BuildContext context, String attachment) {
|
||||
int attachmentIndex = widget.attachmentItems.indexWhere(
|
||||
(element) => element.resource == attachment,
|
||||
void _onTapImage(String attachment) {
|
||||
int attachmentIndex = widget.allAttachments.indexWhere(
|
||||
(element) => element == attachment,
|
||||
);
|
||||
|
||||
context.pushNamed(
|
||||
'ImageView',
|
||||
extra: {
|
||||
'attachmentItems': widget.attachmentItems,
|
||||
'attachments': widget.allAttachments,
|
||||
'index': attachmentIndex,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -16,23 +16,9 @@ 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 'package:together_mobile/utils/ws_receive_callback.dart';
|
||||
import 'input_icon_button.dart';
|
||||
|
||||
class SendMessage {
|
||||
final int type;
|
||||
final List<String> receivers;
|
||||
final List<String> attachments;
|
||||
final String dir;
|
||||
|
||||
SendMessage(this.type, this.receivers, this.attachments, this.dir);
|
||||
|
||||
SendMessage.fromJson(Map<String, dynamic> json)
|
||||
: type = json['type'],
|
||||
receivers = json['receivers'],
|
||||
attachments = json['attachments'],
|
||||
dir = json['dir'];
|
||||
}
|
||||
|
||||
class MessageInputBox extends StatefulWidget {
|
||||
const MessageInputBox({
|
||||
super.key,
|
||||
|
@ -54,7 +40,7 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
final ImagePicker _picker = ImagePicker();
|
||||
bool _hasMsg = false;
|
||||
|
||||
late Box<ChatSetting> _chatBox;
|
||||
late Box<ChatSetting> _chatSettingBox;
|
||||
late Box<MessageT> _messageTBox;
|
||||
|
||||
List<XFile> _imageFileList = [];
|
||||
|
@ -62,7 +48,7 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_chatBox = Hive.box<ChatSetting>('chat_setting');
|
||||
_chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
||||
_messageTBox = Hive.box<MessageT>('message_${widget.contactId}');
|
||||
}
|
||||
|
||||
|
@ -231,20 +217,43 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
String text = _controller.text;
|
||||
String type = 'text/multipart';
|
||||
List<String> attachments = [];
|
||||
Box<AttachmentProgress> apsBox = Hive.box('attachment_send');
|
||||
|
||||
var chatSetting = _chatSettingBox.get(widget.contactId);
|
||||
if (chatSetting == null) {
|
||||
_chatSettingBox.put(
|
||||
widget.contactId,
|
||||
ChatSetting(
|
||||
widget.contactId,
|
||||
widget.chatType,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
now,
|
||||
0,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
chatSetting.latestDateTime = now;
|
||||
chatSetting.unreadCount = 0;
|
||||
chatSetting.isOpen = true;
|
||||
_chatSettingBox.put(widget.contactId, chatSetting);
|
||||
}
|
||||
|
||||
String dirTime = formatDirTime(now);
|
||||
|
||||
if (_imageFileList.isNotEmpty) {
|
||||
String dirTime = formatDirTime(now);
|
||||
for (var i = 0; i < _imageFileList.length; i++) {
|
||||
attachments.add('$dirTime/${getRandomFilename()}');
|
||||
String filename = '$dirTime/${getRandomFilename()}';
|
||||
int totalChunkNum =
|
||||
((await _imageFileList[i].length()) / chunkSize).ceil();
|
||||
attachments.add(filename);
|
||||
apsBox.put(
|
||||
filename, AttachmentProgress(0, totalChunkNum, 0, true, false));
|
||||
}
|
||||
}
|
||||
|
||||
late int msgId;
|
||||
if (_messageTBox.length == 0) {
|
||||
msgId = 0;
|
||||
} else {
|
||||
msgId = _messageTBox.length - 1;
|
||||
}
|
||||
final msgId = formatMsgIDFromTime(now);
|
||||
|
||||
_messageTBox.add(
|
||||
MessageT(
|
||||
|
@ -258,6 +267,10 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
),
|
||||
);
|
||||
|
||||
Box<int> msgIndexBox =
|
||||
await Hive.openBox<int>('msg_index_${widget.contactId}');
|
||||
msgIndexBox.put(msgId, _messageTBox.length - 1);
|
||||
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 50),
|
||||
() => widget.scrollController.animateTo(
|
||||
|
@ -278,32 +291,31 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
};
|
||||
|
||||
if (widget.chatType == 0) {
|
||||
if (attachments.isNotEmpty) {
|
||||
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
for (var i = 0; i < attachments.length; i++) {
|
||||
final dir = Directory('$baseImageDir/$dirTime');
|
||||
if (!(await dir.exists())) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
await _imageFileList[i].saveTo('$baseImageDir/${attachments[i]}');
|
||||
}
|
||||
}
|
||||
msg['event'] = 'friend-chat-msg';
|
||||
msg['receiverId'] = widget.contactId;
|
||||
getIt.get<WebSocketManager>().channel.sink.add(json.encode(msg));
|
||||
if (attachments.isNotEmpty) {
|
||||
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
List<String> encodedDatas = await compute(
|
||||
bytes2json,
|
||||
(
|
||||
0,
|
||||
[widget.contactId],
|
||||
attachments,
|
||||
_imageFileList,
|
||||
baseImageDir,
|
||||
),
|
||||
);
|
||||
|
||||
for (final data in encodedDatas) {
|
||||
getIt.get<WebSocketManager>().channel.sink.add(data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
List<String> receiverIds = getIt
|
||||
.get<ContactAccountProfile>()
|
||||
.groupChats[widget.contactId]!
|
||||
.members;
|
||||
|
||||
if (attachments.isNotEmpty) {
|
||||
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
for (var i = 0; i < attachments.length; i++) {
|
||||
await _imageFileList[i].saveTo('$baseImageDir/${attachments[i]}');
|
||||
}
|
||||
}
|
||||
receiverIds.remove(senderId);
|
||||
msg['event'] = 'group-chat-msg';
|
||||
msg['groupChatId'] = widget.contactId;
|
||||
|
@ -313,22 +325,6 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
getIt.get<Contact>().groupChats[widget.contactId]!.remarkInGroupChat;
|
||||
msg['avatar'] = getIt.get<UserProfile>().avatar;
|
||||
getIt.get<WebSocketManager>().channel.sink.add(json.encode(msg));
|
||||
if (attachments.isNotEmpty) {
|
||||
List<String> encodedDatas = await compute(
|
||||
bytes2json,
|
||||
(
|
||||
1,
|
||||
receiverIds,
|
||||
attachments,
|
||||
_imageFileList,
|
||||
baseImageDir,
|
||||
),
|
||||
);
|
||||
|
||||
for (final data in encodedDatas) {
|
||||
getIt.get<WebSocketManager>().channel.sink.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_controller.text = '';
|
||||
|
@ -336,27 +332,6 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
_imageFileList = [];
|
||||
_hasMsg = false;
|
||||
});
|
||||
|
||||
var chatSetting = _chatBox.get(widget.contactId);
|
||||
if (chatSetting == null) {
|
||||
_chatBox.put(
|
||||
widget.contactId,
|
||||
ChatSetting(
|
||||
widget.contactId,
|
||||
widget.chatType,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
now,
|
||||
0,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
chatSetting.latestDateTime = now;
|
||||
chatSetting.unreadCount = 0;
|
||||
chatSetting.isOpen = true;
|
||||
_chatBox.put(widget.contactId, chatSetting);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isShowTime(DateTime now) {
|
||||
|
@ -372,44 +347,3 @@ class _MessageInputBoxState extends State<MessageInputBox> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> bytes2json(
|
||||
(
|
||||
int,
|
||||
List<String>,
|
||||
List<String>,
|
||||
List<XFile>,
|
||||
String,
|
||||
) args) async {
|
||||
List<String> encodedJson = [];
|
||||
for (var i = 0; i < args.$3.length; i++) {
|
||||
Uint8List bytes = await args.$4[i].readAsBytes();
|
||||
File file = File('${args.$5}/${args.$3[i]}');
|
||||
await file.create(recursive: true);
|
||||
await file.writeAsBytes(bytes);
|
||||
if (args.$1 == 0) {
|
||||
encodedJson.add(
|
||||
json.encode(
|
||||
{
|
||||
'event': 'chat-image',
|
||||
'receiverId': args.$2[0],
|
||||
'filename': args.$3[i],
|
||||
'bytes': bytes,
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
encodedJson.add(
|
||||
json.encode(
|
||||
{
|
||||
'event': 'chat-image',
|
||||
'receiverIds': args.$2,
|
||||
'filename': args.$3[i],
|
||||
'bytes': bytes,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return encodedJson;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import 'package:together_mobile/models/route_state_model.dart';
|
|||
import 'package:together_mobile/utils/format_datetime.dart';
|
||||
import 'components/friend_message_bubble.dart';
|
||||
import 'components/message_input_box.dart';
|
||||
import 'image_view_screen/image_view_screen.dart';
|
||||
|
||||
class FriendMessageScreen extends StatefulWidget {
|
||||
const FriendMessageScreen({
|
||||
|
@ -110,6 +109,7 @@ class _FriendMessageScreenState extends State<FriendMessageScreen> {
|
|||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
// addAutomaticKeepAlives: false,
|
||||
controller: _controller,
|
||||
shrinkWrap: true,
|
||||
reverse: true,
|
||||
|
@ -123,24 +123,13 @@ class _FriendMessageScreenState extends State<FriendMessageScreen> {
|
|||
MessageT messageT = messageTBox.getAt(i)!;
|
||||
|
||||
List<String> allAttachments = [];
|
||||
// Do not reverse the list, cause what i want is
|
||||
// rigth slide to next image, left to last otherwise
|
||||
for (var element in messageTBox.values) {
|
||||
if (element.attachments.isNotEmpty) {
|
||||
allAttachments.addAll(element.attachments);
|
||||
}
|
||||
}
|
||||
// Do not reverse the list, cause what i want is
|
||||
// rigth slide to next image, left to last otherwise
|
||||
// allAttachments = List.from(allAttachments.reversed);
|
||||
|
||||
List<AttachmentItem> attachmentItems = List.generate(
|
||||
allAttachments.length,
|
||||
(int index) {
|
||||
return AttachmentItem(
|
||||
id: index,
|
||||
resource: allAttachments[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return FriendMessageBubble(
|
||||
key: ValueKey(messageT.msgId),
|
||||
|
@ -153,7 +142,7 @@ class _FriendMessageScreenState extends State<FriendMessageScreen> {
|
|||
type: messageT.type,
|
||||
text: messageT.text,
|
||||
attachments: messageT.attachments,
|
||||
attachmentItems: attachmentItems,
|
||||
allAttachments: allAttachments,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -27,7 +27,7 @@ class ImageViewScreen extends StatefulWidget {
|
|||
this.minScale,
|
||||
this.maxScale,
|
||||
this.initialIndex = 0,
|
||||
required this.attachmentItems,
|
||||
required this.attachments,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
}) : pageController = PageController(initialPage: initialIndex);
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ImageViewScreen extends StatefulWidget {
|
|||
final dynamic maxScale;
|
||||
final int initialIndex;
|
||||
final PageController pageController;
|
||||
final List<AttachmentItem> attachmentItems;
|
||||
final List<String> attachments;
|
||||
final Axis scrollDirection;
|
||||
|
||||
@override
|
||||
|
@ -70,7 +70,7 @@ class _ImageViewScreenState extends State<ImageViewScreen> {
|
|||
PhotoViewGallery.builder(
|
||||
scrollPhysics: const BouncingScrollPhysics(),
|
||||
builder: _buildItem,
|
||||
itemCount: widget.attachmentItems.length,
|
||||
itemCount: widget.attachments.length,
|
||||
loadingBuilder: widget.loadingBuilder,
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.black),
|
||||
pageController: widget.pageController,
|
||||
|
@ -96,15 +96,15 @@ class _ImageViewScreenState extends State<ImageViewScreen> {
|
|||
}
|
||||
|
||||
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
|
||||
final AttachmentItem item = widget.attachmentItems[index];
|
||||
final String item = widget.attachments[index];
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: FileImage(
|
||||
File('${getIt.get<UserProfile>().baseImageDir}/${item.resource}'),
|
||||
File('${getIt.get<UserProfile>().baseImageDir}/$item'),
|
||||
),
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained * (0.5 + index / 10),
|
||||
minScale: PhotoViewComputedScale.contained * 0.5,
|
||||
maxScale: PhotoViewComputedScale.covered * 4.1,
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: item.id),
|
||||
// heroAttributes: PhotoViewHeroAttributes(tag: widget.heroTag),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const UseAnimation());
|
||||
}
|
||||
|
||||
class UseAnimation extends StatefulWidget {
|
||||
const UseAnimation({super.key});
|
||||
|
||||
@override
|
||||
State<UseAnimation> createState() => _UseAnimationState();
|
||||
}
|
||||
|
||||
class _UseAnimationState extends State<UseAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _tween;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = AnimationController(vsync: this);
|
||||
_tween = Tween<double>(begin: 0, end: 1).animate(_controller)
|
||||
..addStatusListener((status) {
|
||||
// print(status);
|
||||
})
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
// _controller.forward();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Use Animation',
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('use animation')),
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
width: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Image.asset('assets/images/user_2.png'),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: _tween.value == 1.0
|
||||
? null
|
||||
: const Color.fromARGB(255, 36, 36, 36)
|
||||
.withOpacity(0.3),
|
||||
child: Center(
|
||||
child: Text(
|
||||
(_tween.value * 100).toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
CircularProgressIndicator(
|
||||
value: _tween.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (_tween.value == 1.0) {
|
||||
_controller.reset();
|
||||
}
|
||||
print(_tween.value);
|
||||
// print(_controller.value);
|
||||
_controller.animateTo(_tween.value + 0.1,
|
||||
duration: const Duration(milliseconds: 200));
|
||||
print(_tween.value);
|
||||
},
|
||||
child: const Icon(Icons.plus_one),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -94,6 +94,8 @@ String formatMsgIDFromTime(DateTime dateTime) {
|
|||
dateTime.month < 10 ? '0${dateTime.month}' : '${dateTime.month}';
|
||||
String day = dateTime.day < 10 ? '0${dateTime.day}' : '${dateTime.day}';
|
||||
String hour = dateTime.hour < 10 ? '0${dateTime.hour}' : '${dateTime.hour}';
|
||||
String second =
|
||||
dateTime.second < 10 ? '0${dateTime.second}' : '${dateTime.second}';
|
||||
String minute =
|
||||
dateTime.minute < 10 ? '0${dateTime.minute}' : '${dateTime.minute}';
|
||||
String millisecond = dateTime.millisecond >= 100
|
||||
|
@ -102,5 +104,5 @@ String formatMsgIDFromTime(DateTime dateTime) {
|
|||
? '0${dateTime.minute}'
|
||||
: '00${dateTime.minute}';
|
||||
|
||||
return '$year$month$day$hour$minute$millisecond';
|
||||
return '$year$month$day$hour$minute$second$millisecond';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,449 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:together_mobile/database/box_type.dart';
|
||||
import 'package:together_mobile/database/hive_database.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/route_state_model.dart';
|
||||
import 'package:together_mobile/models/user_model.dart';
|
||||
import 'package:together_mobile/models/websocket_model.dart';
|
||||
import 'package:together_mobile/notification_api.dart';
|
||||
import 'package:together_mobile/request/message.dart';
|
||||
|
||||
const int chunkSize = 1024 * 1024 * 2;
|
||||
|
||||
Future<void> receiveFriendMsg(
|
||||
Map<String, dynamic> msg,
|
||||
bool isShowNotification,
|
||||
) async {
|
||||
print('=================收到了好友信息事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String senderId = msg['senderId'] as String;
|
||||
Box<ChatSetting> chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
||||
Box<MessageT> messageTBox = await HiveDatabase.openMessageBox(senderId);
|
||||
Box<int> msgIndexBox = await Hive.openBox<int>('msg_index_$senderId');
|
||||
ChatSetting? chatSetting = chatSettingBox.get(senderId);
|
||||
DateTime dateTime = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
if (chatSetting == null) {
|
||||
chatSettingBox.put(
|
||||
senderId,
|
||||
ChatSetting(
|
||||
senderId,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
dateTime,
|
||||
1,
|
||||
),
|
||||
);
|
||||
} else if (msgIndexBox.get(msg['msgId'] as String) != null) {
|
||||
return;
|
||||
} else {
|
||||
chatSetting.isOpen = true;
|
||||
chatSetting.latestDateTime = dateTime;
|
||||
chatSetting.unreadCount++;
|
||||
chatSettingBox.put(senderId, chatSetting);
|
||||
}
|
||||
|
||||
List<String> attachments = List.from(msg['attachments']);
|
||||
final DateTime now = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
for (var attachment in attachments) {
|
||||
Box<AttachmentProgress> apr =
|
||||
Hive.box<AttachmentProgress>('attachment_receive');
|
||||
apr.put(attachment, AttachmentProgress(0, 1000, 0, true, false));
|
||||
}
|
||||
|
||||
messageTBox.add(
|
||||
MessageT(
|
||||
msg['msgId'] as String,
|
||||
senderId,
|
||||
msg['text'],
|
||||
msg['type'],
|
||||
now,
|
||||
msg['isShowTime'],
|
||||
attachments,
|
||||
),
|
||||
);
|
||||
|
||||
if (!isShowNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
String name = getIt.get<Contact>().friends[senderId]!.friendRemark.isEmpty
|
||||
? getIt.get<ContactAccountProfile>().friends[senderId]!.nickname
|
||||
: getIt.get<Contact>().friends[senderId]!.friendRemark;
|
||||
|
||||
String routeName = getIt.get<RouteState>().currentPathName;
|
||||
|
||||
String avatar = getIt.get<ContactAccountProfile>().friends[senderId]!.avatar;
|
||||
|
||||
if (!getIt.get<RouteState>().isVisible) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
);
|
||||
} else if (routeName == 'Message') {
|
||||
if (!getIt.get<RouteState>().query.containsValue(senderId)) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
);
|
||||
}
|
||||
} else if (routeName != 'Chat') {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void receiveApplyFriend(Map<String, dynamic> msg) {
|
||||
print('=================收到了申请好友事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
getIt.get<ApplyList>().addJson(msg);
|
||||
}
|
||||
|
||||
void receiveFriendAdded(Map<String, dynamic> msg) {
|
||||
print('=================收到了申请好友通过事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
getIt.get<Contact>().addFriend(msg['friendId'], msg['setting']);
|
||||
getIt
|
||||
.get<ContactAccountProfile>()
|
||||
.addFriendAccountProfile(msg['friendId'], msg['accountProfile']);
|
||||
}
|
||||
|
||||
void receiveFriendDeleted(Map<String, dynamic> msg) {
|
||||
print('=================收到了解除好友事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
getIt.get<Contact>().removeFriend(msg['friendId']);
|
||||
getIt.get<ContactAccountProfile>().removeFriend(msg['friendId']);
|
||||
}
|
||||
|
||||
void receiveGroupChatCreation(Map<String, dynamic> msg) {
|
||||
print('=================收到了群聊邀请事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String groupChatId = msg['groupChat']['id'];
|
||||
getIt.get<Contact>().addGroupChat(groupChatId);
|
||||
getIt.get<ContactAccountProfile>().addGroupChatProfile(groupChatId, msg);
|
||||
}
|
||||
|
||||
void receiveGroupChatMsg(
|
||||
Map<String, dynamic> msg,
|
||||
bool isShowNotification,
|
||||
) async {
|
||||
print('=================收到了群聊信息事件==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String senderId = msg['senderId'] as String;
|
||||
String groupChatId = msg['groupChatId'] as String;
|
||||
Box<ChatSetting> chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
||||
Box<MessageT> messageTBox = await HiveDatabase.openMessageBox(groupChatId);
|
||||
Box<int> msgIndexBox = await Hive.openBox('msg_index_$groupChatId');
|
||||
ChatSetting? chatSetting = chatSettingBox.get(groupChatId);
|
||||
DateTime dateTime = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
getIt.get<ContactAccountProfile>().addGroupChatMemberProfile(
|
||||
groupChatId,
|
||||
msg['senderId'],
|
||||
{
|
||||
'avatar': msg['avatar'],
|
||||
'nickname': msg['nickname'],
|
||||
'remarkInGroupChat': msg['remarkInGroupChat'],
|
||||
},
|
||||
);
|
||||
|
||||
if (chatSetting == null) {
|
||||
chatSettingBox.put(
|
||||
groupChatId,
|
||||
ChatSetting(
|
||||
groupChatId,
|
||||
1,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
dateTime,
|
||||
1,
|
||||
),
|
||||
);
|
||||
} else if (msgIndexBox.get(msg['msgId'] as String) != null) {
|
||||
return;
|
||||
} else {
|
||||
chatSetting.isOpen = true;
|
||||
chatSetting.latestDateTime = dateTime;
|
||||
chatSetting.unreadCount++;
|
||||
chatSettingBox.put(groupChatId, chatSetting);
|
||||
}
|
||||
|
||||
List<String> attachments = List.from(msg['attachments']);
|
||||
final DateTime now = DateTime.parse(msg['dateTime'] as String);
|
||||
|
||||
messageTBox.add(
|
||||
MessageT(
|
||||
msg['msgId'] as String,
|
||||
senderId,
|
||||
msg['text'],
|
||||
msg['type'],
|
||||
now,
|
||||
msg['isShowTime'],
|
||||
attachments,
|
||||
),
|
||||
);
|
||||
|
||||
if (!isShowNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
String avatar =
|
||||
getIt.get<ContactAccountProfile>().groupChats[groupChatId]!.avatar;
|
||||
late String name;
|
||||
|
||||
if (getIt.get<Contact>().friends.containsKey(senderId)) {
|
||||
if (getIt.get<Contact>().friends[senderId]!.friendRemark.isNotEmpty) {
|
||||
name = getIt.get<Contact>().friends[senderId]!.friendRemark;
|
||||
} else if ((msg['remarkInGroupChat'] as String).isNotEmpty) {
|
||||
name = msg['remarkInGroupChat'];
|
||||
} else {
|
||||
name = msg['nickname'];
|
||||
}
|
||||
} else {
|
||||
name = (msg['remarkInGroupChat'] as String).isNotEmpty
|
||||
? msg['remarkInGroupChat']
|
||||
: msg['nickname'];
|
||||
}
|
||||
|
||||
String routeName = getIt.get<RouteState>().currentPathName;
|
||||
|
||||
if (!getIt.get<RouteState>().isVisible) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
groupChatId: groupChatId,
|
||||
);
|
||||
} else if (routeName == 'Message') {
|
||||
if (!getIt.get<RouteState>().query.containsValue(senderId)) {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
groupChatId: groupChatId,
|
||||
);
|
||||
}
|
||||
} else if (routeName != 'Chat') {
|
||||
NotificationAPI.showMessageNotifications(
|
||||
senderId: senderId,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
text: msg['text'] as String,
|
||||
groupChatId: groupChatId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void receiveChatImages(Map<String, dynamic> msg) async {
|
||||
print('=================收到了聊天图片事件==================');
|
||||
// print(msg);
|
||||
print('=======================================');
|
||||
String chatImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
String filename = msg['filename'];
|
||||
String filePath = '$chatImageDir/$filename';
|
||||
File file = File(filePath);
|
||||
|
||||
if (await file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
late Box<AttachmentProgress> aprBox;
|
||||
try {
|
||||
aprBox = Hive.box<AttachmentProgress>('attachment_receive');
|
||||
} catch (_) {
|
||||
aprBox = await Hive.openBox<AttachmentProgress>('attachment_receive');
|
||||
}
|
||||
String dirTime = filename.split('/')[0];
|
||||
Directory dir = Directory('$chatImageDir/$dirTime');
|
||||
|
||||
if (!(await dir.exists())) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
int totalChunkNum = msg['totalChunkNum'];
|
||||
|
||||
if (totalChunkNum == 1) {
|
||||
await file.writeAsBytes(List<int>.from(msg['bytes']));
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
() {
|
||||
aprBox.put(
|
||||
filename,
|
||||
AttachmentProgress(1, 1, 1.0, true, false),
|
||||
);
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
File tempFile = File('$chatImageDir/${msg['tempFilename']}');
|
||||
AttachmentProgress? ap = aprBox.get(filename);
|
||||
|
||||
if (!(await tempFile.exists())) {
|
||||
await tempFile.writeAsBytes(List<int>.from(msg['bytes']));
|
||||
}
|
||||
|
||||
if (ap == null) {
|
||||
aprBox.put(
|
||||
filename,
|
||||
AttachmentProgress(
|
||||
1,
|
||||
totalChunkNum,
|
||||
1 / totalChunkNum,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ap.hasChunkNum += 1;
|
||||
ap.progress = ap.hasChunkNum / totalChunkNum;
|
||||
ap.isValid = true;
|
||||
|
||||
List<File> tempFiles = List.generate(
|
||||
totalChunkNum,
|
||||
(index) {
|
||||
String tempFilePath =
|
||||
'$chatImageDir/temp/$filename-$totalChunkNum-$index';
|
||||
return File(tempFilePath);
|
||||
},
|
||||
);
|
||||
|
||||
// assemble chunks when all the chunks are arrived
|
||||
if (tempFiles.every((element) => element.existsSync())) {
|
||||
final openedFile = file.openWrite(mode: FileMode.append);
|
||||
|
||||
for (var i = 0; i < totalChunkNum; i++) {
|
||||
Uint8List bytes = await tempFiles[i].readAsBytes();
|
||||
openedFile.add(bytes);
|
||||
await tempFiles[i].delete();
|
||||
}
|
||||
openedFile.close();
|
||||
ap.hasChunkNum = totalChunkNum;
|
||||
ap.progress = 1.0;
|
||||
ap.isValid = true;
|
||||
}
|
||||
|
||||
aprBox.put(filename, ap);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receivePullChatImage(Map<String, dynamic> msg) async {
|
||||
print('=================收到了拉取图片请求==================');
|
||||
print(msg);
|
||||
print('=======================================');
|
||||
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
Map<String, dynamic> sentMsg = {
|
||||
'event': 'chat-image',
|
||||
'senderId': getIt.get<UserAccount>().id,
|
||||
};
|
||||
|
||||
if (msg['chatType'] == 0) {
|
||||
sentMsg['receiverId'] = msg['receiverId'];
|
||||
} else {
|
||||
sentMsg['receiverIds'] = msg['receiverIds'];
|
||||
}
|
||||
|
||||
for (var filename in msg['attachments']) {
|
||||
File file = File('$baseImageDir/$filename');
|
||||
int fileSize = await file.length();
|
||||
int totalChunkNum = (fileSize / chunkSize).ceil();
|
||||
int start = 0;
|
||||
int end = fileSize >= chunkSize ? start + chunkSize : start + fileSize;
|
||||
List<int> bytes = [];
|
||||
await file.openRead(start, end).forEach((element) => bytes.addAll(element));
|
||||
|
||||
sentMsg['filename'] = filename;
|
||||
sentMsg['totalChunkNum'] = totalChunkNum;
|
||||
sentMsg['tempFilename'] = 'temp/$filename-$totalChunkNum-0';
|
||||
sentMsg['currentChunkNum'] = 0;
|
||||
sentMsg['bytes'] = bytes;
|
||||
|
||||
await uploadChatAttachment(sentMsg);
|
||||
getIt.get<WebSocketManager>().addSendImageTimer(filename, totalChunkNum);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveCheckChatImage(Map<String, dynamic> msg) async {
|
||||
print('=================收到了服务端确认收到图片事件==================');
|
||||
// print(msg);
|
||||
print('=======================================');
|
||||
int nextChunkNum = msg['currentChunkNum'] + 1;
|
||||
int totalChunkNum = msg['totalChunkNum'];
|
||||
String baseImageDir = getIt.get<UserProfile>().baseImageDir;
|
||||
String filename = msg['filename'];
|
||||
Box<AttachmentProgress> asBox =
|
||||
Hive.box<AttachmentProgress>('attachment_send');
|
||||
AttachmentProgress aps = asBox.get(filename)!;
|
||||
|
||||
getIt.get<WebSocketManager>().removeSendImageTimer(filename);
|
||||
aps.hasChunkNum += 1;
|
||||
aps.progress = aps.hasChunkNum / aps.totalChunkNum;
|
||||
asBox.put(filename, aps);
|
||||
// print(aps.hasChunkNum);
|
||||
// print(aps.totalChunkNum);
|
||||
// print('已发送了: ${aps.progress}');
|
||||
if (nextChunkNum == totalChunkNum) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start to send next chunk
|
||||
int start = nextChunkNum * chunkSize;
|
||||
File file = File('$baseImageDir/$filename');
|
||||
List<int> bytes = [];
|
||||
|
||||
if (nextChunkNum + 1 == totalChunkNum) {
|
||||
await file
|
||||
.openRead(start)
|
||||
.forEach((element) => bytes.addAll(element as List<int>));
|
||||
} else {
|
||||
await file
|
||||
.openRead(start, start + chunkSize)
|
||||
.forEach((element) => bytes.addAll(element as List<int>));
|
||||
}
|
||||
|
||||
Map<String, dynamic> sentMsg = {
|
||||
'event': 'chat-image',
|
||||
'senderId': getIt.get<UserAccount>().id,
|
||||
};
|
||||
|
||||
if (msg['chatType'] == 0) {
|
||||
sentMsg['receiverId'] = msg['receiverId'];
|
||||
} else {
|
||||
sentMsg['receiverIds'] = msg['receiverIds'];
|
||||
}
|
||||
|
||||
sentMsg['filename'] = filename;
|
||||
sentMsg['totalChunkNum'] = totalChunkNum;
|
||||
sentMsg['tempFilename'] = 'temp/$filename-$totalChunkNum-$nextChunkNum';
|
||||
sentMsg['currentChunkNum'] = nextChunkNum;
|
||||
sentMsg['bytes'] = bytes;
|
||||
|
||||
uploadChatAttachment(sentMsg);
|
||||
getIt.get<WebSocketManager>().addSendImageTimer(filename, totalChunkNum);
|
||||
}
|
20
pubspec.lock
20
pubspec.lock
|
@ -1157,14 +1157,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1265,10 +1257,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: e03928880bdbcbf496fb415573f5ab7b1ea99b9b04f669c01104d085893c3134
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "3.0.7"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1357,6 +1349,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
worker_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: worker_manager
|
||||
sha256: caab0544cb95471e8d1417d21e25838ee4f614f4651a9e8e5a4f26bfeff5f312
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -61,6 +61,7 @@ dependencies:
|
|||
timezone: ^0.9.2
|
||||
video_player: ^2.6.1
|
||||
web_socket_channel: ^2.4.0
|
||||
worker_manager: ^6.3.1
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.5
|
||||
|
|
Loading…
Reference in New Issue