From a0bb03f10178a67820ecb47e4f35d9eb5a25316e Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Sun, 6 Feb 2022 18:05:59 +0330 Subject: [PATCH] D1APP-53 messages ui --- lib/main.dart | 4 - lib/models/message_data/radar_attachment.dart | 12 ++ lib/pages/home/direct/direct.dart | 54 ------ lib/pages/home/direct/direct_state.dart | 28 +++- lib/pages/home/direct/widgets/direct.dart | 88 ++++++++++ lib/pages/home/direct/widgets/message.dart | 158 ++++++++++++++++++ .../direct_list/widgets/chat_room_item.dart | 15 +- lib/pages/splash/splash.dart | 2 +- lib/providers/server_data_provider.dart | 12 +- lib/routes/route_generator.dart | 4 +- lib/services/network/request_helper.dart | 8 +- lib/utils/date_time.dart | 1 + lib/widgets/didvan/app_bar.dart | 23 ++- lib/widgets/didvan/scaffold.dart | 133 +++++++++++---- 14 files changed, 436 insertions(+), 106 deletions(-) delete mode 100644 lib/pages/home/direct/direct.dart create mode 100644 lib/pages/home/direct/widgets/direct.dart create mode 100644 lib/pages/home/direct/widgets/message.dart diff --git a/lib/main.dart b/lib/main.dart index 9c23e4d..01dbfff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/providers/server_data_provider.dart'; import 'package:didvan/providers/theme_provider.dart'; import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/routes/route_generator.dart'; @@ -24,9 +23,6 @@ class Didvan extends StatelessWidget { ChangeNotifierProvider( create: (context) => UserProvider(), ), - ChangeNotifierProvider( - create: (context) => ServerDataProvider(), - ), ChangeNotifierProvider( create: (context) => ThemeProvider(), ), diff --git a/lib/models/message_data/radar_attachment.dart b/lib/models/message_data/radar_attachment.dart index 13dbe76..6e55f2d 100644 --- a/lib/models/message_data/radar_attachment.dart +++ b/lib/models/message_data/radar_attachment.dart @@ -1,3 +1,5 @@ +import 'package:didvan/models/category.dart'; + class RadarAttachment { final int id; final String title; @@ -5,6 +7,8 @@ class RadarAttachment { final int timeToRead; final String image; final bool forManagers; + final String createdAt; + final List categories; const RadarAttachment({ required this.id, @@ -13,6 +17,8 @@ class RadarAttachment { required this.timeToRead, required this.image, required this.forManagers, + required this.categories, + required this.createdAt, }); factory RadarAttachment.fromJson(Map json) => @@ -22,7 +28,13 @@ class RadarAttachment { description: json['description'], timeToRead: json['timeToRead'], image: json['image'], + createdAt: json['createdAt'], forManagers: json['forManagers'], + categories: List.from( + json['categories'].map( + (cat) => CategoryData.fromJson(cat), + ), + ), ); Map toJson() => { diff --git a/lib/pages/home/direct/direct.dart b/lib/pages/home/direct/direct.dart deleted file mode 100644 index 095f8d7..0000000 --- a/lib/pages/home/direct/direct.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:didvan/pages/home/direct/direct_state.dart'; -import 'package:didvan/pages/home/direct/widgets/message_box.dart'; -import 'package:didvan/models/view/app_bar_data.dart'; -import 'package:didvan/widgets/didvan/scaffold.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class Direct extends StatefulWidget { - final int id; - const Direct({Key? key, required this.id}) : super(key: key); - - @override - State createState() => _DirectState(); -} - -class _DirectState extends State { - @override - void initState() { - Future.delayed(Duration.zero, () { - context.read().getMessages(widget.id); - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Material( - child: Stack( - children: [ - Positioned( - top: 0, - bottom: 56, - left: 0, - right: 0, - child: DidvanScaffold( - appBarData: AppBarData( - hasBack: true, - subtitle: 'ارتباط با سردبیر', - title: 'رادار اقتصادی', - ), - slivers: const [], - ), - ), - Positioned( - bottom: MediaQuery.of(context).viewInsets.bottom, - right: 0, - left: 0, - child: const MessageBox(), - ), - ], - ), - ); - } -} diff --git a/lib/pages/home/direct/direct_state.dart b/lib/pages/home/direct/direct_state.dart index 18b9b86..678b4e1 100644 --- a/lib/pages/home/direct/direct_state.dart +++ b/lib/pages/home/direct/direct_state.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/message_data/message_data.dart'; import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; @@ -9,15 +11,35 @@ import 'package:record/record.dart'; class DirectState extends CoreProvier { final _recorder = Record(); + final List messages = []; + late final int typeId; + final Map> dailyMessages = {}; File? recordedFile; bool isRecording = false; - Future getMessages(int id) async { - final RequestService service = RequestService(RequestHelper.direct(id)); + Future getMessages() async { + appState = AppState.busy; + final RequestService service = RequestService(RequestHelper.direct(typeId)); await service.httpGet(); - if (service.isSuccess) {} + if (service.isSuccess) { + final messageDatas = service.result['messages']; + for (var i = 0; i < messageDatas.length; i++) { + messages.add(MessageData.fromJson(messageDatas[i])); + final createdAt = messages.last.createdAt.split('T').first; + if (!dailyMessages.containsKey(createdAt)) { + dailyMessages.addAll({ + createdAt: [messages.last.id] + }); + } else { + dailyMessages[createdAt]!.add(messages.last.id); + } + } + appState = AppState.idle; + return; + } + appState = AppState.failed; } void deleteRecordedFile() { diff --git a/lib/pages/home/direct/widgets/direct.dart b/lib/pages/home/direct/widgets/direct.dart new file mode 100644 index 0000000..2478f71 --- /dev/null +++ b/lib/pages/home/direct/widgets/direct.dart @@ -0,0 +1,88 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/pages/home/direct/direct_state.dart'; +import 'package:didvan/pages/home/direct/widgets/message.dart'; +import 'package:didvan/pages/home/direct/widgets/message_box.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/providers/server_data_provider.dart'; +import 'package:didvan/widgets/didvan/scaffold.dart'; +import 'package:didvan/widgets/state_handlers/sliver_state_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:provider/provider.dart'; + +class Direct extends StatefulWidget { + final Map pageData; + const Direct({Key? key, required this.pageData}) : super(key: key); + + @override + State createState() => _DirectState(); +} + +class _DirectState extends State { + @override + void initState() { + final state = context.read(); + final typeId = ServerDataProvider.labelToTypeId(widget.pageData['type']); + state.typeId = typeId; + Future.delayed(Duration.zero, () { + state.getMessages(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final d = MediaQuery.of(context); + return Material( + child: Stack( + children: [ + Positioned( + top: 0, + bottom: 56, + left: 0, + right: 0, + child: DidvanScaffold( + reverse: true, + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: AppBarData( + hasBack: true, + subtitle: 'ارتباط با سردبیر', + title: widget.pageData['type'].substring(7), + ), + slivers: [ + if (state.appState != AppState.busy) + SliverStateHandler( + itemPadding: const EdgeInsets.only(bottom: 12), + state: state, + builder: (context, state, index) => Message( + message: state.messages[index], + ), + childCount: state.messages.length, + onRetry: state.getMessages, + ), + ], + children: [ + if (state.appState == AppState.busy) + SizedBox( + height: d.size.height - kToolbarHeight - d.padding.top, + child: Center( + child: SpinKitSpinningLines( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ), + Positioned( + bottom: d.viewInsets.bottom, + right: 0, + left: 0, + child: const MessageBox(), + ), + ], + ), + ); + } +} diff --git a/lib/pages/home/direct/widgets/message.dart b/lib/pages/home/direct/widgets/message.dart new file mode 100644 index 0000000..f37d93c --- /dev/null +++ b/lib/pages/home/direct/widgets/message.dart @@ -0,0 +1,158 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/message_data/message_data.dart'; +import 'package:didvan/pages/home/direct/direct_state.dart'; +import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/widgets/didvan/text.dart'; +import 'package:didvan/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; + +class Message extends StatelessWidget { + final MessageData message; + const Message({Key? key, required this.message}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + right: message.writedByAdmin ? 20 : 0, + left: !message.writedByAdmin ? 20 : 0, + ), + child: Column( + children: [ + if (message.id == + context + .read() + .dailyMessages[message.createdAt.split('T').first]! + .last) + Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.splash, + borderRadius: DesignConfig.lowBorderRadius, + ), + child: DidvanText( + DateTime.parse(message.createdAt).toPersianDateStr(), + style: Theme.of(context).textTheme.overline, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.white + : Theme.of(context).colorScheme.black, + ), + ), + _MessageContainer( + isAttachment: false, + writedByAdmin: message.writedByAdmin, + child: + message.text != null ? DidvanText(message.text!) : Container(), + ), + if (message.radar != null) + DidvanText( + 'لینک به مطلب زیر:', + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.primary, + ), + if (message.radar != null) const SizedBox(height: 4), + if (message.radar != null) _ReplyRadarOverview(message: message), + if (message.radar != null) const SizedBox(height: 4), + ], + ), + ); + } +} + +class _ReplyRadarOverview extends StatelessWidget { + final MessageData message; + const _ReplyRadarOverview({Key? key, required this.message}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _MessageContainer( + writedByAdmin: message.writedByAdmin, + isAttachment: true, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SkeletonImage( + imageUrl: message.radar!.image, + height: 80, + width: 80, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + message.radar!.title, + style: Theme.of(context).textTheme.bodyText1, + maxLines: 2, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.focusedBorder, + ), + Row( + children: [ + DidvanText( + 'رادار ' + message.radar!.categories.first.label, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.focusedBorder, + ), + const Spacer(), + DidvanText( + DateTimeUtils.momentGenerator( + message.radar!.createdAt) + + ' | خواندن در ' + + message.radar!.timeToRead.toString() + + ' دقیقه', + color: Theme.of(context).colorScheme.focusedBorder, + style: Theme.of(context).textTheme.overline, + ), + // DidvanText('text'), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _MessageContainer extends StatelessWidget { + final bool writedByAdmin; + final bool isAttachment; + final Widget child; + const _MessageContainer({ + Key? key, + required this.writedByAdmin, + required this.isAttachment, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: DesignConfig.highBorderRadius.copyWith( + bottomLeft: writedByAdmin && !isAttachment ? Radius.zero : null, + bottomRight: !writedByAdmin && !isAttachment ? Radius.zero : null, + ), + color: writedByAdmin ? null : Theme.of(context).colorScheme.focused, + border: Border.all( + color: Theme.of(context).colorScheme.border, + width: 0.5, + ), + ), + child: child, + ); + } +} diff --git a/lib/pages/home/settings/direct_list/widgets/chat_room_item.dart b/lib/pages/home/settings/direct_list/widgets/chat_room_item.dart index 16f3215..a451a86 100644 --- a/lib/pages/home/settings/direct_list/widgets/chat_room_item.dart +++ b/lib/pages/home/settings/direct_list/widgets/chat_room_item.dart @@ -2,6 +2,7 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/chat_room/chat_room.dart'; import 'package:didvan/routes/routes.dart'; +import 'package:didvan/utils/date_time.dart'; import 'package:didvan/widgets/didvan/badge.dart'; import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/text.dart'; @@ -16,7 +17,7 @@ class ChatRoomItem extends StatelessWidget { return GestureDetector( onTap: () => Navigator.of(context).pushNamed( Routes.direct, - arguments: chatRoom.id, + arguments: {'type': chatRoom.type}, ), child: Container( color: Colors.transparent, @@ -35,15 +36,18 @@ class ChatRoomItem extends StatelessWidget { style: Theme.of(context).textTheme.bodyText1, ), ), - DidvanBadge(text: chatRoom.unread.toString()), + if (chatRoom.unread != 0) + DidvanBadge(text: chatRoom.unread.toString()), ], ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(width: 40), - const Icon( - DidvanIcons.check_double_light, + Icon( + chatRoom.lastMessage.readed + ? DidvanIcons.check_double_light + : DidvanIcons.check_light, size: 16, ), const SizedBox(width: 4), @@ -54,9 +58,10 @@ class ChatRoomItem extends StatelessWidget { DidvanText( chatRoom.lastMessage.text ?? '', maxLines: 1, + overflow: TextOverflow.ellipsis, ), DidvanText( - chatRoom.updatedAt, + DateTimeUtils.momentGenerator(chatRoom.updatedAt), style: Theme.of(context).textTheme.caption, color: Theme.of(context).colorScheme.caption, ) diff --git a/lib/pages/splash/splash.dart b/lib/pages/splash/splash.dart index 4486bec..bba5cc0 100644 --- a/lib/pages/splash/splash.dart +++ b/lib/pages/splash/splash.dart @@ -108,7 +108,7 @@ class _SplashState extends State { log(token); RequestService.token = token; await userProvider.getUserInfo(); - await context.read().getData(); + ServerDataProvider.getData(); } Navigator.of(context).pushReplacementNamed( token == null ? Routes.authenticaion : Routes.home, diff --git a/lib/providers/server_data_provider.dart b/lib/providers/server_data_provider.dart index 53eb006..561f479 100644 --- a/lib/providers/server_data_provider.dart +++ b/lib/providers/server_data_provider.dart @@ -1,15 +1,17 @@ -import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; -class ServerDataProvider extends CoreProvier { - final List directTypes = []; +class ServerDataProvider { + static final List directTypes = []; - Future getData() async { + static Future getData() async { await _getDirectTypes(); } - Future _getDirectTypes() async { + static int labelToTypeId(String label) => + directTypes.firstWhere((element) => element.value == label).key; + + static Future _getDirectTypes() async { final service = RequestService(RequestHelper.directTypes); await service.httpGet(); if (service.isSuccess) { diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 15452b7..2653724 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -2,7 +2,7 @@ import 'package:didvan/pages/authentication/authentication.dart'; import 'package:didvan/pages/authentication/authentication_state.dart'; import 'package:didvan/pages/home/comments/comments.dart'; import 'package:didvan/pages/home/comments/comments_state.dart'; -import 'package:didvan/pages/home/direct/direct.dart'; +import 'package:didvan/pages/home/direct/widgets/direct.dart'; import 'package:didvan/pages/home/direct/direct_state.dart'; import 'package:didvan/pages/home/home.dart'; import 'package:didvan/pages/home/home_state.dart'; @@ -109,7 +109,7 @@ class RouteGenerator { return _createRoute( ChangeNotifierProvider( create: (context) => DirectState(), - child: Direct(id: settings.arguments as int), + child: Direct(pageData: settings.arguments as Map), ), ); case Routes.comments: diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index 77bedcc..cca9c02 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -19,11 +19,17 @@ class RequestHelper { static const String directTypes = baseUrl + '/direct/types'; static String direct(int id) => _baseUserUrl + '/direct/$id'; - static String tag(List ids) => + static String tag({ + required List ids, + required String type, + required int itemId, + }) => baseUrl + '/tag' + _urlConcatGenerator([ const MapEntry('limit', '3'), + MapEntry('type', type), + MapEntry('id', itemId.toString()), MapEntry('tags', _urlListConcatGenerator(ids)) ]); diff --git a/lib/utils/date_time.dart b/lib/utils/date_time.dart index e2332a4..4d70166 100644 --- a/lib/utils/date_time.dart +++ b/lib/utils/date_time.dart @@ -67,6 +67,7 @@ class DateTimeUtils { } interval = seconds / 86400; if (interval > 1) { + if (interval.floor() == 1) return 'دیروز'; return interval.floor().toString() + " روز پیش"; } interval = seconds / 3600; diff --git a/lib/widgets/didvan/app_bar.dart b/lib/widgets/didvan/app_bar.dart index 764e588..b61d6dd 100644 --- a/lib/widgets/didvan/app_bar.dart +++ b/lib/widgets/didvan/app_bar.dart @@ -6,12 +6,31 @@ import 'package:flutter/material.dart'; class DidvanAppBar extends StatelessWidget { final AppBarData appBarData; - const DidvanAppBar({Key? key, required this.appBarData}) : super(key: key); + final bool hasBorder; + final Color? backgroundColor; + const DidvanAppBar({ + Key? key, + required this.appBarData, + this.hasBorder = false, + this.backgroundColor = Colors.transparent, + }) : super(key: key); @override Widget build(BuildContext context) { - return Padding( + return Container( + height: kToolbarHeight + MediaQuery.of(context).padding.top, + width: MediaQuery.of(context).size.width, padding: const EdgeInsets.only(right: 4, left: 20), + decoration: BoxDecoration( + color: backgroundColor, + border: hasBorder + ? Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.cardBorder, + ), + ) + : null, + ), child: Row( children: [ IconButton( diff --git a/lib/widgets/didvan/scaffold.dart b/lib/widgets/didvan/scaffold.dart index 6e00a7a..1790794 100644 --- a/lib/widgets/didvan/scaffold.dart +++ b/lib/widgets/didvan/scaffold.dart @@ -2,12 +2,14 @@ import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/widgets/didvan/app_bar.dart'; import 'package:flutter/material.dart'; -class DidvanScaffold extends StatelessWidget { +class DidvanScaffold extends StatefulWidget { final List? slivers; final List? children; final AppBarData appBarData; final EdgeInsets padding; final Color? backgroundColor; + final bool reverse; + const DidvanScaffold({ Key? key, this.slivers, @@ -15,47 +17,120 @@ class DidvanScaffold extends StatelessWidget { this.children, this.padding = const EdgeInsets.symmetric(horizontal: 16), this.backgroundColor, + this.reverse = false, }) : super(key: key); + @override + State createState() => _DidvanScaffoldState(); +} + +class _DidvanScaffoldState extends State { + final _scrollController = ScrollController(); + @override Widget build(BuildContext context) { final double statusBarHeight = MediaQuery.of(context).padding.top; return Scaffold( - backgroundColor: backgroundColor, + backgroundColor: widget.backgroundColor, body: Padding( padding: EdgeInsets.only(top: statusBarHeight), - child: CustomScrollView( - slivers: [ - SliverAppBar( - toolbarHeight: kToolbarHeight, - backgroundColor: - backgroundColor ?? Theme.of(context).colorScheme.background, - automaticallyImplyLeading: false, - pinned: true, - flexibleSpace: DidvanAppBar(appBarData: appBarData), - ), - const SliverToBoxAdapter( - child: SizedBox(height: 16), - ), - if (children != null) - SliverPadding( - padding: padding, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => children![index], - childCount: children!.length, + child: Stack( + children: [ + CustomScrollView( + controller: _scrollController, + reverse: widget.reverse, + slivers: [ + if (!widget.reverse) + SliverAppBar( + toolbarHeight: kToolbarHeight, + backgroundColor: widget.backgroundColor ?? + Theme.of(context).colorScheme.background, + automaticallyImplyLeading: false, + pinned: true, + flexibleSpace: DidvanAppBar(appBarData: widget.appBarData), ), - ), + if (!widget.reverse) + const SliverToBoxAdapter( + child: SizedBox(height: 16), + ), + if (widget.children != null) + SliverPadding( + padding: widget.padding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.children![index], + childCount: widget.children!.length, + ), + ), + ), + if (widget.slivers != null) + for (var i = 0; i < widget.slivers!.length; i++) + SliverPadding( + padding: widget.padding, + sliver: widget.slivers![i], + ), + if (widget.reverse) + SliverToBoxAdapter( + child: SizedBox( + height: kToolbarHeight + + MediaQuery.of(context).padding.top + + 12, + ), + ), + ], + ), + if (widget.reverse) + _AppBar( + appBarData: widget.appBarData, + scrollController: _scrollController, ), - if (slivers != null) - for (var i = 0; i < slivers!.length; i++) - SliverPadding( - padding: padding, - sliver: slivers![i], - ), ], ), ), ); } } + +class _AppBar extends StatefulWidget { + final AppBarData appBarData; + final ScrollController scrollController; + const _AppBar({ + Key? key, + required this.appBarData, + required this.scrollController, + }) : super(key: key); + + @override + __AppBarState createState() => __AppBarState(); +} + +class __AppBarState extends State<_AppBar> { + bool _isScrolled = false; + + @override + void initState() { + widget.scrollController.addListener(() { + final position = widget.scrollController.position.pixels; + if (position > 10 && _isScrolled == false) { + setState(() { + _isScrolled = true; + }); + } + if (position < 10 && _isScrolled == true) { + setState(() { + _isScrolled = false; + }); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return DidvanAppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + appBarData: widget.appBarData, + hasBorder: _isScrolled, + ); + } +}