You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1475 lines
52 KiB
1475 lines
52 KiB
import 'dart:async'; |
|
import 'dart:convert'; |
|
import 'dart:io'; |
|
|
|
import 'package:dio/dio.dart'; |
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; |
|
import 'package:flutter/cupertino.dart'; |
|
import 'package:flutter/material.dart'; |
|
import 'package:flutter/services.dart'; |
|
import 'package:flutter/widgets.dart'; |
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; |
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
|
import 'package:get/get.dart'; |
|
import 'package:huixiang/constant.dart'; |
|
import 'package:huixiang/data/base_data.dart'; |
|
import 'package:huixiang/data/im_user.dart'; |
|
import 'package:huixiang/data/user_info.dart'; |
|
import 'package:huixiang/im/database/message.dart'; |
|
import 'package:huixiang/im/out/message.pb.dart'; |
|
import 'package:huixiang/main.dart'; |
|
import 'package:huixiang/retrofit/retrofit_api.dart'; |
|
import 'package:huixiang/utils/constant.dart'; |
|
import 'package:huixiang/utils/shared_preference.dart'; |
|
import 'package:huixiang/view_widget/my_appbar.dart'; |
|
import 'package:image_pickers/image_pickers.dart'; |
|
import 'package:permission_handler/permission_handler.dart'; |
|
import '../../community/release_dynamic.dart'; |
|
import '../../generated/l10n.dart'; |
|
import '../../utils/font_weight.dart'; |
|
import '../utils/flutter_utils.dart'; |
|
import '../view_widget/custom_image.dart'; |
|
import '../view_widget/request_permission.dart'; |
|
|
|
|
|
class ChatDetailsPage extends StatefulWidget { |
|
final Map<String, dynamic>? arguments; |
|
|
|
ChatDetailsPage({this.arguments}); |
|
|
|
@override |
|
State<StatefulWidget> createState() { |
|
return _ChatDetailsPage(); |
|
} |
|
} |
|
|
|
class _ChatDetailsPage extends State<ChatDetailsPage> |
|
with WidgetsBindingObserver { |
|
ApiService? apiService; |
|
List<Medias> mediaPaths = []; |
|
int selectCount = 9; |
|
int dynamicType = 0; |
|
final TextEditingController chatController = TextEditingController(); |
|
bool isKeyBoardShow = false; |
|
bool emojiShowing = false; |
|
bool moreShow = false; |
|
|
|
double keyboard = -1; |
|
var commentFocus = FocusNode(); |
|
String hintText = "输入消息内容..."; |
|
UserInfo? userInfo; |
|
// final OnChatMessage? _tempOnChatMessage = OnChatMsgInstance.instance.onChatMessage; |
|
final ScrollController scrollController = ScrollController(); |
|
GlobalKey<SliverAnimatedListState> newanimatedListKey = GlobalKey<SliverAnimatedListState>(); |
|
GlobalKey<SliverAnimatedListState> loadanimatedListKey = GlobalKey<SliverAnimatedListState>(); |
|
GlobalKey centerKey = GlobalKey(); |
|
String tex = ""; |
|
String selfUserId = ""; |
|
ImUser? _toUser; |
|
late String conversation; |
|
List<String> imageUrl = []; |
|
|
|
double inputWidgetHeight = 95; |
|
double dynamicInputHeight = 0; |
|
|
|
late StreamSubscription<bool> keyboardSubscription; |
|
bool loading = false; |
|
|
|
List<Message> newmessages = []; |
|
List<Message> loadmessages = []; |
|
int page = 0; |
|
|
|
loadMessageList() async { |
|
selfUserId = SharedInstance.instance.userId; |
|
debugPrint("selfUserId: $selfUserId _toUser?.mid: ${_toUser?.mid} "); |
|
conversation = conversationId(selfUserId, _toUser?.mid); |
|
|
|
ImUser? imUser = await hxDatabase.queryImUserById(conversation); |
|
if (imUser == null) { |
|
await hxDatabase.insertOrUpdateImUser(_toUser!.toJson()); |
|
} |
|
|
|
// unread msg 2 read state |
|
await hxDatabase.readMessage(conversation); |
|
await refresh(); |
|
|
|
socketClient.addCallback(_toUser?.mid ?? '', (Message message) { |
|
newmessages.add(message); |
|
messageShowTime().then((value){ |
|
newanimatedListKey.currentState?.insertItem(newmessages.length - 1); |
|
jumpToBottom(); |
|
}); |
|
if ((message.msgType == MsgType.VIDEO.value || message.msgType == MsgType.IMAGE.value) && (message.attach?.isNotEmpty ?? false)) { |
|
imageUrl.add(message.attach!); |
|
} |
|
}); |
|
|
|
messageImageUrl(newmessages); |
|
} |
|
|
|
Future<double> refresh() async { |
|
List<Message>? messagePage = await hxDatabase.queryUList(conversation, page: page + 1, pageSize: 10); |
|
debugPrint("refresh-message-list: ${messagePage?.length ?? ''} page: $page"); |
|
if (messagePage?.isEmpty ?? true) { |
|
// refreshController.refreshCompleted(); |
|
return 0; |
|
} else { |
|
// refreshController.refreshCompleted(); |
|
page += 1; |
|
} |
|
double height = 0; |
|
|
|
if (page == 1) { |
|
await Future.forEach(messagePage!, (element) async { |
|
height += await chatDetailsItemHeight(element); |
|
}); |
|
if (scrollController.hasClients) { |
|
double scrollHeight = height - (View.of(context).display.size.height / View.of(context).display.devicePixelRatio - 95 - View.of(context).padding.top); |
|
scrollController.position.restoreOffset(scrollHeight, initialRestore: true); |
|
} |
|
newmessages.addAll(messagePage.reversed.toList()); |
|
await messageShowTime(); |
|
newanimatedListKey.currentState?.insertAllItems(0, messagePage.length); |
|
} else { |
|
loadmessages.addAll(messagePage!.toList()); |
|
await messageShowTime(); |
|
loadanimatedListKey.currentState?.insertAllItems(0, messagePage.length, duration: 300.milliseconds); |
|
} |
|
return height; |
|
} |
|
|
|
/// 消息中的图片 |
|
messageImageUrl(List<Message> messages) { |
|
List<String> images = messages.where((e) => (e.msgType == MsgType.VIDEO.value || e.msgType == MsgType.IMAGE.value) && (e.attach?.isNotEmpty ?? false)).map((e) => "${e.attach}").toList(); |
|
if (images.isNotEmpty) { |
|
imageUrl.addAll(images); |
|
} |
|
} |
|
|
|
Future messageShowTime() async { |
|
List<Message>? messagePages = await hxDatabase.queryTList(conversation); |
|
List<Message> tempMessages = newmessages + loadmessages; |
|
if (messagePages?.isEmpty ?? true) { |
|
if (tempMessages.isEmpty) { |
|
return; |
|
} |
|
tempMessages.first.showTime = true; |
|
return; |
|
} |
|
for (var value in tempMessages) { |
|
Message message = messagePages!.firstWhere((element) => value.id == element.id, orElse: () => Message(0, "", "", "", "", "", "", 0, "", 0, 0)); |
|
value.showTime = message.id > 0; |
|
} |
|
} |
|
|
|
///查询个人信息 |
|
queryUser() async { |
|
apiService = ApiService(Dio(), context: context, token: SharedInstance.instance.token); |
|
|
|
userInfo = UserInfo.fromJson(jsonDecode(SharedInstance.instance.userJson)); |
|
if (userInfo?.phone?.isNotEmpty ?? false) { |
|
setState(() {}); |
|
return; |
|
} |
|
BaseData<UserInfo>? baseData = await apiService?.queryInfo().catchError((onError) { |
|
return BaseData<UserInfo>()..isSuccess = false; |
|
}); |
|
if ((baseData?.isSuccess ?? false) && baseData?.data != null) { |
|
setState(() { |
|
userInfo = baseData!.data; |
|
}); |
|
SharedInstance.instance.saveUser(baseData!.data!); |
|
} |
|
} |
|
|
|
refreshState() { |
|
if (mounted) setState(() {}); |
|
} |
|
|
|
@override |
|
void initState() { |
|
super.initState(); |
|
|
|
_toUser = widget.arguments?["toUser"]; |
|
// OnChatMsgInstance.instance.onChatMessage = this; |
|
WidgetsBinding.instance.addObserver(this); |
|
commentFocus.addListener(_focusNodeListener); |
|
|
|
queryUser(); |
|
loadMessageList(); |
|
|
|
scrollController.addListener(() { |
|
debugPrint("scrollController-onChange: ${scrollController.position.pixels}"); |
|
}); |
|
|
|
var keyboardVisibilityController = KeyboardVisibilityController(); |
|
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) { |
|
isKeyBoardShow = visible; |
|
debugPrint("keyboardVisibility-onChange: ${isKeyBoardShow}"); |
|
if (isKeyBoardShow) { |
|
if (!emojiShowing && !moreShow) { |
|
chatController.clear(); |
|
} |
|
} else { |
|
inputWidgetHeight = 95; |
|
} |
|
jumpToBottom(); |
|
}); |
|
} |
|
|
|
void jumpToBottom() { |
|
Future.delayed(const Duration(milliseconds: 400), () { |
|
if (scrollController.hasClients && scrollController.position.pixels != scrollController.position.maxScrollExtent) { |
|
scrollController.position.animateTo(scrollController.position.maxScrollExtent, duration: 250.milliseconds, curve: Curves.easeInOut); |
|
} |
|
}); |
|
} |
|
|
|
@override |
|
void didChangeMetrics() { |
|
super.didChangeMetrics(); |
|
dynamicInputHeight = MediaQuery.of(context).viewInsets.bottom; |
|
if (dynamicInputHeight > 0 && dynamicInputHeight > keyboard) { |
|
keyboard = dynamicInputHeight; |
|
} |
|
} |
|
|
|
@override |
|
void dispose() { |
|
super.dispose(); |
|
// OnChatMsgInstance.instance.onChatMessage = _tempOnChatMessage; |
|
keyboardSubscription.cancel(); |
|
WidgetsBinding.instance.removeObserver(this); |
|
commentFocus.removeListener(_focusNodeListener); |
|
scrollController.dispose(); |
|
socketClient.removeCallback(_toUser?.mid ?? ''); |
|
} |
|
|
|
void _focusNodeListener() { |
|
/*if (_focusNode.hasFocus || _focusNode.consumeKeyboardToken()){ |
|
setState(() { |
|
smileyPadGone = true; |
|
}); |
|
}*/ |
|
} |
|
|
|
///输入框点击事件 |
|
_onTextFieldTap() { |
|
if (emojiShowing) { |
|
emojiShowing = false; |
|
} |
|
if (moreShow) { |
|
moreShow = false; |
|
} |
|
} |
|
|
|
///表情包点击事件 |
|
_onSmileyTap() { |
|
emojiShowing = !emojiShowing; |
|
if (isKeyBoardShow && commentFocus.hasFocus) { |
|
commentFocus.unfocus(); |
|
} |
|
if (moreShow) { |
|
moreShow = false; |
|
} |
|
if (emojiShowing) { |
|
if (inputWidgetHeight < 270) { |
|
inputWidgetHeight = 95 + (keyboard == -1 ? 270 : keyboard); |
|
jumpToBottom(); |
|
} |
|
} else { |
|
inputWidgetHeight = 95; |
|
} |
|
setState(() {}); |
|
} |
|
|
|
///更多点击事件 |
|
_onMoreTap() { |
|
moreShow = !moreShow; |
|
if (isKeyBoardShow && commentFocus.hasFocus) { |
|
commentFocus.unfocus(); |
|
} |
|
if (emojiShowing) { |
|
emojiShowing = false; |
|
} |
|
if (moreShow) { |
|
if (inputWidgetHeight == 95) { |
|
inputWidgetHeight += (keyboard == -1 ? 167 : keyboard); |
|
jumpToBottom(); |
|
} |
|
} else { |
|
inputWidgetHeight = 95; |
|
} |
|
setState(() {}); |
|
} |
|
|
|
///图片/视频选择 |
|
Future getImageOrVideo(GalleryMode galleryMode) async { |
|
if (selectCount == 0) return; |
|
mediaPaths.clear(); |
|
List<Media> medias = await ImagePickers.pickerPaths( |
|
galleryMode: galleryMode, |
|
selectCount: (galleryMode == GalleryMode.video) ? 1 : selectCount, |
|
showGif: true, |
|
showCamera: false, |
|
compressSize: 500, |
|
uiConfig: UIConfig( |
|
uiThemeColor: Color(0xFFAAAAAA), |
|
), |
|
cropConfig: CropConfig( |
|
enableCrop: false, |
|
width: 200, |
|
height: 200, |
|
), |
|
); |
|
|
|
mediaPaths.addAll(medias.map((e) => Medias(e)).toList()); |
|
selectCount = 9 - mediaPaths.length; |
|
|
|
if (mediaPaths.length > 0) { |
|
if (galleryMode == GalleryMode.image) { |
|
dynamicType = 1; |
|
} else { |
|
dynamicType = 2; |
|
} |
|
|
|
List<String> filePath = mediaPaths.map((e) => "${e.path}").toList(); |
|
Future.forEach(filePath.toSet(), (path) async { |
|
String fileUrl = await qiniu.uploadFile(apiService!, path); |
|
socketClient.sendMessage(_toUser?.mid ?? '', fileUrl, attach: path, msgType: galleryMode == GalleryMode.image ? 2 : 4).then((value) { |
|
Message message = value; |
|
newmessages.add(message); |
|
messageShowTime().then((value) { |
|
newanimatedListKey.currentState?.insertItem(newmessages.length - 1); |
|
chatController.clear(); |
|
jumpToBottom(); |
|
}); |
|
}); |
|
}); |
|
} |
|
setState(() {}); |
|
} |
|
|
|
///拍照 |
|
openCamera() async { |
|
if (await Permission.camera.isPermanentlyDenied) { |
|
showCupertinoDialog( |
|
context: context, |
|
builder: (context) { |
|
return RequestPermission( |
|
"assets/image/icon_camera_permission_tips.webp", |
|
S.of(context).ninxiangjiquanxianweikaiqi, |
|
S.of(context).weilekaipaizhaoxuanzhetouxiang, |
|
S.of(context).kaiqiquanxian, |
|
(result) async { |
|
if (result) { |
|
await openAppSettings(); |
|
} |
|
}, |
|
heightRatioWithWidth: 0.82, |
|
); |
|
}, |
|
); |
|
} else if (await Permission.camera.isGranted) { |
|
if (await Permission.storage.isPermanentlyDenied) { |
|
showCupertinoDialog( |
|
context: context, |
|
builder: (context) { |
|
return RequestPermission( |
|
"assets/image/icon_storage_permission_tips.webp", |
|
"您未开启存储权限,请点击开启", |
|
"为了您可以在使用过程中访问您设备上的照片、媒体内容和文件,请您开启存储使用权限", |
|
S.of(context).kaiqiquanxian, |
|
(result) async { |
|
if (result) { |
|
await openAppSettings(); |
|
} |
|
}, |
|
heightRatioWithWidth: 0.82, |
|
); |
|
}, |
|
); |
|
} else if (await Permission.storage.isGranted) { |
|
Media? medias = await ImagePickers.openCamera( |
|
cameraMimeType: CameraMimeType.photo, |
|
cropConfig: CropConfig( |
|
enableCrop: true, |
|
width: 200, |
|
height: 200, |
|
), |
|
compressSize: 500, |
|
); |
|
if (medias == null) return; |
|
String path = medias.path ?? ''; |
|
String fileUrl = await qiniu.uploadFile(apiService!, path); |
|
socketClient.sendMessage(_toUser?.mid ?? '', fileUrl, attach: path, msgType: 2).then((value) { |
|
Message message = value; |
|
newmessages.add(message); |
|
messageShowTime().then((value) { |
|
newanimatedListKey.currentState?.insertItem(newmessages.length - 1); |
|
chatController.clear(); |
|
jumpToBottom(); |
|
}); |
|
}); |
|
} else { |
|
showCameraTipsAlertDialog(context); |
|
return false; |
|
} |
|
} else { |
|
showCameraTipsAlertDialog(context); |
|
return false; |
|
} |
|
} |
|
|
|
///拍照权限说明 |
|
showCameraTipsAlertDialog(context) async { |
|
//显示对话框 |
|
showDialog( |
|
context: context, |
|
barrierDismissible: false, |
|
barrierColor: null, |
|
builder: (BuildContext context) { |
|
return Column( |
|
children: [ |
|
Container( |
|
width: double.infinity, |
|
padding: EdgeInsets.all(15), |
|
margin: EdgeInsets.all(15), |
|
decoration: new BoxDecoration( |
|
color: Colors.white, |
|
borderRadius: BorderRadius.circular(10), |
|
), |
|
child: Row( |
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
children: [ |
|
Padding( |
|
padding: EdgeInsets.only( |
|
right: 10.w, |
|
), |
|
child: Icon( |
|
Icons.add_photo_alternate_outlined, |
|
color: Colors.black, |
|
size: 22, |
|
)), |
|
Expanded( |
|
child: Column( |
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
mainAxisSize: MainAxisSize.min, |
|
children: [ |
|
Text( |
|
"相机权限使用说明", |
|
style: TextStyle( |
|
fontSize: 18.sp, |
|
fontWeight: MyFontWeight.regular, |
|
color: Colors.black, |
|
), |
|
), |
|
SizedBox( |
|
height: 3.h, |
|
), |
|
Text( |
|
"实现您扫码、拍摄等功能。", |
|
style: TextStyle( |
|
fontSize: 13.sp, |
|
height: 1.2, |
|
fontWeight: MyFontWeight.regular, |
|
color: Colors.black, |
|
), |
|
), |
|
], |
|
), |
|
), |
|
], |
|
), |
|
) |
|
], |
|
mainAxisSize: MainAxisSize.min, |
|
); |
|
}, |
|
); |
|
await Permission.camera.request(); |
|
Navigator.of(context).pop(); |
|
if (await Permission.camera.isGranted) { |
|
openCamera(); |
|
} |
|
} |
|
|
|
@override |
|
Widget build(BuildContext ctx) { |
|
return GestureDetector( |
|
behavior: HitTestBehavior.translucent, |
|
onTap: () { |
|
setState(() { |
|
emojiShowing = false; |
|
isKeyBoardShow = false; |
|
moreShow = false; |
|
}); |
|
inputWidgetHeight = 95; |
|
FocusScope.of(context).requestFocus(FocusNode()); |
|
// jumpToBottom(); |
|
}, |
|
child: Scaffold( |
|
resizeToAvoidBottomInset: true, |
|
backgroundColor: Color(0xFFF6F6F6), |
|
appBar: MyAppBar( |
|
title: _toUser?.nickname, |
|
titleColor: Color(0xFF0D0D0D), |
|
titleSize: 17.sp, |
|
leading: true, |
|
leadingColor: Colors.black, |
|
toolbarHeight: kToolbarHeight, |
|
action: GestureDetector( |
|
behavior: HitTestBehavior.opaque, |
|
onTap: () async { |
|
await Navigator.of(context).pushNamed( |
|
'/router/chat_setting', |
|
arguments: { |
|
"userId": _toUser?.mid, |
|
}, |
|
); |
|
page = 0; |
|
refresh().then((value) { |
|
refreshState(); |
|
}); |
|
}, |
|
child: Container( |
|
alignment: Alignment.center, |
|
child: Icon( |
|
Icons.more_horiz, |
|
color: Colors.black, |
|
size: 30, |
|
), |
|
), |
|
), |
|
), |
|
body: |
|
// Container( |
|
// height: MediaQuery.of(context).size.height - kToolbarHeight - MediaQuery.of(context).padding.top - MediaQuery.of(context).viewInsets.bottom, |
|
// child: |
|
Column( |
|
children: [ |
|
Expanded( |
|
child: chatDetailsList(), |
|
flex: 1, |
|
), |
|
input(), |
|
], |
|
), |
|
// ), |
|
), |
|
); |
|
} |
|
|
|
///聊天列表 |
|
Widget chatDetailsList() { |
|
return RefreshIndicator( |
|
child: CustomScrollView( |
|
center: centerKey, |
|
controller: scrollController, |
|
physics: ClampingScrollPhysics(), |
|
slivers: [ |
|
SliverAnimatedList( |
|
itemBuilder: (context, index, animation) { |
|
Message message = loadmessages[index]; |
|
return chatDetailsItem(message, index, loadanimatedListKey); |
|
}, |
|
key: loadanimatedListKey, |
|
initialItemCount: loadmessages.length, |
|
), |
|
SliverPadding( |
|
padding: EdgeInsets.zero, |
|
key: centerKey, |
|
), |
|
SliverAnimatedList( |
|
itemBuilder: (context, index, animation) { |
|
Message message = newmessages[index]; |
|
return chatDetailsItem(message, index, loadanimatedListKey); |
|
}, |
|
key: newanimatedListKey, |
|
initialItemCount: newmessages.length, |
|
), |
|
], |
|
), |
|
onRefresh: () async { |
|
refresh(); |
|
}, |
|
); |
|
// return AnimatedList( |
|
// key: animatedListKey, |
|
// initialItemCount: messages.length, |
|
// controller: scrollController, |
|
// physics: BouncingScrollPhysics()..frictionFactor(0.8), |
|
// itemBuilder: (context, position, animation) { |
|
// return chatDetailsItem(position); |
|
// }, |
|
// ); |
|
} |
|
|
|
Future<double> chatDetailsItemHeight(Message message) async { |
|
bool isSelf = message.fromId == selfUserId; |
|
bool isText = message.msgType == 1; |
|
bool isImage = message.msgType == 2; |
|
double itemHeight = 32.0; |
|
if (message.showTime) { |
|
itemHeight += 10.sp; |
|
} |
|
if (!isSelf && isText) { |
|
itemHeight += 10.h; |
|
double itemWidth = MediaQuery.of(context).size.width - 92.w - 44; |
|
double textH = textHeight(message.content, 17.sp, 1.2.h, itemWidth) + 12.h; |
|
itemHeight += textH > 44 ? textH : 44; |
|
} |
|
if (isSelf && isText) { |
|
itemHeight += 10.h; |
|
double itemWidth = MediaQuery.of(context).size.width - 88.w - 44; |
|
if (message.state == 3) { |
|
itemWidth -= 20; |
|
itemWidth -= 12.w; |
|
} |
|
double textH = textHeight(message.content, 17.sp, 1.2.h, itemWidth) + 12.h; |
|
itemHeight += textH > 44 ? textH : 44; |
|
} |
|
if (!isSelf && isImage) { |
|
itemHeight += 10.h; |
|
Size size = await fetchImageSize(message); |
|
itemHeight += size.height; |
|
} |
|
if (isSelf && isImage) { |
|
itemHeight += 10.h; |
|
Size size = await fetchImageSize(message); |
|
itemHeight += size.height; |
|
} |
|
return itemHeight; |
|
} |
|
|
|
textHeight(text, fontSize, height, width) { |
|
final TextPainter textPainter = TextPainter( |
|
text: TextSpan( |
|
text: text, |
|
style: TextStyle( |
|
fontSize: fontSize, |
|
height: height, |
|
), |
|
), |
|
textDirection: TextDirection.ltr, |
|
)..layout(minWidth: 0, maxWidth: width); |
|
return textPainter.height; |
|
} |
|
|
|
Widget chatDetailsItem(Message message, index, animationKey) { |
|
bool isSelf = message.fromId == selfUserId; |
|
bool isText = message.msgType == 1; |
|
bool isImage = message.msgType == 2; |
|
GlobalKey _buttonKey = GlobalKey(); |
|
return Container( |
|
padding: EdgeInsets.only( |
|
top: 16, |
|
bottom: 16, |
|
), |
|
child: Column( |
|
children: [ |
|
if (message.showTime) |
|
Text( |
|
// position == messages.length-1 |
|
// ? |
|
AppUtils.milliTimeFormatter(DateTime.fromMillisecondsSinceEpoch( |
|
int.parse(message.time))) |
|
// : AppUtils.getTimeDisplay( |
|
// DateTime.fromMillisecondsSinceEpoch( |
|
// int.parse(messages[position].time)), |
|
// DateTime.fromMillisecondsSinceEpoch( |
|
// int.parse(messages[position+1].time))) |
|
, |
|
textAlign: TextAlign.center, |
|
style: TextStyle( |
|
color: Color(0xFFA29E9E), |
|
fontSize: 10.sp, |
|
height: 1.0, |
|
fontWeight: MyFontWeight.regular, |
|
), |
|
), |
|
// if (messages.indexOf(message) == (messages.length - 1)) |
|
// Padding( |
|
// padding: EdgeInsets.only(top: 10.h, bottom: 24.h), |
|
// child: Text( |
|
// "在对方未回复或关注你之前,你只能发送一条信息", |
|
// textAlign: TextAlign.center, |
|
// style: TextStyle( |
|
// color: Color(0xFFA29E9E), |
|
// fontSize: 10.sp, |
|
// fontWeight: MyFontWeight.regular, |
|
// ), |
|
// ), |
|
// ), |
|
// if (messages.indexOf(message) == (messages.length - 1)) |
|
// SizedBox( |
|
// height: 16.h, |
|
// ), |
|
|
|
/// not self |
|
if (!isSelf && isText) |
|
SizedBox( |
|
height: 10.h, |
|
), |
|
if (!isSelf && isText) |
|
Padding( |
|
padding: EdgeInsets.only(left: 17.w, right: 39.w), |
|
child: Row( |
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
GestureDetector( |
|
behavior: HitTestBehavior.opaque, |
|
onTap: () { |
|
if (_toUser?.mid?.isEmpty ?? true) { |
|
"用户信息错误".toast; |
|
return; |
|
} |
|
Navigator.of(context).pushNamed( |
|
'/router/personal_page', |
|
arguments: { |
|
"memberId": _toUser?.mid ?? "", |
|
"inletType": 1 |
|
}, |
|
); |
|
}, |
|
child: MImage( |
|
_toUser?.avatar ?? '', |
|
isCircle: true, |
|
height: 44, |
|
width: 44, |
|
fit: BoxFit.cover, |
|
errorSrc: "assets/image/default_1.webp", |
|
fadeSrc: "assets/image/default_1.webp", |
|
), |
|
), |
|
SizedBox( |
|
width: 12.w, |
|
), |
|
Expanded( |
|
child: Container( |
|
alignment: Alignment.centerLeft, |
|
child: GestureDetector( |
|
onLongPress: () { |
|
showMessageMenu(context, _buttonKey, message, index, loadanimatedListKey); |
|
}, |
|
child: Container( |
|
decoration: BoxDecoration( |
|
borderRadius: BorderRadius.circular(6), |
|
color: Color(0xFFFFFFFF), |
|
boxShadow: [ |
|
BoxShadow( |
|
color: Color(0xFFA8A3A3).withAlpha(12), |
|
offset: Offset(0, 4), |
|
blurRadius: 4, |
|
spreadRadius: 0, |
|
), |
|
], |
|
), |
|
key: _buttonKey, |
|
padding: EdgeInsets.symmetric( |
|
vertical: 6.h, |
|
horizontal: 12.w, |
|
), |
|
child: Text( |
|
tex = message.content, |
|
textAlign: TextAlign.left, |
|
style: TextStyle( |
|
height: 1.2.h, |
|
color: Color(0XFF0D0D0D), |
|
fontSize: 17.sp, |
|
fontWeight: MyFontWeight.medium, |
|
), |
|
), |
|
), |
|
), |
|
), |
|
), |
|
], |
|
), |
|
), |
|
|
|
/// self |
|
if (isSelf && isText) |
|
SizedBox( |
|
height: 10.h, |
|
), |
|
if (isSelf && isText) |
|
Padding( |
|
padding: EdgeInsets.only(left: 36.w, right: 16.w), |
|
child: Row( |
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
Expanded( |
|
child: Container( |
|
alignment: Alignment.centerRight, |
|
child: Row( |
|
mainAxisAlignment: MainAxisAlignment.end, |
|
children: [ |
|
if (message.state == 3) |
|
Container( |
|
decoration: BoxDecoration( |
|
borderRadius: BorderRadius.circular(100), |
|
color: Color(0xFFFF441A), |
|
), |
|
width: 20, |
|
height: 20, |
|
alignment: Alignment.center, |
|
child: Text( |
|
"!", |
|
textAlign: TextAlign.left, |
|
style: TextStyle( |
|
color: Colors.white, |
|
fontSize: 17.sp, |
|
fontWeight: MyFontWeight.regular, |
|
), |
|
), |
|
), |
|
if (message.state == 3) |
|
SizedBox( |
|
width: 12.w, |
|
), |
|
InkWell( |
|
onLongPress: () { |
|
showMessageMenu(context, _buttonKey, message, index, loadanimatedListKey); |
|
}, |
|
child: Container( |
|
decoration: BoxDecoration( |
|
borderRadius: BorderRadius.circular(6), |
|
color: Color(0xFF32A060), |
|
boxShadow: [ |
|
BoxShadow( |
|
color: Color(0xFFA8A3A3).withAlpha(12), |
|
offset: Offset(0, 4), |
|
blurRadius: 4, |
|
spreadRadius: 0, |
|
), |
|
], |
|
), |
|
key: _buttonKey, |
|
padding: EdgeInsets.symmetric( |
|
vertical: 6.h, |
|
horizontal: 12.w, |
|
), |
|
child: Text( |
|
tex = message.content, |
|
textAlign: TextAlign.left, |
|
style: TextStyle( |
|
height: 1.2.h, |
|
color: Colors.white, |
|
fontSize: 17.sp, |
|
fontWeight: MyFontWeight.medium, |
|
), |
|
), |
|
), |
|
), |
|
], |
|
), |
|
), |
|
), |
|
SizedBox( |
|
width: 12.w, |
|
), |
|
GestureDetector( |
|
behavior: HitTestBehavior.opaque, |
|
onTap: () { |
|
Navigator.of(context).pushNamed( |
|
'/router/personal_page', |
|
arguments: { |
|
"memberId": selfUserId, |
|
"inletType": 1, |
|
}, |
|
); |
|
}, |
|
child: MImage( |
|
userInfo?.headimg ?? "", |
|
isCircle: true, |
|
height: 44, |
|
width: 44, |
|
fit: BoxFit.cover, |
|
errorSrc: "assets/image/default_1.webp", |
|
fadeSrc: "assets/image/default_1.webp", |
|
), |
|
), |
|
], |
|
), |
|
), |
|
|
|
/// not self image 图片 |
|
if (!isSelf && isImage) |
|
Padding( |
|
padding: EdgeInsets.only( |
|
left: 17.w, right: 39.w, top: 10.h, |
|
), |
|
child: Row( |
|
children: [ |
|
MImage( |
|
userInfo?.headimg ?? "", |
|
isCircle: true, |
|
width: 44, |
|
height: 44, |
|
fit: BoxFit.cover, |
|
errorSrc: "assets/image/default_1.webp", |
|
fadeSrc: "assets/image/default_1.webp", |
|
), |
|
SizedBox( |
|
width: 12.w, |
|
), |
|
GestureDetector( |
|
onLongPress: () { |
|
showMessageMenu(context, _buttonKey, message, index, loadanimatedListKey); |
|
}, |
|
onTap: () { |
|
if (imageUrl.contains(message.attach) && (message.attach?.isNotEmpty ?? false)) { |
|
ImagePickers.previewImages(imageUrl, imageUrl.indexOf(message.attach!)); |
|
} |
|
}, |
|
child: FutureBuilder( |
|
future: fetchImageSize(message), |
|
key: _buttonKey, |
|
builder: (BuildContext context, AsyncSnapshot snapshot) { |
|
if (message.attach?.isEmpty ?? true) { |
|
return SizedBox( |
|
width: 180.w, |
|
height: 200.h, |
|
); |
|
} |
|
if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { |
|
return Image.file( |
|
File(message.attach!), |
|
width: 180.w, |
|
height: 200.h, |
|
alignment: Alignment.centerLeft, |
|
fit: BoxFit.contain, |
|
); |
|
} else { |
|
return Image.file( |
|
File(message.attach!), |
|
width: snapshot.data.width, |
|
height: snapshot.data.height, |
|
alignment: Alignment.centerLeft, |
|
fit: BoxFit.cover, |
|
); |
|
} |
|
}, |
|
), |
|
), |
|
Spacer(), |
|
], |
|
), |
|
), |
|
|
|
/// self image 图片 |
|
if (isSelf && isImage) |
|
Padding( |
|
padding: EdgeInsets.only( |
|
left: 39.w, |
|
right: 17.w, |
|
top: 10.h, |
|
), |
|
child: Row( |
|
children: [ |
|
Spacer(), |
|
GestureDetector( |
|
onLongPress: () { |
|
showMessageMenu(context, _buttonKey, message, index, loadanimatedListKey); |
|
}, |
|
onTap: () { |
|
if (imageUrl.contains(message.attach) && (message.attach?.isNotEmpty ?? false)) { |
|
ImagePickers.previewImages(imageUrl, imageUrl.indexOf(message.attach!)); |
|
} |
|
}, |
|
child: FutureBuilder ( |
|
future: fetchImageSize(message), |
|
key: _buttonKey, |
|
builder: (BuildContext context, AsyncSnapshot snapshot) { |
|
if (message.attach?.isEmpty ?? true) { |
|
return SizedBox( |
|
width: 180.w, |
|
height: 200.h, |
|
); |
|
} |
|
if (snapshot.connectionState == ConnectionState.waiting || snapshot.hasError) { |
|
return Image.file( |
|
File(message.attach!), |
|
width: 180.w, |
|
height: 200.h, |
|
alignment: Alignment.centerRight, |
|
fit: BoxFit.contain, |
|
); |
|
} else { |
|
return Image.file( |
|
File(message.attach!), |
|
width: snapshot.data.width, |
|
height: snapshot.data.height, |
|
alignment: Alignment.centerRight, |
|
fit: BoxFit.contain, |
|
); |
|
} |
|
}, |
|
), |
|
), |
|
SizedBox( |
|
width: 12.w, |
|
), |
|
MImage( |
|
userInfo?.headimg ?? "", |
|
isCircle: true, |
|
width: 44, |
|
height: 44, |
|
fit: BoxFit.cover, |
|
errorSrc: "assets/image/default_1.webp", |
|
fadeSrc: "assets/image/default_1.webp", |
|
), |
|
], |
|
), |
|
), |
|
|
|
/// no reply long time |
|
// if (messages.indexOf(message) == 0) |
|
// Padding( |
|
// padding: EdgeInsets.only(left: 17.w, right: 17.w, top: 24.h), |
|
// child: Text( |
|
// "由于对方没有回复你,你只能发送一条消息,需要对方关注或回复后才能恢复正常聊天", |
|
// textAlign: TextAlign.center, |
|
// style: TextStyle( |
|
// color: Color(0xFFA29E9E), |
|
// fontSize: 10.sp, |
|
// fontWeight: MyFontWeight.regular, |
|
// ), |
|
// ), |
|
// ), |
|
], |
|
), |
|
); |
|
} |
|
|
|
/// 显示消息菜单 |
|
showMessageMenu(context, _buttonKey, message, index, animatedListKey) { |
|
SmartDialog.showAttach( |
|
targetContext: _buttonKey.currentContext, |
|
alignment: Alignment.topCenter, |
|
maskColor: Colors.transparent, |
|
animationType: SmartAnimationType.fade, |
|
builder: (ctx) { |
|
return Stack( |
|
alignment: Alignment.bottomCenter, |
|
children: [ |
|
Container( |
|
padding: EdgeInsets.only(bottom: 16), |
|
child: Container( |
|
decoration: BoxDecoration( |
|
color: Color(0xFF2A2A2A), |
|
borderRadius: BorderRadius.circular(6), |
|
), |
|
padding: EdgeInsets.symmetric( |
|
horizontal: 24.w, |
|
vertical: 7.5.h, |
|
), |
|
child: IntrinsicWidth( |
|
child: Row( |
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
children: [ |
|
GestureDetector( |
|
onTap: () { |
|
this.copy(message.content); |
|
SmartDialog.dismiss(status: SmartStatus.attach); |
|
}, |
|
child: Column( |
|
children: [ |
|
Image.asset( |
|
"assets/image/icon_chat_copy.webp", |
|
height: 16, |
|
width: 16, |
|
), |
|
SizedBox( |
|
height: 2.h, |
|
), |
|
Text( |
|
"复制", |
|
textAlign: |
|
TextAlign.center, |
|
style: TextStyle( |
|
color: Colors.white, |
|
fontSize: 12.sp, |
|
fontWeight: MyFontWeight |
|
.regular, |
|
), |
|
), |
|
], |
|
), |
|
), |
|
24.w.vd, |
|
GestureDetector( |
|
onTap: () async { |
|
await hxDatabase.deleteByMsgId("${message.id}"); |
|
if (animatedListKey == loadanimatedListKey) { |
|
loadmessages.remove(message); |
|
animatedListKey.currentState?.removeItem(index, (context, animation) {return Container();}); |
|
} else { |
|
newmessages.remove(message); |
|
animatedListKey.currentState?.removeItem(index, (context, animation) {return Container();}); |
|
} |
|
SmartDialog.dismiss(status: SmartStatus.attach); |
|
}, |
|
child: Column( |
|
children: [ |
|
Image.asset( |
|
"assets/image/icon_chat_delete.webp", |
|
height: 16, |
|
width: 16, |
|
), |
|
SizedBox( |
|
height: 2.h, |
|
), |
|
Text( |
|
S.of(context).shanchu, |
|
textAlign: TextAlign.center, |
|
style: TextStyle( |
|
color: Colors.white, |
|
fontSize: 12.sp, |
|
fontWeight: MyFontWeight.regular, |
|
), |
|
), |
|
], |
|
), |
|
), |
|
], |
|
), |
|
), |
|
), |
|
), |
|
Image.asset( |
|
"assets/image/icon_copy_j.webp", |
|
height: 17, |
|
fit: BoxFit.fill, |
|
width: 17, |
|
), |
|
], |
|
); |
|
}, |
|
); |
|
|
|
} |
|
|
|
Future<Size> fetchImageSize(Message message) async { |
|
String? imageLocalPath = message.attach; |
|
String imageUrl = message.content; |
|
debugPrint("$imageLocalPath"); |
|
debugPrint("$imageUrl"); |
|
|
|
if (imageLocalPath?.isEmpty ?? true) { |
|
return Future.value(Size.zero); |
|
} |
|
|
|
File file = File(imageLocalPath!); |
|
if (!(await file.exists())) { |
|
await qiniu.downloadFile(imageUrl, savePath: imageLocalPath); |
|
} |
|
|
|
Size size = Size.zero; |
|
|
|
String? sizeStr = SharedInstance.instance.getSize(imageLocalPath); |
|
if (sizeStr != null && sizeStr != "") { |
|
Map<String, dynamic> sizeMap = jsonDecode(sizeStr); |
|
size = Size(sizeMap["width"], sizeMap["height"]); |
|
if (size != Size.zero) { |
|
return size; |
|
} |
|
} |
|
|
|
Completer<Size> completer = Completer(); |
|
Image.file(file) |
|
.image |
|
.resolve(ImageConfiguration()) |
|
.addListener(ImageStreamListener((image, synchronousCall) { |
|
size = Size((image.image.width).toDouble(), |
|
(image.image.height).toDouble()); |
|
size = resizeImage(size); |
|
Map<String, double> sizeMap = { |
|
"width": size.width, |
|
"height": size.height, |
|
}; |
|
SharedInstance.instance.setSize(imageLocalPath, jsonEncode(sizeMap)); |
|
completer.complete(size); |
|
})); |
|
return completer.future; |
|
} |
|
|
|
resizeImage(Size size) { |
|
if (size.width > 180 || size.height > 200) { |
|
if (size.width > size.height) { |
|
int height = size.height ~/ (size.width ~/ 180); |
|
size = Size(180, height.toDouble()); |
|
} else { |
|
int width = size.width ~/ (size.height ~/ 200); |
|
size = Size(width.toDouble(), 200); |
|
} |
|
} |
|
return size; |
|
} |
|
|
|
///富文本输入框 |
|
Widget input() { |
|
return Container( |
|
color: Color(0xFFFDFCFC), |
|
// duration: 400.milliseconds, |
|
height: inputWidgetHeight, |
|
padding: EdgeInsets.only(top: 14, bottom: 15), |
|
child: Column( |
|
children: [ |
|
///输入框 |
|
Row( |
|
children: [ |
|
Expanded( |
|
flex: 1, |
|
child: Container( |
|
margin: EdgeInsets.only( |
|
left: 20.w, |
|
), |
|
decoration: BoxDecoration( |
|
color: Color(0xFFF6F6F6), |
|
borderRadius: BorderRadius.circular(6), |
|
), |
|
child: Container( |
|
margin: EdgeInsets.only(left: 17.w), |
|
alignment: Alignment.topLeft, |
|
child: TextField( |
|
textInputAction: TextInputAction.send, |
|
onTap: () { |
|
moreShow = false; |
|
_onTextFieldTap(); |
|
}, |
|
onEditingComplete: () { |
|
var commentText = chatController.text; |
|
if (commentText.trim() == "") { |
|
return; |
|
} |
|
socketClient.sendMessage(_toUser?.mid ?? '', commentText) |
|
.then((value) { |
|
Message message = value; |
|
newmessages.add(message); |
|
messageShowTime().then((value) { |
|
newanimatedListKey.currentState?.insertItem(newmessages.length - 1); |
|
chatController.clear(); |
|
jumpToBottom(); |
|
}); |
|
}); |
|
}, |
|
maxLines: 8, |
|
minLines: 1, |
|
focusNode: commentFocus, |
|
controller: chatController, |
|
decoration: InputDecoration( |
|
border: InputBorder.none, |
|
hintText: hintText, |
|
hintStyle: TextStyle( |
|
fontSize: 14.sp, |
|
color: Color(0xFF868686), |
|
), |
|
), |
|
), |
|
), |
|
), |
|
), |
|
GestureDetector( |
|
behavior: HitTestBehavior.opaque, |
|
onTap: () { |
|
_onSmileyTap(); |
|
}, |
|
child: Container( |
|
padding: EdgeInsets.only(left: 15.w, right: 8.w), |
|
child: Image.asset( |
|
"assets/image/icon_chat_emo.webp", |
|
height: 26, |
|
width: 26, |
|
), |
|
), |
|
), |
|
GestureDetector( |
|
behavior: HitTestBehavior.opaque, |
|
onTap: () { |
|
_onMoreTap(); |
|
}, |
|
child: Container( |
|
padding: EdgeInsets.only(left: 8.w, right: 19.w), |
|
child: Image.asset( |
|
"assets/image/chat_more.webp", |
|
height: 26, |
|
width: 26, |
|
), |
|
), |
|
), |
|
], |
|
), |
|
SizedBox( |
|
height: 16, |
|
), |
|
|
|
///表情 |
|
if (emojiShowing) |
|
Container( |
|
height: keyboard == -1 ? 270 : keyboard, |
|
width: MediaQuery.of(context).size.width, |
|
child: EmojiPicker( |
|
textEditingController: chatController, |
|
config: Config( |
|
emojiViewConfig: EmojiViewConfig( |
|
columns: 7, |
|
emojiSizeMax: 28 * (Platform.isIOS ? 1.10 : 1.0), |
|
verticalSpacing: 0, |
|
horizontalSpacing: 0, |
|
backgroundColor: const Color(0xFFF2F2F2), |
|
gridPadding: EdgeInsets.zero, |
|
recentsLimit: 35, |
|
replaceEmojiOnLimitExceed: false, |
|
noRecents: Text( |
|
"最近使用", |
|
style: TextStyle(fontSize: 20, color: Colors.black26), |
|
textAlign: TextAlign.center, |
|
), |
|
loadingIndicator: const SizedBox.shrink(), |
|
buttonMode: ButtonMode.MATERIAL, |
|
), |
|
skinToneConfig: SkinToneConfig( |
|
enabled: true, |
|
dialogBackgroundColor: Colors.white, |
|
indicatorColor: Colors.grey, |
|
), |
|
categoryViewConfig: CategoryViewConfig( |
|
initCategory: Category.RECENT, |
|
iconColor: Colors.grey, |
|
iconColorSelected: Colors.blue, |
|
backspaceColor: Colors.blue, |
|
categoryIcons: const CategoryIcons(), |
|
tabIndicatorAnimDuration: Duration(milliseconds: 0), |
|
), |
|
bottomActionBarConfig: const BottomActionBarConfig( |
|
backgroundColor: Colors.transparent, |
|
buttonIconColor: Colors.blue, |
|
showBackspaceButton: true, |
|
showSearchViewButton: false, |
|
), |
|
swapCategoryAndBottomBar: false, |
|
checkPlatformCompatibility: true, |
|
), |
|
), |
|
), |
|
|
|
///更多 |
|
if (moreShow) |
|
Container( |
|
height: keyboard == -1 ? 167 : keyboard, |
|
child: Column( |
|
children: [ |
|
Container( |
|
width: double.infinity, |
|
height: 1.h, |
|
color: Color(0xFFF7F7F7), |
|
margin: EdgeInsets.only(bottom: 10.h), |
|
), |
|
Row( |
|
children: [ |
|
GestureDetector( |
|
behavior: HitTestBehavior.opaque, |
|
onTap: () { |
|
openCamera(); |
|
}, |
|
child: Column( |
|
children: [ |
|
Container( |
|
decoration: BoxDecoration( |
|
borderRadius: BorderRadius.circular(6), |
|
color: Color(0xFFFFFFFF), |
|
boxShadow: [ |
|
BoxShadow( |
|
color: Color(0xFFD5D5D5).withAlpha(25), |
|
offset: Offset(4, 2), |
|
blurRadius: 4, |
|
spreadRadius: 0, |
|
) |
|
], |
|
), |
|
margin: EdgeInsets.only(right: 15.w, left: 14.w), |
|
padding: EdgeInsets.all(12), |
|
child: Image.asset( |
|
"assets/image/icon_chat_camera.webp", |
|
height: 24, |
|
width: 24, |
|
), |
|
), |
|
Padding( |
|
padding: EdgeInsets.only(top: 8.h), |
|
child: Text( |
|
"拍照", |
|
style: TextStyle( |
|
fontSize: 12.sp, |
|
color: Color(0xFFA29E9E), |
|
fontWeight: MyFontWeight.regular, |
|
), |
|
), |
|
), |
|
], |
|
), |
|
), |
|
GestureDetector( |
|
behavior: HitTestBehavior.opaque, |
|
onTap: () { |
|
getImageOrVideo(GalleryMode.image); |
|
}, |
|
child: Column( |
|
children: [ |
|
Container( |
|
decoration: BoxDecoration( |
|
borderRadius: BorderRadius.circular(6), |
|
color: Color(0xFFFFFFFF), |
|
boxShadow: [ |
|
BoxShadow( |
|
color: Color(0xFFD5D5D5).withAlpha(25), |
|
offset: Offset(4, 2), |
|
blurRadius: 4, |
|
spreadRadius: 0, |
|
) |
|
], |
|
), |
|
padding: EdgeInsets.all(12), |
|
child: Image.asset( |
|
"assets/image/icon_chat_photo.webp", |
|
height: 24, |
|
width: 24, |
|
), |
|
), |
|
Padding( |
|
padding: EdgeInsets.only(top: 8.h), |
|
child: Text( |
|
"相册", |
|
style: TextStyle( |
|
fontSize: 12.sp, |
|
color: Color(0xFFA29E9E), |
|
fontWeight: MyFontWeight.regular, |
|
), |
|
), |
|
), |
|
], |
|
), |
|
), |
|
], |
|
), |
|
], |
|
), |
|
), |
|
], |
|
), |
|
); |
|
} |
|
|
|
/// 复制文本 |
|
copy(String tex) { |
|
debugPrint(tex); |
|
Clipboard.setData(ClipboardData(text: tex)); |
|
} |
|
|
|
}
|
|
|