chat_page.dart 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:math';
  5. import 'package:cached_network_image/cached_network_image.dart';
  6. import 'package:flutter/cupertino.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:flutter/scheduler.dart';
  9. import 'package:flutter/services.dart';
  10. import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
  11. import 'package:get_it/get_it.dart';
  12. import 'package:images_picker/images_picker.dart';
  13. import 'package:provider/provider.dart';
  14. import 'package:sport/bean/game.dart';
  15. import 'package:sport/bean/image.dart' as photo;
  16. import 'package:sport/bean/message.dart';
  17. import 'package:sport/bean/post.dart';
  18. import 'package:sport/bean/post_user.dart';
  19. import 'package:sport/bean/user_info.dart';
  20. import 'package:sport/db/message_db.dart';
  21. import 'package:sport/pages/game/game_detail.dart';
  22. import 'package:sport/pages/my/feedback_page.dart';
  23. import 'package:sport/pages/social/post_detail_page.dart';
  24. import 'package:sport/pages/social/share_webview.dart';
  25. import 'package:sport/pages/social/user_detail_page.dart';
  26. import 'package:sport/provider/game_model.dart';
  27. import 'package:sport/provider/message_model.dart';
  28. import 'package:sport/provider/user_model.dart';
  29. import 'package:sport/router/navigator_util.dart';
  30. import 'package:sport/services/api/inject_api.dart';
  31. import 'package:sport/services/userid.dart';
  32. import 'package:sport/utils/DateFormat.dart';
  33. import 'package:sport/utils/toast.dart';
  34. import 'package:sport/widgets/appbar.dart';
  35. import 'package:sport/widgets/button_cancel.dart';
  36. import 'package:sport/widgets/button_primary.dart';
  37. import 'package:sport/widgets/decoration.dart';
  38. import 'package:sport/widgets/dialog/popupmenu.dart' as menu;
  39. import 'package:sport/widgets/dialog/request_dialog.dart';
  40. import 'package:sport/widgets/loading.dart';
  41. import 'package:sport/widgets/menu_bar.dart';
  42. import 'package:sport/widgets/space.dart';
  43. import 'gallery_photo_view.dart';
  44. class ChatPage extends StatefulWidget {
  45. final UserInfo user;
  46. final Post? post; // 分享来的帖子
  47. final String? hash; // 分享来的link
  48. final String? image; // 分享来的图片 ...
  49. final bool fetch; // 分享来的图片 ...
  50. ChatPage(this.user, {this.post, this.hash, this.image, this.fetch = false});
  51. @override
  52. State<StatefulWidget> createState() => _ChatPageState();
  53. }
  54. // 服务端的数据 只有 拿了 和 没拿 ,客户端 就是 读了 和没读 ...
  55. class _ChatPageState extends State<ChatPage> with InjectLoginApi, InjectApi, UserId, WidgetsBindingObserver {
  56. StreamSubscription? _streamSubscription;
  57. List<MessageItem> messageList = [];
  58. // String othersAvatarUrl = "";
  59. GlobalKey SCROLLVIEW = new GlobalKey();
  60. double? emptyHeight;
  61. bool _resizeToAvoidBottomInset = true;
  62. late MessageModel messageModel;
  63. late ScrollController _scrollController;
  64. late TextEditingController _controller;
  65. late FocusNode _focusNode = new FocusNode();
  66. final ValueNotifier<bool> _postable = ValueNotifier(false);
  67. double keyBoardHeight = 0.0; // 初始化下面menu的高度 后续会动态调整后 优化
  68. double keyBoardHeightCache = 270.0; // 初始化下面menu的高度 后续会动态调整后 优化
  69. int funIndex = 0; // 初始化下面menu的高度 后续会动态调整后 优化
  70. List<dynamic> emojiData = [];
  71. @override
  72. dispose() {
  73. super.dispose();
  74. _streamSubscription?.cancel();
  75. }
  76. @override
  77. initState() {
  78. super.initState();
  79. _scrollController = new ScrollController();
  80. _controller = TextEditingController()
  81. ..addListener(() {
  82. _postable.value = _controller.value.text.isNotEmpty == true;
  83. });
  84. messageModel = GetIt.I<MessageModel>();
  85. WidgetsBinding.instance?.addObserver(this); // 监听一手自己...
  86. initMessageList();
  87. initListen(); // 开启监听... 先试试...
  88. addPost(); // 这里是分享过来的 ...
  89. // initScrollBottom();
  90. // print("${Provider.of<UserModel>(context, listen: false).user.toJson()}");
  91. // _menuController.scroll();
  92. if (widget.fetch == true) {
  93. SchedulerBinding.instance?.addPostFrameCallback((timeStamp) async {
  94. messageModel.loopMessage(context);
  95. });
  96. }
  97. initEmoji();
  98. }
  99. void initEmoji() async {
  100. String list = await DefaultAssetBundle.of(context).loadString("lib/assets/json/emoji_list.json");
  101. setState(() {
  102. List<dynamic> data = json.decode(list);
  103. emojiData.addAll(data);
  104. });
  105. }
  106. // 这个是初始化 聊天列表的...
  107. initMessageList() async {
  108. List<MessageItem> data = [];
  109. int me = Provider.of<UserModel>(context, listen: false).user.id;
  110. var chat = await MessageDB().findHasUserId(me, widget.user.id!);
  111. if (chat.isEmpty == true) return data;
  112. int chatId = chat[0]['id'];
  113. var list = await MessageDB().getMessageForUserId(chatId);
  114. // DateTime now = DateTime.now();
  115. int millisecondsSinceEpoch = 0;
  116. MessageItem? _item;
  117. for (var item in list) {
  118. _item = MessageItem.fromJson(item);
  119. data.add(_item);
  120. if (millisecondsSinceEpoch == 0 || millisecondsSinceEpoch - (_item.dateTime?.millisecondsSinceEpoch ?? 0) > 2 * 60 * 1000) {
  121. millisecondsSinceEpoch = (_item.dateTime?.millisecondsSinceEpoch ?? 0);
  122. data.add(new MessageItem(type: "time", dateTime: _item.dateTime));
  123. }
  124. }
  125. if (data.isNotEmpty == true && data.last.type != "time") {
  126. if (_item != null) data.add(new MessageItem(type: "time", dateTime: _item.dateTime));
  127. }
  128. // 读过就操作一手...
  129. MessageDB().updateStatus(chatId);
  130. // 这里每次都会 拿最新的 而不是添加...
  131. setState(() {
  132. messageList = data;
  133. });
  134. _scrollController.jumpTo(0);
  135. if(messageList.isNotEmpty){
  136. if(messageList.first.self == true){
  137. _focusNode.requestFocus();
  138. }
  139. }
  140. }
  141. Future add(MessageInstance? message) async {
  142. if (message == null) return;
  143. GetIt.I<MessageModel>().add(context, [message]);
  144. }
  145. Future addPost() async {
  146. // 这里是拿到了 帖子
  147. MessageInstance? _instance;
  148. if (widget.post != null) {
  149. // 拿到后还得 存
  150. _instance = (await api.shareForwardSubject(int.parse(widget.post!.id!), widget.user.id!)).data;
  151. }
  152. // 也可能是拿到了 link
  153. else if (widget.hash != null) {
  154. _instance = (await api.getshareForwardSport(widget.hash!, widget.user.id!)).data;
  155. } else if (widget.image != null) {
  156. _instance = (await api.postChatSend("${widget.user.id!}", "image", '{"url":"${widget.image}"}')).data;
  157. }
  158. if (_instance != null) {
  159. print("[_instance]${_instance.toJson()}---------------------------------------");
  160. messageModel.add(context, [_instance]);
  161. }
  162. }
  163. // 在本页中如果收到了就 ...
  164. initListen() {
  165. Stream<int> queryStream = messageModel.queryStream;
  166. _streamSubscription = queryStream.listen((count) async {
  167. print("new message");
  168. initMessageList();
  169. });
  170. }
  171. // 封装的聊天msg
  172. // @who 判断是用户自己的还是 聊的那个人
  173. // @msg 信息 聊天的那个
  174. // @type 聊天的类型 可能是通过分享过来的那种 ? 游戏 或者是 别的 社区 或者是 链接 ?
  175. Widget _buildChatItem(MessageItem item) {
  176. // type 是时间 还是 消息...
  177. // print("${item.type}----------------------------------");
  178. if (item.type == "time") {
  179. return Padding(
  180. padding: EdgeInsets.symmetric(vertical: 5.0),
  181. child: Center(
  182. child: Text(
  183. "${DateFormat.format(item.dateTime, level: 1)}",
  184. style: TextStyle(fontSize: 12.0),
  185. ),
  186. ),
  187. );
  188. }
  189. GlobalKey anchorKey = GlobalKey();
  190. MessageData? data = item.data;
  191. if (data == null) return Container();
  192. int who = item.self == true ? 1 : 0;
  193. UserModel userModel = Provider.of<UserModel>(context, listen: false);
  194. String userAvatar = (who == 1 ? userModel.user.avatar : widget.user.avatar)!;
  195. String userName = (who == 1 ? userModel.user.name : widget.user.name)!;
  196. int userId = (who == 1 ? userModel.user.id : widget.user.id)!;
  197. double widthScale = item.type == "game-invite"
  198. ? 0.65
  199. : item.type == "image"
  200. ? 0.4
  201. : 0.6;
  202. double maxWidth = MediaQuery.of(context).size.width * widthScale;
  203. Widget chatItemOfType(String type) {
  204. if (type == "text" || type == "image") {
  205. return _TypeImage(
  206. data: data,
  207. );
  208. }
  209. // 论坛消息 ...
  210. if (type == "forum-forward") {
  211. return InkWell(
  212. child: _TypeForumForward(
  213. data: data,
  214. ),
  215. onTap: () async {
  216. Post? post = (await api.getPostDetail("${data.subject!.id}")).data;
  217. if (post != null) NavigatorUtil.goPage(context, (context) => PostDetailPage(post, false, null));
  218. },
  219. );
  220. }
  221. // 链接..
  222. if (type == "share") {
  223. return InkWell(
  224. child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
  225. CachedNetworkImage(
  226. imageUrl: userAvatar,
  227. width: 60.0,
  228. height: 60.0,
  229. ),
  230. Space(
  231. width: 5.0,
  232. ),
  233. Expanded(
  234. child: RichText(
  235. text: TextSpan(children: [
  236. TextSpan(text: userName, style: Theme.of(context).textTheme.headline6!.copyWith(color: Color(0xFFFFC400))),
  237. TextSpan(text: "分享了他的运动记录,快来围观吧~", style: Theme.of(context).textTheme.subtitle1!),
  238. ]),
  239. ),
  240. )
  241. ]),
  242. onTap: () async {
  243. await NavigatorUtil.goPage(
  244. context,
  245. (context) => WebViewSharePage(
  246. data.url!,
  247. hash: data.share?.hash,
  248. ));
  249. },
  250. );
  251. }
  252. if (type == 'game-invite')
  253. return GestureDetector(
  254. onTap: () {
  255. startGame(context, data.game, invite: data.inviteInfo!, user: widget.user);
  256. },
  257. child: ClipRRect(
  258. borderRadius: BorderRadius.circular(10.0),
  259. child: Container(
  260. width: double.infinity,
  261. color: Colors.white,
  262. child: _GameInviteWidget(
  263. maxWidth: maxWidth,
  264. message: item,
  265. user: widget.user,
  266. me: item.self == true,
  267. ),
  268. ),
  269. ),
  270. );
  271. return Container();
  272. }
  273. Widget chatContent() {
  274. return ConstrainedBox(
  275. constraints: BoxConstraints(maxWidth: maxWidth),
  276. child: Container(
  277. // padding: who == 1 ? EdgeInsets.fromLTRB(12, 6, 20, 8) : EdgeInsets.fromLTRB(20, 6, 12, 8),
  278. key: anchorKey,
  279. child: GestureDetector(
  280. behavior: HitTestBehavior.opaque,
  281. onLongPressStart: (e) {
  282. RenderObject? renderBox = anchorKey.currentContext?.findRenderObject();
  283. if (renderBox is RenderBox) {
  284. var offset = renderBox.localToGlobal(Offset(0.0, renderBox.size.height));
  285. final RelativeRect position = RelativeRect.fromLTRB(
  286. e.globalPosition.dx, //取点击位置坐弹出x坐标
  287. offset.dy, //取text高度做弹出y坐标(这样弹出就不会遮挡文本)
  288. e.globalPosition.dx,
  289. offset.dy);
  290. PopupMenuEntry menuItem({String? imgUrl, String? text, Function? callBack}) => menu.PopupMenuItem(
  291. child: InkWell(
  292. onTap: () {
  293. callBack?.call();
  294. Navigator.pop(context);
  295. },
  296. child: Row(
  297. mainAxisSize: MainAxisSize.min,
  298. children: <Widget>[
  299. Image.asset(
  300. "lib/assets/img/$imgUrl",
  301. width: 24,
  302. ),
  303. SizedBox(
  304. width: 4,
  305. ),
  306. Text(
  307. text ?? "",
  308. )
  309. ],
  310. ),
  311. ),
  312. );
  313. showMenu(
  314. context: context,
  315. position: position,
  316. items: <PopupMenuEntry>[
  317. PopupMenuItem(
  318. child: Container(
  319. child: Column(
  320. children: <Widget>[
  321. menuItem(
  322. imgUrl: "linkpop_icon_copy.png",
  323. text: "复制",
  324. callBack: () {
  325. Clipboard.setData(ClipboardData(text: '${data.text}'));
  326. ToastUtil.show("复制成功");
  327. }),
  328. who == 1
  329. ? menuItem(
  330. imgUrl: "linkpop_icon_del.png",
  331. text: "删除",
  332. callBack: () async {
  333. await MessageDB().deleteMessageIdMessage(item.id!);
  334. messageList.remove(item);
  335. setState(() {
  336. messageList = messageList;
  337. });
  338. },
  339. )
  340. : menuItem(
  341. imgUrl: "linkpop_icon_modify_1.png",
  342. text: "举报",
  343. callBack: () async {
  344. await api.postForumReport(userId: widget.user.id, content: "该用户涉嫌发送不良消息内容为:${data.text}");
  345. ToastUtil.show("举报已受理...");
  346. }),
  347. menuItem(imgUrl: "linkpop_icon_cancel.png", text: "取消")
  348. ],
  349. ),
  350. ))
  351. ],
  352. );
  353. }
  354. },
  355. child: chatItemOfType(item.type ?? ""))),
  356. );
  357. }
  358. Widget customPoint() {
  359. if (item.type != "image" && item.type != "game-invite") {
  360. return CustomPaint(
  361. painter: who == 1 ? BubblePainterRight() : BubblePainter(),
  362. child: Padding(
  363. padding: who == 1 ? EdgeInsets.fromLTRB(12, 6, 20, 8) : EdgeInsets.fromLTRB(20, 6, 12, 8),
  364. child: chatContent(),
  365. ));
  366. }
  367. return chatContent();
  368. }
  369. Widget spaceItem = Space(
  370. width: 12,
  371. );
  372. Widget avatar = InkWell(
  373. onTap: () async {
  374. await NavigatorUtil.goTransparentPage(context, (context, _, __) => UserDetailPage(PostUser(id: "$userId", name: userName, avatar: userAvatar)));
  375. },
  376. child: CircleAvatar(
  377. backgroundColor: Colors.black26,
  378. backgroundImage: CachedNetworkImageProvider(userAvatar),
  379. radius: 20,
  380. ),
  381. );
  382. List<Widget> chatContentUsr = [customPoint(), spaceItem, avatar];
  383. List<Widget> chatContentOther = [avatar, spaceItem, customPoint()];
  384. return Padding(
  385. padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  386. child: Row(
  387. mainAxisAlignment: who == 1 ? MainAxisAlignment.end : MainAxisAlignment.start,
  388. crossAxisAlignment: CrossAxisAlignment.start,
  389. children: who == 1 ? chatContentUsr : chatContentOther,
  390. ));
  391. }
  392. @override
  393. void didUpdateWidget(ChatPage oldWidget) {
  394. super.didUpdateWidget(oldWidget);
  395. }
  396. @override
  397. Widget build(BuildContext context) {
  398. return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) {
  399. var bottom = MediaQuery.of(context).viewInsets.bottom;
  400. if (bottom != 0) keyBoardHeightCache = bottom;
  401. return Scaffold(
  402. appBar: AppBar(
  403. title: InkWell(
  404. child: Text(
  405. "${widget.user.name}",
  406. style: titleStyle,
  407. ),
  408. onTap: () {
  409. NavigatorUtil.goTransparentPage(context, (context, _, __) => UserDetailPage(PostUser.fromJson({"id": "${widget.user.id}"})));
  410. },
  411. ),
  412. leading: buildBackButton(context),
  413. ),
  414. // resizeToAvoidBottomInset: false, // 透传MediaQuery 的高度?
  415. body: WillPopScope(
  416. onWillPop: () async {
  417. // 这是分享.... 只能一步一步pop 出去
  418. if (widget.post != null || widget.hash != null || widget.image != null) {
  419. Navigator.pop(context, true);
  420. return false;
  421. }
  422. return true;
  423. },
  424. child: GestureDetector(
  425. behavior: HitTestBehavior.translucent,
  426. onTap: () async {
  427. await SystemChannels.textInput.invokeMethod('TextInput.hide');
  428. // final currentFocus = FocusScope.of(context);
  429. // if (!currentFocus.hasPrimaryFocus && currentFocus.hasFocus) {
  430. // FocusManager.instance.primaryFocus?.unfocus();
  431. // }
  432. setState(() {
  433. keyBoardHeight = 0;
  434. });
  435. },
  436. child: Column(
  437. children: [
  438. Expanded(
  439. child: Column(
  440. children: [
  441. Flexible(
  442. child: CustomScrollView(
  443. key: SCROLLVIEW,
  444. shrinkWrap: true,
  445. reverse: true,
  446. controller: _scrollController,
  447. // controller: scrollMenuController,
  448. slivers: <Widget>[
  449. SliverToBoxAdapter(
  450. child: Container(
  451. height: 20.0,
  452. ),
  453. ),
  454. if (messageList.length == 0)
  455. SliverToBoxAdapter(
  456. child: Container(),
  457. ),
  458. if (messageList.length != 0)
  459. SliverList(
  460. delegate: SliverChildBuilderDelegate((content, index) {
  461. // MessageInstance data = messageList[index];
  462. return _buildChatItem(messageList[index]);
  463. }, childCount: messageList.length),
  464. ),
  465. SliverToBoxAdapter(
  466. child: Container(
  467. height: 20.0,
  468. ),
  469. ),
  470. ],
  471. ),
  472. ),
  473. ],
  474. ),
  475. ),
  476. Container(
  477. padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  478. decoration: shadowTop(),
  479. child: Row(
  480. children: <Widget>[
  481. GestureDetector(
  482. onTap: () async {
  483. setState(() {
  484. keyBoardHeight = max(bottom, keyBoardHeightCache);
  485. funIndex = 1;
  486. });
  487. await SystemChannels.textInput.invokeMethod('TextInput.hide');
  488. },
  489. child: Padding(
  490. // padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  491. child: Image.asset("lib/assets/img/bbs_icon_addmore.png"),
  492. padding: EdgeInsets.only(right: 12.0),
  493. ),
  494. ),
  495. GestureDetector(
  496. onTap: () async {
  497. setState(() {
  498. keyBoardHeight = max(bottom, keyBoardHeightCache);
  499. funIndex = 2;
  500. });
  501. await SystemChannels.textInput.invokeMethod('TextInput.hide');
  502. },
  503. child: Padding(
  504. // padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  505. child: Image.asset("lib/assets/img/bbs_icon_expression.png"),
  506. padding: EdgeInsets.only(right: 12.0),
  507. ),
  508. ),
  509. Expanded(
  510. child: CupertinoTextField(
  511. cursorColor: const Color(0xffFFC400),
  512. controller: _controller,
  513. focusNode: _focusNode,
  514. keyboardType: TextInputType.multiline,
  515. style: TextStyle(
  516. fontSize: 16.0,
  517. ),
  518. strutStyle: StrutStyle(forceStrutHeight: true, height: 1.4),
  519. minLines: 1,
  520. maxLines: 3,
  521. // maxLength: 200,
  522. decoration: BoxDecoration(shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(10)), color: Color(0xfff1f1f1)),
  523. ),
  524. ),
  525. Space(
  526. width: 5.0,
  527. ),
  528. ValueListenableBuilder(
  529. valueListenable: _postable,
  530. builder: (_, able, __) => PrimaryButton(
  531. width: 75,
  532. height: 35.0,
  533. content: "发送",
  534. callback: () async {
  535. if (_controller.text.isEmpty == true) {
  536. // ToastUtil.show("请输入正确的内容");
  537. return;
  538. }
  539. var content = _controller.text;
  540. _controller.clear();
  541. MessageInstance? message = (await api.postChatSend("${widget.user.id!}", "text", '{"text":"$content"}')).data;
  542. if (message != null) await add(message); // await 是等待的标志 我等待完 在做后面的init 的事?
  543. },
  544. shadow: able == true,
  545. // buttonColor: able == false ? Color(0xffd2d2d2) : null,
  546. buttonColor: able == false ? Color(0xffFFC400).withOpacity(0.3) : null,
  547. )),
  548. ],
  549. )),
  550. Container(
  551. height: isKeyboardVisible ? 0 : keyBoardHeight,color: Colors.white,
  552. child: IndexedStack(
  553. index: funIndex,
  554. children: [
  555. Container(),
  556. Container(
  557. padding: EdgeInsets.only(left: 24.0, right: 24.0),
  558. decoration: BoxDecoration(border: Border(top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
  559. child: GridView(
  560. shrinkWrap: true,
  561. padding: EdgeInsets.only(top: 24.0),
  562. physics: NeverScrollableScrollPhysics(),
  563. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  564. crossAxisCount: 4, //横轴三个子widget
  565. ),
  566. children: <Widget>[
  567. InkWell(
  568. child: Column(
  569. crossAxisAlignment: CrossAxisAlignment.center,
  570. children: <Widget>[
  571. Container(
  572. padding: EdgeInsets.fromLTRB(14.0, 16.0, 14.0, 14.0),
  573. child: Image.asset("lib/assets/img/bbs_icon_picture.png"),
  574. decoration: BoxDecoration(
  575. borderRadius: BorderRadius.circular(10.0),
  576. color: Color(0xfff1f1f1),
  577. ),
  578. ),
  579. SizedBox(
  580. height: 5.0,
  581. ),
  582. Text("图片")
  583. ],
  584. ),
  585. onTap: () async {
  586. try {
  587. var resultList = await ImagesPicker.pick(quality: 0.8, maxSize: 1024, count: 9);
  588. if (resultList != null) {
  589. List<File> files = [];
  590. for (var i = 0; i < resultList.length; i++) {
  591. Media asset = resultList[i];
  592. File file = File(asset.path);
  593. files.add(file);
  594. }
  595. _uploadImage(files);
  596. }
  597. } on Exception catch (e) {}
  598. },
  599. ),
  600. InkWell(
  601. child: Column(
  602. crossAxisAlignment: CrossAxisAlignment.center,
  603. children: <Widget>[
  604. Container(
  605. padding: EdgeInsets.fromLTRB(14.0, 16.0, 14.0, 14.0),
  606. child: Image.asset("lib/assets/img/bbs_icon_photo.png"),
  607. decoration: BoxDecoration(
  608. borderRadius: BorderRadius.circular(10.0),
  609. color: Color(0xfff1f1f1),
  610. ),
  611. ),
  612. SizedBox(
  613. height: 5.0,
  614. ),
  615. Text("拍照")
  616. ],
  617. ),
  618. onTap: () async {
  619. try {
  620. // 拍完直接发...
  621. final pickedFile = await ImagesPicker.openCamera(
  622. pickType: PickType.image,
  623. );
  624. if (pickedFile == null || pickedFile.isEmpty) return;
  625. _uploadImage([File(pickedFile[0].path)]);
  626. } on Exception catch (e) {}
  627. },
  628. ),
  629. ])),
  630. Container(
  631. padding: EdgeInsets.only(left: 5, top: 5, right: 5, bottom: 5),
  632. decoration: BoxDecoration(border: Border(top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
  633. child: GridView.custom(
  634. padding: EdgeInsets.all(3),
  635. shrinkWrap: true,
  636. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  637. crossAxisCount: 6,
  638. mainAxisSpacing: 0.5,
  639. crossAxisSpacing: 6.0,
  640. ),
  641. childrenDelegate: SliverChildBuilderDelegate(
  642. (context, index) {
  643. return GestureDetector(
  644. onTap: () {
  645. String intPutString = _controller.text + String.fromCharCode(emojiData[index]["unicode"]);
  646. var content = intPutString;
  647. _controller.value = TextEditingValue(
  648. // 设置内容
  649. text: content,
  650. // 保持光标在最后
  651. selection: TextSelection.fromPosition(TextPosition(affinity: TextAffinity.downstream, offset: content.length)));
  652. // 主要是 onchange 没有办法 加上 表情 ...
  653. setState(() {});
  654. },
  655. child: Center(
  656. child: Text(
  657. String.fromCharCode(emojiData[index]["unicode"]),
  658. style: TextStyle(fontSize: 33),
  659. ),
  660. ),
  661. );
  662. },
  663. childCount: emojiData.length,
  664. ),
  665. ),
  666. )
  667. ],
  668. ),
  669. ),
  670. ],
  671. ),
  672. )),
  673. resizeToAvoidBottomInset: isKeyboardVisible,
  674. );
  675. });
  676. }
  677. _uploadImage(List<File> files) async {
  678. await request(context, () async {
  679. for (var file in files) {
  680. String path = file.path;
  681. var data = (await api.postChatUpload(file)).data;
  682. Image image = Image.file(File.fromUri(Uri.parse(path)));
  683. image.image.resolve(ImageConfiguration()).addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
  684. // print("$data----------------------${image.image.width} - ${image.image.height}------");
  685. api.postChatSend("${widget.user.id!}", "image", '{ "url":"${data['url']}", "w":${image.image.width}, "h":${image.image.height} }').then((value) => add(value.data));
  686. file.delete();
  687. }));
  688. }
  689. });
  690. }
  691. }
  692. class _GameInviteWidget extends StatefulWidget {
  693. final double maxWidth;
  694. final MessageItem message;
  695. final UserInfo user;
  696. final bool me;
  697. const _GameInviteWidget({Key? key, required this.maxWidth, required this.message, required this.user, required this.me}) : super(key: key);
  698. @override
  699. State<StatefulWidget> createState() {
  700. return _GameInviteWidgetState();
  701. }
  702. }
  703. class _GameInviteWidgetState extends State<_GameInviteWidget> {
  704. @override
  705. Widget build(BuildContext context) {
  706. double maxWidth = widget.maxWidth;
  707. MessageItem item = widget.message;
  708. MessageData? data = item.data;
  709. GameInfoData? game = data?.game;
  710. return Column(
  711. children: [
  712. Container(
  713. height: maxWidth * 0.35,
  714. decoration: BoxDecoration(
  715. image: DecorationImage(
  716. image: CachedNetworkImageProvider(game?.coverHorizontal ?? ""),
  717. fit: BoxFit.cover,
  718. ),
  719. ),
  720. child: Container(
  721. width: double.infinity,
  722. // margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  723. // alignment: Alignment.bottomCenter,
  724. decoration: const BoxDecoration(
  725. gradient: const LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [
  726. const Color(0x00000000),
  727. // Color(0x00000000),
  728. const Color(0xAA000000)
  729. ]),
  730. ),
  731. child: Center(
  732. child: Column(
  733. mainAxisAlignment: MainAxisAlignment.center,
  734. children: [
  735. Image.asset(
  736. widget.message.self == true
  737. ? "lib/assets/img/mine_image_pk.png"
  738. : data?.status == "wait"
  739. ? "lib/assets/img/mine_image_pk.png"
  740. : data?.status == "yes"
  741. ? "lib/assets/img/join_icon_pass.png"
  742. : "lib/assets/img/join_icon_no.png",
  743. height: 40.0,
  744. ),
  745. SizedBox(
  746. height: 8,
  747. ),
  748. Text(
  749. widget.message.self == true ? "你发起在${game?.name}的邀请" : "你的好友在${game?.name}向你发起了邀请",
  750. style: Theme.of(context).textTheme.subtitle2!.copyWith(color: Colors.white),
  751. ),
  752. ],
  753. ),
  754. )),
  755. ),
  756. if (widget.me != true)
  757. Padding(
  758. padding: const EdgeInsets.all(16.0),
  759. child: data?.status == "wait"
  760. ? Row(
  761. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  762. children: <Widget>[
  763. Expanded(
  764. child: CancelButton(
  765. height: 35,
  766. callback: () async {
  767. data?.status = "no";
  768. await MessageDB().updateData(item);
  769. setState(() {});
  770. },
  771. content: "拒绝"),
  772. ),
  773. SizedBox(
  774. width: 16,
  775. ),
  776. Expanded(
  777. child: PrimaryButton(
  778. height: 35,
  779. callback: () async {
  780. data?.status = "yes";
  781. await MessageDB().updateData(item);
  782. setState(() {});
  783. GameModel gameModel = GetIt.I<GameModel>();
  784. GameInfoData? e = await gameModel.getGame(game?.id ?? 0);
  785. startGame(context, e, invite: data?.inviteInfo, user: widget.user);
  786. },
  787. content: "同意"))
  788. ],
  789. )
  790. : data?.status == "yes"
  791. ? Center(
  792. child: Text(
  793. "已同意",
  794. style: TextStyle(fontSize: 14.0, color: Color(0xff00DC42)),
  795. ))
  796. : Center(
  797. child: Text(
  798. "已拒绝",
  799. style: TextStyle(fontSize: 14.0, color: Color(0xffFF5B1D)),
  800. )),
  801. ),
  802. ],
  803. );
  804. }
  805. }
  806. class _TypeImage extends StatelessWidget {
  807. final MessageData data;
  808. const _TypeImage({Key? key, required this.data}) : super(key: key);
  809. @override
  810. Widget build(BuildContext context) {
  811. String? url = data.url;
  812. int w = data.w != 0 ? data.w! : (MediaQuery.of(context).size.width ~/ 4);
  813. int h = data.h != 0 ? data.h! : (MediaQuery.of(context).size.width ~/ 4);
  814. double ratio = w / h;
  815. // print("1111111 ${data.toJson()} w: $w h: $h $ratio -- ${data.w }:${data.h}");
  816. return Column(
  817. children: <Widget>[
  818. if (data.text != null)
  819. Text(
  820. "${data.text}",
  821. style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 16, color: Colors.black),
  822. ),
  823. if (url != null)
  824. InkWell(
  825. child: AspectRatio(
  826. aspectRatio: ratio,
  827. child: CachedNetworkImage(
  828. imageUrl: "${data.url}?x-oss-process=image/resize,p_50",
  829. fit: BoxFit.cover,
  830. ),
  831. ),
  832. onTap: () {
  833. Navigator.push(
  834. context,
  835. FadeRoute(
  836. page: GalleryPhotoViewWrapper(
  837. galleryItems: [photo.Image(id: "1", src: data.url)],
  838. backgroundDecoration: const BoxDecoration(
  839. color: Colors.black,
  840. ),
  841. scrollDirection: Axis.horizontal,
  842. loadingBuilder: (_, __) => RequestLoadingWidget(),
  843. ),
  844. ),
  845. );
  846. },
  847. )
  848. ],
  849. crossAxisAlignment: CrossAxisAlignment.start,
  850. );
  851. }
  852. }
  853. class _TypeForumForward extends StatelessWidget {
  854. final MessageData data;
  855. const _TypeForumForward({Key? key, required this.data}) : super(key: key);
  856. @override
  857. Widget build(BuildContext context) {
  858. return Row(
  859. crossAxisAlignment: CrossAxisAlignment.start,
  860. children: <Widget>[
  861. Image.asset(
  862. "lib/assets/img/chat.png",
  863. width: 14.0,
  864. height: 14.0,
  865. fit: BoxFit.cover,
  866. ),
  867. Space(
  868. width: 5.0,
  869. ),
  870. Expanded(
  871. child: RichText(
  872. maxLines: 4,
  873. overflow: TextOverflow.ellipsis,
  874. text: TextSpan(children: [
  875. // TextSpan(text: "@${data.user?.name}:", style: Theme.of(context).textTheme.headline6!.copyWith(color: Color(0xFFFFC400))),
  876. TextSpan(text: "分享了一篇帖子", style: Theme.of(context).textTheme.subtitle1),
  877. if(data.subject?.content?.isNotEmpty == true)
  878. TextSpan(text: "\n\n${data.subject?.content}", style: Theme.of(context).textTheme.subtitle1, ),
  879. ]),
  880. ),
  881. )
  882. ],
  883. );
  884. }
  885. }