chat_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:math';
  4. import 'package:cached_network_image/cached_network_image.dart';
  5. import 'package:flutter/cupertino.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/rendering.dart';
  8. import 'package:flutter/scheduler.dart';
  9. import 'package:flutter/services.dart';
  10. import 'package:flutter/widgets.dart';
  11. import 'package:shared_preferences/shared_preferences.dart';
  12. import 'package:sport/bean/image.dart' as photo;
  13. import 'package:sport/bean/message.dart';
  14. import 'package:sport/bean/post.dart';
  15. import 'package:sport/bean/post_user.dart';
  16. import 'package:sport/bean/user_info.dart';
  17. import 'package:sport/db/message_db.dart';
  18. import 'package:sport/pages/social/post_detail_page.dart';
  19. import 'package:sport/pages/social/share_webview.dart';
  20. import 'package:sport/pages/social/user_detail_page.dart';
  21. import 'package:sport/pages/my/feedback_page.dart';
  22. import 'package:sport/provider/lib/view_state_model.dart';
  23. import 'package:sport/provider/message_model.dart';
  24. import 'package:sport/provider/user_model.dart';
  25. import 'package:sport/router/navigator_util.dart';
  26. import 'package:sport/services/api/inject_api.dart';
  27. import 'package:sport/services/api/resp.dart';
  28. import 'package:sport/services/userid.dart';
  29. import 'package:sport/utils/DateFormat.dart';
  30. import 'package:sport/utils/toast.dart';
  31. import 'package:sport/widgets/appbar.dart';
  32. import 'package:sport/widgets/decoration.dart';
  33. import 'package:sport/widgets/loading.dart';
  34. import 'package:sport/widgets/menu_bar.dart';
  35. import 'package:sport/widgets/space.dart';
  36. import 'package:sport/widgets/dialog/popupmenu.dart' as menu;
  37. import 'package:provider/provider.dart';
  38. import 'gallery_photo_view.dart';
  39. final List<String> avatarList = [
  40. "https://wx3.sinaimg.cn/mw1024/a6bdcd78gy1gfna8yznv5j20m90m9tbz.jpg",
  41. "https://wx2.sinaimg.cn/mw1024/a6bdcd78gy1gfna8x3nbzj20m90m943a.jpg",
  42. "https://wx3.sinaimg.cn/mw1024/a6bdcd78gy1gfnaavrha3j20m90m9dkj.jpg",
  43. "https://wx4.sinaimg.cn/mw1024/a6bdcd78gy1gfna8znb76j20m90m978t.jpg",
  44. "https://wx4.sinaimg.cn/mw1024/a6bdcd78gy1gfna8wnwquj20lr0lradp.jpg",
  45. "https://wx4.sinaimg.cn/mw1024/a6bdcd78gy1gfna90vpgkj20lr0lrtby.jpg",
  46. "https://wx1.sinaimg.cn/mw1024/a6bdcd78gy1gfna8y4jb6j20m90m9tcj.jpg",
  47. "https://wx3.sinaimg.cn/mw1024/a6bdcd78gy1gfnaawemhtj20m90m9q7n.jpg",
  48. "https://wx4.sinaimg.cn/mw1024/a6bdcd78gy1gfnaaveqmwj20ls0lsn07.jpg",
  49. "https://wxt.sinaimg.cn/mw1024/a6bdcd78gy1gfnaat7uw5j20ls0lr0w0.jpg",
  50. "https://wx2.sinaimg.cn/mw1024/a6bdcd78gy1gfnaaubdhhj20m90m9786.jpg",
  51. ];
  52. class ChatPage extends StatefulWidget {
  53. final UserInfo user;
  54. final Post post; // 分享来的帖子
  55. final String hash; // 分享来的link
  56. final String image; // 分享来的图片 ...
  57. ChatPage(this.user, {this.post, this.hash, this.image});
  58. @override
  59. State<StatefulWidget> createState() => _ChatPageState();
  60. }
  61. // 服务端的数据 只有 拿了 和 没拿 ,客户端 就是 读了 和没读 ...
  62. class _ChatPageState extends State<ChatPage>
  63. with InjectLoginApi, InjectApi, UserId, WidgetsBindingObserver {
  64. GetMenuController _menuController = new GetMenuController();
  65. // ScrollController scrollMenuController = new ScrollController();
  66. StreamSubscription _streamSubscription;
  67. List<MessageItem> messageList = [];
  68. // String othersAvatarUrl = "";
  69. GlobalKey SCROLLVIEW = new GlobalKey();
  70. double emptyHeight;
  71. dispose() {
  72. super.dispose();
  73. _streamSubscription?.cancel();
  74. }
  75. initState() {
  76. super.initState();
  77. WidgetsBinding.instance.addObserver(this); // 监听一手自己...
  78. initMessageList();
  79. initListen(); // 开启监听... 先试试...
  80. addPost(); // 这里是分享过来的 ...
  81. // initScrollBottom();
  82. // print("${Provider.of<UserModel>(context, listen: false).user.toJson()}");
  83. // _menuController.scroll();
  84. }
  85. // 这个是初始化 聊天列表的...
  86. initMessageList() async {
  87. // await Future.delayed(Duration(milliseconds: 1000));
  88. // List<MessageItem> messageList = [];
  89. List<MessageItem> data = [];
  90. var list = await MessageDB().getMessageForUserId(widget.user.id);
  91. int day = -1;
  92. for (var item in list) {
  93. MessageItem _item = MessageItem.fromJson(item);
  94. if (_item.day != day) {
  95. day = _item.day;
  96. data.add(new MessageItem(
  97. messageType: "time",
  98. dateTime:
  99. DateFormat.format(DateTime.parse(_item.message.createdAt))));
  100. }
  101. data.add(_item);
  102. }
  103. // 读过就操作一手...
  104. await MessageDB().updateStatus(widget.user.id);
  105. // messageList.addAll(data);
  106. int cout = 0;
  107. data.forEach((element) {
  108. messageList.insert(0, element);
  109. });
  110. // 这里每次都会 拿最新的 而不是添加...
  111. setState(() {
  112. messageList = messageList;
  113. });
  114. // SchedulerBinding.instance.addPostFrameCallback((_) {
  115. ////here the sublist is already build
  116. // scrollMenuController.jumpTo(scrollMenuController.position.maxScrollExtent);
  117. // print("2222222222222222222222222");
  118. // print("${scrollMenuController.position.minScrollExtent} - ${scrollMenuController.position.maxScrollExtent}");
  119. // });
  120. _menuController.scroll();
  121. }
  122. Future addPost() async {
  123. // 这里是拿到了 帖子
  124. if (widget.post != null) {
  125. // 拿到后还得 存
  126. MessageInstance _instance = (await api.shareForwardSubject(
  127. int.parse(widget.post.id), widget.user.id))
  128. .data;
  129. print(
  130. "[_instance]${_instance.toJson()}---------------------------------------");
  131. await MessageDB().insert(new MessageItem(
  132. message: _instance, status: 0, curId: 0, userId: widget.user.id));
  133. addMessageToPage(_instance);
  134. }
  135. // 也可能是拿到了 link
  136. else if (widget.hash != null) {
  137. MessageInstance _instance =
  138. (await api.getshareForwardSport(widget.hash, widget.user.id)).data;
  139. await MessageDB().insert(new MessageItem(
  140. message: _instance, status: 0, curId: 0, userId: widget.user.id));
  141. addMessageToPage(_instance);
  142. } else if (widget.image != null) {
  143. MessageInstance _instance = (await api.postChatSend(
  144. widget.user.id, "image", '{"url":"${widget.image}"}'))
  145. .data;
  146. await MessageDB().insert(new MessageItem(
  147. message: _instance, status: 0, curId: 0, userId: widget.user.id));
  148. addMessageToPage(_instance);
  149. }
  150. }
  151. // 在本页中如果收到了就 ...
  152. initListen() {
  153. Stream<List<MessageInstance>> queryStream =
  154. Provider.of<MessageModel>(context, listen: false).queryStream;
  155. _streamSubscription =
  156. queryStream.listen((List<MessageInstance> list) async {
  157. for (MessageInstance item in list) {
  158. addMessageToPage(item);
  159. }
  160. await MessageDB().updateStatus(widget.user.id);
  161. });
  162. }
  163. //
  164. // initScrollBottom() {
  165. // _timer = Timer(Duration.zero, () {
  166. // _menuController.scrollToBottom(context, SCROLLVIEW, true);
  167. // });
  168. // }
  169. MessageItem instanceToItem(MessageInstance _instance) {
  170. int id = _instance.fromUser.id == int.parse(selfId)
  171. ? _instance.toUser.id
  172. : _instance.fromUser.id;
  173. // 里面收到 直接已读... 好像curId 可以不加 status 也可以不写 ...
  174. MessageItem item =
  175. new MessageItem(message: _instance, curId: 0, status: 0, userId: id);
  176. return item;
  177. }
  178. // 各种来自别的地方收到的消息 输出到 页面上
  179. addMessageToPage(MessageInstance _instance) {
  180. MessageItem item = instanceToItem(_instance);
  181. // messageList.insert(0, item);
  182. // messageList.add(item);
  183. messageList.insert(0, item); // 到头插...
  184. setState(() {
  185. messageList = messageList;
  186. });
  187. }
  188. // 封装的聊天msg
  189. // @who 判断是用户自己的还是 聊的那个人
  190. // @msg 信息 聊天的那个
  191. // @type 聊天的类型 可能是通过分享过来的那种 ? 游戏 或者是 别的 社区 或者是 链接 ?
  192. Widget _buildChatItem(MessageItem item) {
  193. // type 是时间 还是 消息...
  194. print("${item.toJson()}----------------------------------");
  195. if (item.messageType != null) {
  196. return Padding(
  197. padding: EdgeInsets.symmetric(vertical: 5.0),
  198. child: Center(
  199. child: Text(
  200. "${item.dateTime}",
  201. style: TextStyle(fontSize: 12.0),
  202. ),
  203. ),
  204. );
  205. }
  206. GlobalKey anchorKey = GlobalKey();
  207. MessageInstance data = item.message;
  208. int who = data.fromUser.id ==
  209. Provider.of<UserModel>(context, listen: false).user.id
  210. ? 1
  211. : 0;
  212. String userAvatar = data.fromUser.avatar;
  213. Widget chatItemOfType(String type) {
  214. List<photo.Image> images = [];
  215. images.add(photo.Image(id: "1", src: data.data.url));
  216. // 文本或者是图片...
  217. if (type == "text" || type == "image") {
  218. return Column(
  219. children: <Widget>[
  220. if (data.data.text != null)
  221. Text(
  222. "${data.data.text}",
  223. style: Theme.of(context)
  224. .textTheme
  225. .subtitle1
  226. .copyWith(fontSize: 16, color: Colors.black),
  227. ),
  228. if (data.data.url != null)
  229. InkWell(
  230. child: Container(
  231. constraints: BoxConstraints(
  232. maxHeight: MediaQuery.of(context).size.height,
  233. maxWidth: MediaQuery.of(context).size.width,
  234. ),
  235. child: CachedNetworkImage(
  236. imageUrl:
  237. data.data.url + "?x-oss-process=image/resize,p_10",
  238. height: 100,
  239. ),
  240. ),
  241. onTap: () {
  242. Navigator.push(
  243. context,
  244. FadeRoute(
  245. page: GalleryPhotoViewWrapper(
  246. galleryItems: images,
  247. backgroundDecoration: const BoxDecoration(
  248. color: Colors.black,
  249. ),
  250. initialIndex: 1,
  251. scrollDirection: Axis.horizontal,
  252. loadingBuilder: (_, __) => RequestLoadingWidget(),
  253. ),
  254. ),
  255. );
  256. },
  257. )
  258. ],
  259. crossAxisAlignment: CrossAxisAlignment.start,
  260. );
  261. }
  262. // 论坛消息 ...
  263. if (type == "forum-forward") {
  264. print("[forum-forward]${item.toJson()}----------------------");
  265. return InkWell(
  266. child: Row(
  267. crossAxisAlignment: CrossAxisAlignment.center,
  268. children: <Widget>[
  269. ClipRRect(
  270. child: Image.asset(
  271. "lib/assets/img/chat.png",
  272. width: 40,
  273. height: 40,
  274. fit: BoxFit.cover,
  275. ),
  276. borderRadius: BorderRadius.circular(10),
  277. ),
  278. Space(
  279. width: 10.0,
  280. ),
  281. Expanded(
  282. child: RichText(
  283. text: TextSpan(children: [
  284. TextSpan(
  285. text: "@" + data.data.user.name + ":",
  286. style: Theme.of(context)
  287. .textTheme
  288. .headline6
  289. .copyWith(color: Color(0xFFFFC400))),
  290. TextSpan(
  291. text: "分享了一篇帖子",
  292. style: Theme.of(context).textTheme.subtitle1),
  293. ]),
  294. ),
  295. )
  296. ],
  297. ),
  298. onTap: () async {
  299. Post post =
  300. (await api.getPostDetail("${data.data.subject.id}")).data;
  301. NavigatorUtil.goPage(
  302. context, (context) => PostDetailPage(post, false, null));
  303. },
  304. );
  305. }
  306. // 链接..
  307. if (type == "share") {
  308. return InkWell(
  309. child: Row(
  310. crossAxisAlignment: CrossAxisAlignment.start,
  311. children: <Widget>[
  312. CachedNetworkImage(
  313. imageUrl: avatarList[8],
  314. width: 60.0,
  315. height: 60.0,
  316. ),
  317. Space(
  318. width: 5.0,
  319. ),
  320. Expanded(
  321. child: RichText(
  322. text: TextSpan(children: [
  323. TextSpan(
  324. text: data.fromUser.name,
  325. style: Theme.of(context)
  326. .textTheme
  327. .headline6
  328. .copyWith(color: Color(0xFFFFC400))),
  329. TextSpan(
  330. text: "分享了他的运动记录,快来围观吧~",
  331. style: Theme.of(context).textTheme.subtitle1),
  332. ]),
  333. ),
  334. )
  335. ]),
  336. onTap: () async {
  337. await NavigatorUtil.goPage(
  338. context,
  339. (context) => WebViewSharePage(
  340. data.data.url,
  341. hash: data.data.share.hash,
  342. ));
  343. },
  344. );
  345. }
  346. }
  347. Widget chatContent(){
  348. return ConstrainedBox(
  349. constraints:
  350. BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
  351. child: Container(
  352. padding: who == 1
  353. ? EdgeInsets.fromLTRB(12, 6, 20, 8)
  354. : EdgeInsets.fromLTRB(20, 6, 12, 8),
  355. key: anchorKey,
  356. child: GestureDetector(
  357. behavior: HitTestBehavior.opaque,
  358. onLongPressStart: (e) {
  359. RenderBox renderBox =
  360. anchorKey.currentContext.findRenderObject();
  361. var offset = renderBox
  362. .localToGlobal(Offset(0.0, renderBox.size.height));
  363. final RelativeRect position = RelativeRect.fromLTRB(
  364. e.globalPosition.dx, //取点击位置坐弹出x坐标
  365. offset.dy, //取text高度做弹出y坐标(这样弹出就不会遮挡文本)
  366. e.globalPosition.dx,
  367. offset.dy);
  368. PopupMenuEntry menuItem(
  369. {String imgUrl, String text, Function callBack}) =>
  370. menu.PopupMenuItem(
  371. child: InkWell(
  372. onTap: () {
  373. callBack();
  374. Navigator.pop(context);
  375. },
  376. child: Row(
  377. mainAxisSize: MainAxisSize.min,
  378. children: <Widget>[
  379. Image.asset(
  380. "lib/assets/img/$imgUrl",
  381. width: 24,
  382. ),
  383. SizedBox(
  384. width: 4,
  385. ),
  386. Text(
  387. text,
  388. )
  389. ],
  390. ),
  391. ),
  392. );
  393. showMenu(
  394. context: context,
  395. position: position,
  396. items: <PopupMenuEntry>[
  397. PopupMenuItem(
  398. child: Container(
  399. child: Column(
  400. children: <Widget>[
  401. menuItem(
  402. imgUrl: "linkpop_icon_copy.png",
  403. text: "复制",
  404. callBack: () {
  405. Clipboard.setData(ClipboardData(
  406. text: '${data.data.text}'));
  407. ToastUtil.show("复制成功");
  408. }),
  409. who == 1
  410. ? menuItem(
  411. imgUrl: "linkpop_icon_del.png",
  412. text: "删除",
  413. callBack: () async {
  414. await MessageDB()
  415. .deleteMessageIdMessage(
  416. item.messageId);
  417. messageList.remove(item);
  418. setState(() {
  419. messageList = messageList;
  420. });
  421. },
  422. )
  423. : menuItem(
  424. imgUrl: "linkpop_icon_modify_1.png",
  425. text: "举报",
  426. callBack: () async {
  427. await api.postForumReport(
  428. userId: item.userId,
  429. content:
  430. "该用户涉嫌发送不良消息内容为:${item.message.data.text}");
  431. ToastUtil.show("举报已受理...");
  432. }),
  433. menuItem(
  434. imgUrl: "linkpop_icon_cancel.png", text: "取消")
  435. ],
  436. ),
  437. ))
  438. ],
  439. );
  440. },
  441. child: chatItemOfType(data.type))),
  442. );
  443. }
  444. Widget customPoint(){
  445. print("[data.type]:${data.type}--------------------");
  446. if(data.type != "image"){
  447. return CustomPaint(
  448. painter: who == 1 ? BubblePainterRight() : BubblePainter(),
  449. child: chatContent());
  450. }
  451. return chatContent();
  452. };
  453. Widget spaceItem = Space(
  454. width: 12,
  455. );
  456. String avatarUrl = data.fromUser?.id == int.parse(selfId)
  457. ? Provider.of<UserModel>(context, listen: false).user?.avatar == null
  458. ? avatarList[4]
  459. : Provider.of<UserModel>(context, listen: false).user?.avatar
  460. : widget.user?.avatar == null
  461. ? data.fromUser.avatar
  462. : widget.user?.avatar;
  463. Widget avatar = InkWell(
  464. onTap: () async {
  465. await NavigatorUtil.goPage(
  466. context,
  467. (context) => UserDetailPage(PostUser(
  468. id: "${data.fromUser?.id}",
  469. name: data.fromUser?.name,
  470. avatar: avatarUrl)));
  471. },
  472. child: CircleAvatar(
  473. backgroundImage: CachedNetworkImageProvider(avatarUrl),
  474. radius: 20,
  475. ),
  476. );
  477. List<Widget> chatContentUsr = [customPoint(), spaceItem, avatar];
  478. List<Widget> chatContentOther = [avatar, spaceItem, customPoint()];
  479. return Padding(
  480. padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  481. child: Row(
  482. mainAxisAlignment:
  483. who == 1 ? MainAxisAlignment.end : MainAxisAlignment.start,
  484. crossAxisAlignment: CrossAxisAlignment.start,
  485. children: who == 1 ? chatContentUsr : chatContentOther,
  486. ));
  487. }
  488. @override
  489. Widget build(BuildContext context) {
  490. return Scaffold(
  491. appBar: AppBar(
  492. title: InkWell(
  493. child: Text(
  494. "${widget.user.name}${widget.user.id}",
  495. style: titleStyle,
  496. ),
  497. onTap: (){
  498. NavigatorUtil.goPage(context, (context) => UserDetailPage(PostUser.fromJson({"id":"${widget.user.id}"})));
  499. },
  500. ),
  501. leading: buildBackButton(context),
  502. ),
  503. // resizeToAvoidBottomInset: false, // 透传MediaQuery 的高度?
  504. body: WillPopScope(
  505. onWillPop: () async {
  506. // 这是分享.... 只能一步一步pop 出去
  507. if (widget.post != null ||
  508. widget.hash != null ||
  509. widget.image != null) {
  510. Navigator.pop(context, true);
  511. return false;
  512. }
  513. return true;
  514. },
  515. child: MenuBar(
  516. CustomScrollView(
  517. key: SCROLLVIEW,
  518. shrinkWrap: true,
  519. reverse: true,
  520. controller: _menuController.scrollMenuController,
  521. // controller: scrollMenuController,
  522. slivers: <Widget>[
  523. if (messageList?.length == 0)
  524. SliverToBoxAdapter(
  525. child: Container(),
  526. ),
  527. if (messageList?.length != 0)
  528. SliverList(
  529. delegate: SliverChildBuilderDelegate((content, index) {
  530. // MessageInstance data = messageList[index];
  531. return _buildChatItem(messageList[index]);
  532. }, childCount: messageList.length),
  533. ),
  534. ],
  535. ),
  536. menuIdentity:
  537. new MenuIdentity(menuScene: "chat", userId: widget.user.id),
  538. inputField: "",
  539. scrollToBottom: _menuController.scroll, // 暂时可以废弃...
  540. sendCallBack: addMessageToPage,
  541. globalkey: SCROLLVIEW,
  542. )));
  543. }
  544. }