458 lines
13 KiB
Dart
458 lines
13 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:hive_flutter/hive_flutter.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';
|
|
|
|
enum SocketStatus {
|
|
connected,
|
|
reconnecting,
|
|
error,
|
|
closed,
|
|
}
|
|
|
|
class WebSocketManager extends ChangeNotifier {
|
|
late Uri wsUrl;
|
|
late WebSocketChannel channel;
|
|
String id = '';
|
|
SocketStatus socketStatus = SocketStatus.closed;
|
|
Timer? heartBeatTimer;
|
|
Timer? serverTimer;
|
|
Timer? reconnectTimer;
|
|
int reconnectCount = 5;
|
|
int reconnectTimes = 0;
|
|
Duration heartBeatTimeout = const Duration(seconds: 4);
|
|
Duration reconnectTimeout = const Duration(seconds: 3);
|
|
|
|
void connect(String userId, bool isReconnect) {
|
|
id = userId;
|
|
wsUrl = Uri.parse('ws://10.0.2.2:8000/ws/$id?is_reconnect=$isReconnect');
|
|
if (isReconnect) {
|
|
socketStatus = SocketStatus.reconnecting;
|
|
notifyListeners();
|
|
}
|
|
// This doesn't blcok the programe whethe it connect the server or not
|
|
// So heartBeat will be executre straightly
|
|
channel = WebSocketChannel.connect(wsUrl);
|
|
heartBeatInspect();
|
|
|
|
channel.stream.listen(onData, onError: onError, onDone: onDone);
|
|
}
|
|
|
|
void disconnect() {
|
|
channel.sink.close();
|
|
wsUrl = Uri();
|
|
id = '';
|
|
socketStatus = SocketStatus.closed;
|
|
notifyListeners();
|
|
heartBeatTimer?.cancel();
|
|
serverTimer?.cancel();
|
|
reconnectTimer?.cancel();
|
|
heartBeatTimer = null;
|
|
serverTimer = null;
|
|
reconnectTimer = null;
|
|
reconnectTimes = 0;
|
|
}
|
|
|
|
void onData(jsonData) {
|
|
// If socket can receive msg, that means connection is estabilished
|
|
socketStatus = SocketStatus.connected;
|
|
notifyListeners();
|
|
print('websocket connected <$channel>');
|
|
if (reconnectTimer != null) {
|
|
reconnectTimer!.cancel();
|
|
reconnectTimer = null;
|
|
reconnectTimes = 0;
|
|
}
|
|
|
|
heartBeatInspect();
|
|
|
|
Map<String, dynamic> data = json.decode(jsonData);
|
|
switch (data['event']) {
|
|
case 'friend-chat-msg':
|
|
receiveFriendMsg(data, true);
|
|
case 'apply-friend':
|
|
receiveApplyFriend(data);
|
|
case 'friend-added':
|
|
receiveFriendAdded(data);
|
|
case 'friend-deleted':
|
|
receiveFriendDeleted(data);
|
|
case 'chat-image':
|
|
receiveChatImages(data);
|
|
case 'group-chat-creation':
|
|
receiveGroupChatCreation(data);
|
|
case 'group-chat-msg':
|
|
receiveGroupChatMsg(data, true);
|
|
}
|
|
}
|
|
|
|
// This will be trigger while server or client close the connection
|
|
// for example server is restarting
|
|
void onDone() {
|
|
print('websocket disconnected <$channel>');
|
|
print('22222222222222222222');
|
|
if (socketStatus == SocketStatus.closed) {
|
|
// Client close the connection
|
|
return;
|
|
}
|
|
if (socketStatus == SocketStatus.connected) {
|
|
// Server close the connection
|
|
socketStatus = SocketStatus.reconnecting;
|
|
notifyListeners();
|
|
print(111111111111111);
|
|
reconnectTimes++;
|
|
reconnect();
|
|
}
|
|
if (reconnectTimes >= reconnectCount) {
|
|
socketStatus = SocketStatus.error;
|
|
reconnectTimes = 0;
|
|
} else {
|
|
socketStatus = SocketStatus.reconnecting;
|
|
print('3333333333333333333');
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
// This will be trigger while server exactly shutdown
|
|
void onError(Object error, StackTrace st) {
|
|
print('Websocket connect occurs error: <$error>');
|
|
// print(st);
|
|
if (reconnectTimes >= reconnectCount) {
|
|
socketStatus = SocketStatus.error;
|
|
notifyListeners();
|
|
channel.sink.close();
|
|
if (heartBeatTimer != null) {
|
|
heartBeatTimer!.cancel();
|
|
heartBeatTimer = null;
|
|
}
|
|
} else {
|
|
print('${reconnectTimes}th reconnection');
|
|
reconnect();
|
|
}
|
|
}
|
|
|
|
void heartBeatInspect() {
|
|
print('start heartbeat inspect......');
|
|
|
|
if (heartBeatTimer != null) {
|
|
heartBeatTimer!.cancel();
|
|
heartBeatTimer = null;
|
|
}
|
|
|
|
if (serverTimer != null) {
|
|
serverTimer!.cancel();
|
|
serverTimer = null;
|
|
}
|
|
|
|
heartBeatTimer = Timer(heartBeatTimeout, () {
|
|
channel.sink.add(json.encode({'event': 'ping'}));
|
|
serverTimer = Timer(heartBeatTimeout, () {
|
|
// This will trigger the onDone callback
|
|
channel.sink.close(status.internalServerError);
|
|
socketStatus = SocketStatus.reconnecting;
|
|
notifyListeners();
|
|
});
|
|
});
|
|
}
|
|
|
|
void reconnect() {
|
|
if (heartBeatTimer != null) {
|
|
heartBeatTimer!.cancel();
|
|
heartBeatTimer = null;
|
|
}
|
|
|
|
if (serverTimer != null) {
|
|
serverTimer!.cancel();
|
|
serverTimer = null;
|
|
}
|
|
|
|
if (reconnectTimer != null) {
|
|
reconnectTimer!.cancel();
|
|
reconnectTimer = null;
|
|
}
|
|
|
|
reconnectTimer = Timer(reconnectTimeout, () {
|
|
if (reconnectTimes < reconnectCount) {
|
|
print('websocket reconnecting......');
|
|
reconnectTimes++;
|
|
connect(id, true);
|
|
} else {
|
|
print('reconnection times exceed the max times......');
|
|
// If it is still disconnection after reconnect 30 times, set the socket
|
|
// status to error, means the network is bad, and stop reconnecting.
|
|
socketStatus = SocketStatus.error;
|
|
notifyListeners();
|
|
channel.sink.close();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void receiveFriendMsg(Map<String, dynamic> msg, bool isShowNotification) async {
|
|
print('=================收到了好友信息事件==================');
|
|
print(msg);
|
|
print('=======================================');
|
|
String senderId = msg['senderId'] as String;
|
|
late Box<MessageT> messageTBox;
|
|
try {
|
|
messageTBox = Hive.box('message_$senderId');
|
|
} catch (e) {
|
|
messageTBox = await Hive.openBox('message_$senderId');
|
|
}
|
|
|
|
Box<ChatSetting> chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
|
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 ((messageTBox.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);
|
|
|
|
messageTBox.put(
|
|
msg['msgId'] as String,
|
|
MessageT(
|
|
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 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;
|
|
late Box<MessageT> messageTBox;
|
|
try {
|
|
messageTBox = Hive.box('message_$groupChatId');
|
|
} catch (e) {
|
|
messageTBox = await Hive.openBox('message_$groupChatId');
|
|
}
|
|
|
|
Box<ChatSetting> chatSettingBox = Hive.box<ChatSetting>('chat_setting');
|
|
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 ((messageTBox.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.put(
|
|
msg['msgId'] as String,
|
|
MessageT(
|
|
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,
|
|
);
|
|
}
|
|
}
|