D1APP-53 messages ui

This commit is contained in:
MohammadTaha Basiri 2022-02-06 18:05:59 +03:30
parent e2799e9786
commit a0bb03f101
14 changed files with 436 additions and 106 deletions

View File

@ -1,6 +1,5 @@
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:didvan/config/theme_data.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/theme_provider.dart';
import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/providers/user_provider.dart';
import 'package:didvan/routes/route_generator.dart'; import 'package:didvan/routes/route_generator.dart';
@ -24,9 +23,6 @@ class Didvan extends StatelessWidget {
ChangeNotifierProvider<UserProvider>( ChangeNotifierProvider<UserProvider>(
create: (context) => UserProvider(), create: (context) => UserProvider(),
), ),
ChangeNotifierProvider<ServerDataProvider>(
create: (context) => ServerDataProvider(),
),
ChangeNotifierProvider<ThemeProvider>( ChangeNotifierProvider<ThemeProvider>(
create: (context) => ThemeProvider(), create: (context) => ThemeProvider(),
), ),

View File

@ -1,3 +1,5 @@
import 'package:didvan/models/category.dart';
class RadarAttachment { class RadarAttachment {
final int id; final int id;
final String title; final String title;
@ -5,6 +7,8 @@ class RadarAttachment {
final int timeToRead; final int timeToRead;
final String image; final String image;
final bool forManagers; final bool forManagers;
final String createdAt;
final List<CategoryData> categories;
const RadarAttachment({ const RadarAttachment({
required this.id, required this.id,
@ -13,6 +17,8 @@ class RadarAttachment {
required this.timeToRead, required this.timeToRead,
required this.image, required this.image,
required this.forManagers, required this.forManagers,
required this.categories,
required this.createdAt,
}); });
factory RadarAttachment.fromJson(Map<String, dynamic> json) => factory RadarAttachment.fromJson(Map<String, dynamic> json) =>
@ -22,7 +28,13 @@ class RadarAttachment {
description: json['description'], description: json['description'],
timeToRead: json['timeToRead'], timeToRead: json['timeToRead'],
image: json['image'], image: json['image'],
createdAt: json['createdAt'],
forManagers: json['forManagers'], forManagers: json['forManagers'],
categories: List<CategoryData>.from(
json['categories'].map(
(cat) => CategoryData.fromJson(cat),
),
),
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {

View File

@ -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<Direct> createState() => _DirectState();
}
class _DirectState extends State<Direct> {
@override
void initState() {
Future.delayed(Duration.zero, () {
context.read<DirectState>().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(),
),
],
),
);
}
}

View File

@ -1,5 +1,7 @@
import 'dart:io'; 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/providers/core_provider.dart';
import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
@ -9,15 +11,35 @@ import 'package:record/record.dart';
class DirectState extends CoreProvier { class DirectState extends CoreProvier {
final _recorder = Record(); final _recorder = Record();
final List<MessageData> messages = [];
late final int typeId;
final Map<String, List<int>> dailyMessages = {};
File? recordedFile; File? recordedFile;
bool isRecording = false; bool isRecording = false;
Future<void> getMessages(int id) async { Future<void> getMessages() async {
final RequestService service = RequestService(RequestHelper.direct(id)); appState = AppState.busy;
final RequestService service = RequestService(RequestHelper.direct(typeId));
await service.httpGet(); 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() { void deleteRecordedFile() {

View File

@ -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<String, dynamic> pageData;
const Direct({Key? key, required this.pageData}) : super(key: key);
@override
State<Direct> createState() => _DirectState();
}
class _DirectState extends State<Direct> {
@override
void initState() {
final state = context.read<DirectState>();
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<DirectState>();
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<DirectState>(
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(),
),
],
),
);
}
}

View File

@ -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<DirectState>()
.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,
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/chat_room/chat_room.dart'; import 'package:didvan/models/chat_room/chat_room.dart';
import 'package:didvan/routes/routes.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/badge.dart';
import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/divider.dart';
import 'package:didvan/widgets/didvan/text.dart'; import 'package:didvan/widgets/didvan/text.dart';
@ -16,7 +17,7 @@ class ChatRoomItem extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: () => Navigator.of(context).pushNamed( onTap: () => Navigator.of(context).pushNamed(
Routes.direct, Routes.direct,
arguments: chatRoom.id, arguments: {'type': chatRoom.type},
), ),
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
@ -35,15 +36,18 @@ class ChatRoomItem extends StatelessWidget {
style: Theme.of(context).textTheme.bodyText1, style: Theme.of(context).textTheme.bodyText1,
), ),
), ),
DidvanBadge(text: chatRoom.unread.toString()), if (chatRoom.unread != 0)
DidvanBadge(text: chatRoom.unread.toString()),
], ],
), ),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(width: 40), const SizedBox(width: 40),
const Icon( Icon(
DidvanIcons.check_double_light, chatRoom.lastMessage.readed
? DidvanIcons.check_double_light
: DidvanIcons.check_light,
size: 16, size: 16,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@ -54,9 +58,10 @@ class ChatRoomItem extends StatelessWidget {
DidvanText( DidvanText(
chatRoom.lastMessage.text ?? '', chatRoom.lastMessage.text ?? '',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
DidvanText( DidvanText(
chatRoom.updatedAt, DateTimeUtils.momentGenerator(chatRoom.updatedAt),
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.caption, color: Theme.of(context).colorScheme.caption,
) )

View File

@ -108,7 +108,7 @@ class _SplashState extends State<Splash> {
log(token); log(token);
RequestService.token = token; RequestService.token = token;
await userProvider.getUserInfo(); await userProvider.getUserInfo();
await context.read<ServerDataProvider>().getData(); ServerDataProvider.getData();
} }
Navigator.of(context).pushReplacementNamed( Navigator.of(context).pushReplacementNamed(
token == null ? Routes.authenticaion : Routes.home, token == null ? Routes.authenticaion : Routes.home,

View File

@ -1,15 +1,17 @@
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
class ServerDataProvider extends CoreProvier { class ServerDataProvider {
final List<MapEntry> directTypes = []; static final List<MapEntry> directTypes = [];
Future<void> getData() async { static Future<void> getData() async {
await _getDirectTypes(); await _getDirectTypes();
} }
Future<void> _getDirectTypes() async { static int labelToTypeId(String label) =>
directTypes.firstWhere((element) => element.value == label).key;
static Future<void> _getDirectTypes() async {
final service = RequestService(RequestHelper.directTypes); final service = RequestService(RequestHelper.directTypes);
await service.httpGet(); await service.httpGet();
if (service.isSuccess) { if (service.isSuccess) {

View File

@ -2,7 +2,7 @@ import 'package:didvan/pages/authentication/authentication.dart';
import 'package:didvan/pages/authentication/authentication_state.dart'; import 'package:didvan/pages/authentication/authentication_state.dart';
import 'package:didvan/pages/home/comments/comments.dart'; import 'package:didvan/pages/home/comments/comments.dart';
import 'package:didvan/pages/home/comments/comments_state.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/direct/direct_state.dart';
import 'package:didvan/pages/home/home.dart'; import 'package:didvan/pages/home/home.dart';
import 'package:didvan/pages/home/home_state.dart'; import 'package:didvan/pages/home/home_state.dart';
@ -109,7 +109,7 @@ class RouteGenerator {
return _createRoute( return _createRoute(
ChangeNotifierProvider<DirectState>( ChangeNotifierProvider<DirectState>(
create: (context) => DirectState(), create: (context) => DirectState(),
child: Direct(id: settings.arguments as int), child: Direct(pageData: settings.arguments as Map<String, dynamic>),
), ),
); );
case Routes.comments: case Routes.comments:

View File

@ -19,11 +19,17 @@ class RequestHelper {
static const String directTypes = baseUrl + '/direct/types'; static const String directTypes = baseUrl + '/direct/types';
static String direct(int id) => _baseUserUrl + '/direct/$id'; static String direct(int id) => _baseUserUrl + '/direct/$id';
static String tag(List<int> ids) => static String tag({
required List<int> ids,
required String type,
required int itemId,
}) =>
baseUrl + baseUrl +
'/tag' + '/tag' +
_urlConcatGenerator([ _urlConcatGenerator([
const MapEntry('limit', '3'), const MapEntry('limit', '3'),
MapEntry('type', type),
MapEntry('id', itemId.toString()),
MapEntry('tags', _urlListConcatGenerator(ids)) MapEntry('tags', _urlListConcatGenerator(ids))
]); ]);

View File

@ -67,6 +67,7 @@ class DateTimeUtils {
} }
interval = seconds / 86400; interval = seconds / 86400;
if (interval > 1) { if (interval > 1) {
if (interval.floor() == 1) return 'دیروز';
return interval.floor().toString() + " روز پیش"; return interval.floor().toString() + " روز پیش";
} }
interval = seconds / 3600; interval = seconds / 3600;

View File

@ -6,12 +6,31 @@ import 'package:flutter/material.dart';
class DidvanAppBar extends StatelessWidget { class DidvanAppBar extends StatelessWidget {
final AppBarData appBarData; 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 @override
Widget build(BuildContext context) { 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), 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( child: Row(
children: [ children: [
IconButton( IconButton(

View File

@ -2,12 +2,14 @@ import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/widgets/didvan/app_bar.dart'; import 'package:didvan/widgets/didvan/app_bar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DidvanScaffold extends StatelessWidget { class DidvanScaffold extends StatefulWidget {
final List<Widget>? slivers; final List<Widget>? slivers;
final List<Widget>? children; final List<Widget>? children;
final AppBarData appBarData; final AppBarData appBarData;
final EdgeInsets padding; final EdgeInsets padding;
final Color? backgroundColor; final Color? backgroundColor;
final bool reverse;
const DidvanScaffold({ const DidvanScaffold({
Key? key, Key? key,
this.slivers, this.slivers,
@ -15,47 +17,120 @@ class DidvanScaffold extends StatelessWidget {
this.children, this.children,
this.padding = const EdgeInsets.symmetric(horizontal: 16), this.padding = const EdgeInsets.symmetric(horizontal: 16),
this.backgroundColor, this.backgroundColor,
this.reverse = false,
}) : super(key: key); }) : super(key: key);
@override
State<DidvanScaffold> createState() => _DidvanScaffoldState();
}
class _DidvanScaffoldState extends State<DidvanScaffold> {
final _scrollController = ScrollController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top; final double statusBarHeight = MediaQuery.of(context).padding.top;
return Scaffold( return Scaffold(
backgroundColor: backgroundColor, backgroundColor: widget.backgroundColor,
body: Padding( body: Padding(
padding: EdgeInsets.only(top: statusBarHeight), padding: EdgeInsets.only(top: statusBarHeight),
child: CustomScrollView( child: Stack(
slivers: [ children: [
SliverAppBar( CustomScrollView(
toolbarHeight: kToolbarHeight, controller: _scrollController,
backgroundColor: reverse: widget.reverse,
backgroundColor ?? Theme.of(context).colorScheme.background, slivers: [
automaticallyImplyLeading: false, if (!widget.reverse)
pinned: true, SliverAppBar(
flexibleSpace: DidvanAppBar(appBarData: appBarData), toolbarHeight: kToolbarHeight,
), backgroundColor: widget.backgroundColor ??
const SliverToBoxAdapter( Theme.of(context).colorScheme.background,
child: SizedBox(height: 16), automaticallyImplyLeading: false,
), pinned: true,
if (children != null) flexibleSpace: DidvanAppBar(appBarData: widget.appBarData),
SliverPadding(
padding: padding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => children![index],
childCount: children!.length,
), ),
), 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,
);
}
}