Hibok
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 
 

741 řádky
23 KiB

  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:chat/home/audio_chat_view.dart';
  4. import 'package:chat/utils/screen.dart';
  5. import 'package:chat/utils/upload_util.dart';
  6. import 'package:fixnum/fixnum.dart';
  7. import 'package:cached_network_image/cached_network_image.dart';
  8. import 'package:chat/chat/translate_state.dart';
  9. import 'package:chat/data/UserData.dart';
  10. import 'package:chat/data/chat_data_mgr.dart';
  11. import 'package:chat/data/constants.dart';
  12. import 'package:chat/generated/i18n.dart';
  13. import 'package:chat/home/add_friend.dart';
  14. import 'package:chat/models/ChatMsg.dart';
  15. import 'package:chat/models/UserInfo.dart';
  16. import 'package:chat/models/keyboard_provider.dart';
  17. import 'package:chat/models/ref_name_provider.dart';
  18. import 'package:chat/models/voucher_change.dart';
  19. import 'package:chat/proto/all.pbserver.dart';
  20. import 'package:chat/utils/CustomUI.dart';
  21. import 'package:chat/utils/HttpUtil.dart';
  22. import 'package:chat/utils/MessageMgr.dart';
  23. import 'package:chat/utils/TokenMgr.dart';
  24. import 'package:chat/utils/analyze_utils.dart';
  25. import 'package:chat/utils/app_navigator.dart';
  26. import 'package:chat/utils/blacklist_mgr.dart';
  27. import 'package:chat/utils/count_down_button.dart';
  28. import 'package:chat/utils/friend_list_mgr.dart';
  29. import 'package:chat/utils/msgHandler.dart';
  30. import 'package:chat/utils/net_state_widget.dart';
  31. import 'package:chat/utils/sound_util.dart';
  32. import 'package:chat/utils/sp_utils.dart';
  33. import 'package:chat/utils/sql_util.dart';
  34. import 'package:dio/dio.dart';
  35. import 'package:extended_text/extended_text.dart';
  36. import 'package:flutter/cupertino.dart';
  37. import 'package:flutter/material.dart';
  38. import 'package:oktoast/oktoast.dart';
  39. import 'package:provider/provider.dart';
  40. import 'package:scroll_to_index/scroll_to_index.dart';
  41. //import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
  42. import '../r.dart';
  43. import 'ChatPageItem.dart';
  44. import 'input_bar.dart';
  45. import 'package:chat/utils/PopUpMenu.dart' as myPop;
  46. import 'package:chat/models/money_change.dart';
  47. class ChatPage extends StatefulWidget {
  48. final int friendId;
  49. final int enterType; // 0默认 1图片
  50. final dynamic enterContent;
  51. final bool isTranslateButler;
  52. ChatPage(
  53. {Key key,
  54. this.friendId,
  55. this.enterType = 0,
  56. this.enterContent,
  57. this.isTranslateButler = false})
  58. : super(key: key);
  59. _ChatPageState createState() => _ChatPageState();
  60. }
  61. class _ChatPageState extends State<ChatPage> {
  62. MessageMgr msgMgr = MessageMgr();
  63. UserInfo friendInfo;
  64. List<MsgModel> msgList;
  65. KeyboardIndexProvider _keyboardIndexProvider = KeyboardIndexProvider();
  66. TextEditingController nickNameController = new TextEditingController();
  67. //统计聊天时长
  68. int startTime;
  69. int jumpTime;
  70. AutoScrollController controller;
  71. @override
  72. void dispose() {
  73. var endTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  74. AnalyzeUtils.commitChatDuration(startTime, endTime);
  75. MessageMgr().off('Send CoinBag', _sendCoin);
  76. msgMgr.off('New Chat Message', receiveMsg);
  77. msgMgr.off('Keyboard Hide', dealWithKeyboardHide);
  78. msgMgr.off('Delete Select Message', _deleteItem);
  79. msgMgr.off('Jump to Msg', jumpToMsg);
  80. MsgHandler.curActiveSession = 0;
  81. SoundUtils().stop();
  82. nickNameController.dispose();
  83. super.dispose();
  84. }
  85. jumpToMsg(time) async {
  86. hideKeyBoard();
  87. int jumIndex = 0;
  88. jumpTime = time;
  89. for (int i = 0; i < msgList.length; i++) {
  90. if (time == msgList[i].time) {
  91. jumIndex = i;
  92. break;
  93. }
  94. }
  95. if (jumIndex < 0) {
  96. jumIndex = 0;
  97. }
  98. if (jumIndex > msgList.length - 1) {
  99. jumIndex = msgList.length - 1;
  100. }
  101. await controller.scrollToIndex(jumIndex,
  102. preferPosition: AutoScrollPosition.begin);
  103. controller.highlight(jumIndex,
  104. highlightDuration: new Duration(milliseconds: 100));
  105. }
  106. @override
  107. void initState() {
  108. super.initState();
  109. print('init chatpage');
  110. getDefaultSetting();
  111. getUserInfo();
  112. controller = AutoScrollController(
  113. viewportBoundaryGetter: () =>
  114. Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
  115. axis: Axis.vertical);
  116. startTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  117. msgList = ChatDataMgr().getRecord();
  118. msgMgr.on('New Chat Message', receiveMsg);
  119. msgMgr.on('Keyboard Hide', dealWithKeyboardHide);
  120. msgMgr.on('Send CoinBag', _sendCoin);
  121. msgMgr.on('Delete Select Message', _deleteItem);
  122. msgMgr.on('Jump to Msg', jumpToMsg);
  123. }
  124. void _sendFile(File file) async {
  125. // File file = await FilePicker.getFile();
  126. int fileSize = file.lengthSync();
  127. print('选择的文件 ${file.path} 大小 $fileSize');
  128. if (fileSize > 33 * 1024 * 1024) {
  129. showToast(I18n.of(context).max_file.replaceFirst('/s1', 33.toString()));
  130. return;
  131. }
  132. var fileName = file.path.split('/').last;
  133. print('fileName $fileName');
  134. var ext = '';
  135. var extList = fileName.split('.');
  136. if (extList.length > 1) {
  137. ext = extList.last;
  138. }
  139. print('ext $ext');
  140. var fileMsg = FileChat.create();
  141. fileMsg.type = ext;
  142. fileMsg.size = fileSize;
  143. fileMsg.name = fileName;
  144. var msg = MsgHandler.createSendMsg(
  145. ChatType.FileChatType, fileMsg.writeToBuffer(),
  146. friendId: widget.friendId,
  147. localFile: file.path,
  148. channelType: ChatChannelType.Session);
  149. sendMsg(msg);
  150. }
  151. _sendCoin(args) async {
  152. var res = await _checkCoinBeforeSend(args['amount']);
  153. if (res != null) {
  154. args['redNo'] = res['redNo'];
  155. args['friendId'] = widget.friendId;
  156. var msg = MsgHandler.createCoinBagMsg(args);
  157. sendMsg(msg);
  158. }
  159. }
  160. //校验
  161. _checkCoinBeforeSend(int amount) async {
  162. Map data = {
  163. "userId": UserData().basicInfo.userId,
  164. "rUserId": friendInfo.userId,
  165. "price": amount,
  166. };
  167. data['sign'] = TokenMgr().getSign(data);
  168. Response res = await HttpUtil().post('red/packet/send', data: data);
  169. if (res == null) {
  170. return null;
  171. }
  172. Map resData = res.data;
  173. if (resData['code'] == 0) {
  174. print(resData['data']);
  175. return resData['data'];
  176. }
  177. return null;
  178. }
  179. void getDefaultSetting() async {
  180. bool soundPlayMode =
  181. (await SPUtils.getBool(Constants.SOUND_PLAY_MODE)) ?? false;
  182. _keyboardIndexProvider.init(soundPlayMode);
  183. }
  184. dealWithKeyboardHide(args) {
  185. if (_keyboardIndexProvider.curKeyboardIndex == 0) {
  186. hideKeyBoard();
  187. }
  188. }
  189. getUserInfo() async {
  190. //先从本地获取用户信息
  191. friendInfo = await HttpUtil().getFriendInfo(widget.friendId, true);
  192. //如果是新的聊天,向服务器发送创建会话消息
  193. if (msgList.length == 0) {
  194. MsgHandler.getActiveSesstion(
  195. [friendInfo.userId, UserData().basicInfo.userId]);
  196. }
  197. if (mounted) {
  198. setState(() {});
  199. }
  200. WidgetsBinding.instance.addPostFrameCallback((_) {
  201. if (widget.enterType == 1) {
  202. print('接收到的:${widget.enterContent}');
  203. _sendFile(File(widget.enterContent));
  204. } else if (widget.enterType == 2 || widget.enterType == 3) {
  205. //转发消息
  206. MsgModel originMsg = widget.enterContent;
  207. MsgModel msg = MsgHandler.createSendMsg(
  208. ChatType.valueOf(originMsg.msgType), originMsg.msgContent);
  209. msg.extraInfo = originMsg.extraInfo;
  210. if (originMsg.extraFile == null ||
  211. originMsg.extraFile.contains('http')) {
  212. msg.extraFile = originMsg.extraFile;
  213. } else {
  214. msg.extraFile = UploadUtil().getFullUrl(
  215. originMsg.extraFile, originMsg.sessionId, originMsg.channelType);
  216. }
  217. msg.localFile = originMsg.localFile;
  218. msg.friendId = widget.friendId;
  219. if (msg.localFile != null) {
  220. msg.state = MsgState.Uploaded;
  221. }
  222. sendMsg(msg);
  223. }
  224. });
  225. }
  226. @override
  227. Widget build(BuildContext context) {
  228. print('build chatpage');
  229. if (friendInfo == null) {
  230. return Scaffold(
  231. backgroundColor: const Color(0xFFE2E9F1),
  232. body: SafeArea(
  233. child: Center(
  234. child: CircularProgressIndicator(),
  235. ),
  236. ),
  237. );
  238. }
  239. List<Widget> actions = [];
  240. int voucher = Provider.of<VoucherChangeProvider>(context).voucher;
  241. actions.add(Row(
  242. children: <Widget>[
  243. CustomUI.buildImageLabel("assets/images/voucher.png", voucher,
  244. imgOpc: 0.5, imgHeight: 13),
  245. CustomUI.buildImageLabel(
  246. R.assetsImagesCoin, Provider.of<MoneyChangeProvider>(context).money,
  247. isLeft: false)
  248. ],
  249. ));
  250. actions.add(TranslateSateWidget(friendId: friendInfo.userId));
  251. actions.add(Container(
  252. margin: EdgeInsets.only(top: 1),
  253. child: myPop.PopupMenuButton(
  254. icon: Icon(
  255. Icons.more_horiz,
  256. size: 24,
  257. ),
  258. offset: Offset(0, 100),
  259. onSelected: (int index) {
  260. if (index == 2) {
  261. AppNavigator.pushInformUserPage(
  262. context, friendInfo.sex == 1, friendInfo.userId);
  263. } else if (index == 1) {
  264. Navigator.of(context).push(
  265. new MaterialPageRoute(
  266. builder: (context) {
  267. return AddFriendPage(
  268. userId: friendInfo.userId,
  269. pageType: SendMessagePageType.Remark,
  270. originalName: Provider.of<RefNameProvider>(context)
  271. .getRefName(
  272. friendInfo.userId, friendInfo.nickName));
  273. },
  274. ),
  275. );
  276. }
  277. },
  278. itemBuilder: (BuildContext context) {
  279. return <myPop.PopupMenuItem<int>>[
  280. myPop.PopupMenuItem(
  281. child: Container(
  282. alignment: Alignment.center,
  283. color: Colors.white,
  284. padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
  285. child: Text(I18n.of(context).anonymous_report,
  286. textScaleFactor: 1.0,
  287. maxLines: 1,
  288. style: TextStyle(
  289. color: Color(AppColors.AppBarColor), fontSize: 12)),
  290. ),
  291. value: 2,
  292. ),
  293. myPop.PopupMenuItem(
  294. child: Container(
  295. decoration: BoxDecoration(
  296. border: Border(
  297. top: BorderSide(width: 1, color: Colors.grey[300])),
  298. color: Colors.white,
  299. ),
  300. alignment: Alignment.center,
  301. padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
  302. child: Text(I18n.of(context).Remark,
  303. textScaleFactor: 1.0,
  304. maxLines: 1,
  305. style: TextStyle(
  306. color: Color(AppColors.AppBarColor), fontSize: 12)),
  307. ),
  308. value: 1,
  309. ),
  310. ];
  311. })));
  312. return Stack(
  313. children: <Widget>[
  314. MultiProvider(
  315. providers: [
  316. ChangeNotifierProvider(create: (_) => _keyboardIndexProvider),
  317. Provider<bool>.value(value: false),
  318. Provider<int>.value(value: widget.friendId),
  319. ],
  320. child: GestureDetector(
  321. onTap: hideKeyBoard,
  322. child: ExtendedTextSelectionPointerHandler(
  323. ///选择文字,消除弹窗
  324. builder: (states) {
  325. return Listener(
  326. child: Scaffold(
  327. resizeToAvoidBottomInset: false,
  328. backgroundColor: const Color(0xFFE2E9F1),
  329. appBar: AppBar(
  330. title: Text(
  331. '${Provider.of<RefNameProvider>(context).getRefName(friendInfo.userId, friendInfo.nickName)}',
  332. textScaleFactor: 1.0,
  333. style: TextStyle(
  334. color: Constants.BlackTextColor,
  335. fontSize: 16.47),
  336. ),
  337. leading: CustomUI.buildCustomLeading(context),
  338. titleSpacing: -10,
  339. centerTitle: false,
  340. elevation: 1,
  341. actions: actions),
  342. body: SafeArea(
  343. child: Column(
  344. children: <Widget>[
  345. NetStateWidget(),
  346. widget.isTranslateButler
  347. ? _buildTranslationButler()
  348. : Container(),
  349. Expanded(child: _buildMessageList()),
  350. InputBar(
  351. sendMsg: sendMsg,
  352. isTranslateHK: widget.isTranslateButler,
  353. ),
  354. ],
  355. ))),
  356. behavior: HitTestBehavior.translucent,
  357. onPointerDown: (value) {
  358. for (var state in states) {
  359. if (!state.containsPosition(value.position)) {
  360. //clear other selection
  361. state.clearSelection();
  362. }
  363. }
  364. },
  365. onPointerMove: (value) {
  366. //clear other selection
  367. for (var state in states) {
  368. if (!state.containsPosition(value.position)) {
  369. //clear other selection
  370. state.clearSelection();
  371. }
  372. }
  373. },
  374. );
  375. },
  376. ))),
  377. // widget.isTranslateButler ? getAudioChatView() : Container(),
  378. // widget.isTranslateButler ? zoomAudioButton() : Container()
  379. ],
  380. );
  381. }
  382. Widget getAudioChatView() {
  383. if (friendInfo == null) {
  384. return Container();
  385. }
  386. return isActive == null
  387. ? Container()
  388. : Offstage(
  389. offstage: !isShowAudio,
  390. child: AudioChatPage(
  391. userInfo: friendInfo,
  392. isTranslateButler: true,
  393. translateButlerCloseCallBack: audioChatPageCallBack,
  394. ),
  395. );
  396. }
  397. audioChatPageCallBack(int args) {
  398. switch (args) {
  399. case 1:
  400. setState(() {
  401. isShowAudio = !isShowAudio;
  402. isShowZoomButton = true;
  403. });
  404. break;
  405. case 2:
  406. setState(() {
  407. isActive = false;
  408. });
  409. break;
  410. }
  411. }
  412. bool isShowAudio = false;
  413. ///控住连麦窗口是否显示
  414. bool isActive = true;
  415. ///控制连麦是否关闭
  416. bool isShowZoomButton = true;
  417. GlobalKey mykey = GlobalKey();
  418. double dx = 0, dy = 0;
  419. double zoomButtonSize = 60;
  420. void dragEvent(DragUpdateDetails details) {
  421. final RenderObject box = context.findRenderObject();
  422. // 获得自定义Widget的大小,用来计算Widget的中心锚点
  423. dx = details.globalPosition.dx - mykey.currentContext.size.width / 2;
  424. dy = details.globalPosition.dy - mykey.currentContext.size.height / 2;
  425. print('dx $dx dy $dy screen width:${Screen.width}');
  426. if (dx > Screen.width - zoomButtonSize) {
  427. dx = Screen.width - zoomButtonSize;
  428. } else if (dx < 0) {
  429. dx = 0;
  430. }
  431. if (dy > Screen.height - zoomButtonSize) {
  432. dy = Screen.height - zoomButtonSize;
  433. } else if (dy < 0) {
  434. dy = 0;
  435. }
  436. setState(() {});
  437. }
  438. Widget zoomAudioButton() {
  439. friendInfo == null
  440. ? Container()
  441. : AudioChatPage(
  442. userInfo: friendInfo,
  443. );
  444. if (friendInfo == null || !isShowZoomButton) {
  445. return Container();
  446. }
  447. Widget button = Container(
  448. key: mykey,
  449. color: Colors.blue,
  450. width: 60,
  451. height: 60,
  452. );
  453. return GestureDetector(
  454. onHorizontalDragUpdate: dragEvent,
  455. onVerticalDragUpdate: dragEvent,
  456. onTap: () {
  457. setState(() {
  458. isShowAudio = !isShowAudio;
  459. isShowZoomButton = !isShowZoomButton;
  460. });
  461. },
  462. child: Container(
  463. child: Transform.translate(
  464. offset: Offset(dx, dy),
  465. child: Align(
  466. alignment: Alignment.topLeft,
  467. child: button,
  468. ),
  469. ),
  470. ),
  471. );
  472. }
  473. Widget _buildTranslationButler() {
  474. bool hasHeadImg = true;
  475. if (friendInfo.headimgurl == null || friendInfo.headimgurl.length == 0) {
  476. hasHeadImg = false;
  477. }
  478. /// 5H币/1分钟
  479. String coinTIme = I18n.of(context).translation_butler_coin_time;
  480. coinTIme = coinTIme.replaceAll('/s1', '5');
  481. coinTIme = coinTIme.replaceAll('/s2', '1');
  482. return Container(
  483. padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
  484. color: Colors.white,
  485. child: Row(
  486. children: <Widget>[
  487. ClipRRect(
  488. borderRadius: BorderRadius.circular(8),
  489. child: hasHeadImg
  490. ? CachedNetworkImage(
  491. imageUrl: friendInfo.headimgurl,
  492. placeholder: (context, url) => Image.asset(
  493. Constants.DefaultHeadImgUrl,
  494. width: 54,
  495. height: 54,
  496. ),
  497. width: 54,
  498. height: 54,
  499. )
  500. : SizedBox(
  501. width: 54,
  502. height: 54,
  503. child: Image.asset(R.assetsImagesDefaultNorAvatar))),
  504. Padding(
  505. padding: EdgeInsets.only(left: 10),
  506. child: Container(
  507. child: Column(
  508. crossAxisAlignment: CrossAxisAlignment.start,
  509. children: <Widget>[
  510. Text(
  511. I18n.of(context).translation_butler,
  512. textScaleFactor: 1.0,
  513. overflow: TextOverflow.ellipsis,
  514. maxLines: 1,
  515. style: TextStyle(
  516. color: AppColors.NewAppbarTextColor, fontSize: 15),
  517. ),
  518. SizedBox(
  519. height: 5,
  520. ),
  521. Text(
  522. coinTIme,
  523. textScaleFactor: 1.0,
  524. maxLines: 1,
  525. style: TextStyle(color: Color(0xFF797979), fontSize: 13),
  526. )
  527. ],
  528. ),
  529. constraints: BoxConstraints(maxWidth: 135),
  530. ),
  531. ),
  532. Expanded(
  533. child: Container(
  534. constraints: BoxConstraints(maxWidth: 130),
  535. width: double.maxFinite,
  536. child: CountDownButton(
  537. I18n.of(context).translation_butler_end_service,
  538. () {
  539. Navigator.of(context).pop();
  540. },
  541. countDownTime: 3600,
  542. align: Alignment.centerRight,
  543. onPress: () {},
  544. ),
  545. // alignment: Alignment(1,0),
  546. )),
  547. ],
  548. ),
  549. );
  550. }
  551. Widget _buildMessageList() {
  552. return Container(
  553. alignment: Alignment.topCenter,
  554. child: msgList.length == 0
  555. ? Padding(
  556. padding: EdgeInsets.all(8),
  557. child: Text(
  558. I18n.of(context).chat_tips,
  559. textAlign: TextAlign.center,
  560. textScaleFactor: 1.0,
  561. style: TextStyle(color: Colors.grey, fontSize: 12),
  562. ))
  563. : NotificationListener(
  564. child: Scrollbar(
  565. child: ListView.builder(
  566. controller: controller,
  567. physics: new ClampingScrollPhysics(),
  568. itemCount: msgList.length,
  569. itemBuilder: _buildItem,
  570. padding: EdgeInsets.symmetric(vertical: 8),
  571. reverse: true,
  572. shrinkWrap: true,
  573. )),
  574. onNotification: (notification) {
  575. if (notification is ScrollNotification) {}
  576. return true;
  577. },
  578. ),
  579. );
  580. }
  581. hideKeyBoard() {
  582. _keyboardIndexProvider.changeSelectIndex(-1);
  583. }
  584. readOnly() {
  585. _keyboardIndexProvider.changeReadOnlyKey(true);
  586. }
  587. sendMsg(MsgModel msg) async {
  588. // if(widget.isTranslateButler){ ///翻译管家聊天通道
  589. // msg.channelType = ChatChannelType.TransHK.value;
  590. // }
  591. print('对方是否拉黑你 ${friendInfo.isBlackened}');
  592. if (BlacklistMgr.isBlack(friendInfo.userId)) {
  593. return;
  594. }
  595. print('chat page session id:${msg.sessionId}');
  596. if (!friendInfo.isCanStrangerNews &&
  597. !FriendListMgr().isMyFriend(friendInfo.userId)) {
  598. showToast(I18n.of(context).stranger_close_tips);
  599. return;
  600. }
  601. MsgHandler.insertMsgToDB(msg);
  602. MsgHandler.sendChatMsg(msg);
  603. if (mounted) {
  604. setState(() {});
  605. await controller.scrollToIndex(0,
  606. preferPosition: AutoScrollPosition.begin);
  607. }
  608. }
  609. MsgModel msg;
  610. int count = 0;
  611. testBig(MsgModel msg) async {
  612. for (int k = 0; k < 100; k++) {
  613. msg.msgContent = utf8.encode('测试$count');
  614. Int64 time = Int64((DateTime.now()).millisecondsSinceEpoch);
  615. msg.time = time.toInt();
  616. MsgHandler.insertMsgToDB(msg);
  617. MsgHandler.sendChatMsg(msg);
  618. await Future.delayed(Duration(milliseconds: 500), () {});
  619. count++;
  620. }
  621. count = 0;
  622. print('发送完毕');
  623. showToast('发送完毕');
  624. }
  625. void receiveMsg(args) {
  626. if (mounted) {
  627. setState(() {});
  628. }
  629. }
  630. _deleteItem(msg) {
  631. MessageMgr().emit('Cancel Request', msg);
  632. print('#### 开始删除--');
  633. msgList.remove(msg);
  634. setState(() {});
  635. SqlUtil().deleteSigleRecordWith(msg.sessionId, msg.time);
  636. }
  637. Widget _wrapScrollTag({int index, Widget child}) => AutoScrollTag(
  638. key: ValueKey(index),
  639. controller: controller,
  640. index: index,
  641. child: child,
  642. highlightColor: Colors.white.withOpacity(1),
  643. );
  644. Widget _buildItem(BuildContext context, int index) {
  645. var lastMsgTime;
  646. if (index < msgList.length - 1) {
  647. lastMsgTime = msgList[index + 1].time;
  648. }
  649. MsgModel msg = msgList[index];
  650. return _wrapScrollTag(
  651. index: index,
  652. child: ChatPageItem(
  653. key: Key(msg.time.toString()),
  654. msg: msg,
  655. hideKeyboard: readOnly,
  656. friendInfo: friendInfo,
  657. lastMsgTime: lastMsgTime));
  658. }
  659. }