diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07ae338..08d99b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ - You can join us on [AnitaB.org Open Source Zulip](https://anitab-org.zulipchat.com/). Each active repo has its own stream to direct questions to (for example #powerup or #portal). Mentorship System stream is [#mentorship-system](https://anitab-org.zulipchat.com/#narrow/stream/222534-mentorship-system). - Remember that this is an inclusive community, committed to creating a safe, positive environment. See the full [Code of Conduct](code_of_conduct.md). -- Follow our [Commit Message Style Guide](https://github.com/anitab-org/mentorship-android/wiki/Commit-Message-Style-Guide) when you commit your changes. +- Follow our [Commit Message Style Guide](https://github.com/anitab-org/mentorship-flutter/wiki/Commit-Message-Style-Guide) when you commit your changes. - Please consider raising an issue before submitting a pull request (PR) to solve a problem that is not present in our [issue tracker](https://github.com/anitab-org/mentorship-flutter/issues). This allows maintainers to first validate the issue you are trying to solve and also reference the PR to a specific issue. - When developing a new feature, include at least one test when applicable. - When submitting a PR, please follow [this template](.github/PULL_REQUEST_TEMPLATE.md) (which will probably be already filled up once you create the PR). diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index a1f527c..202fec5 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -16,20 +16,33 @@ import 'package:mentorship_client/screens/home/pages/stats/stats_page.dart'; import 'package:mentorship_client/screens/settings/settings_screen.dart'; import 'package:toast/toast.dart'; +import 'pages/members/bloc/members_page_bloc.dart'; +import 'pages/members/bloc/members_page_event.dart'; +import 'pages/stats/bloc/stats_page_bloc.dart'; +import 'pages/stats/bloc/stats_page_event.dart'; + /// [HomeScreen] is the main screen in the app. It's what user sees after successfully logging in. /// HomeScreen's main task is to have scaffold with AppBar and BottomNavBar. Content (i.e body) /// is provided by one of 5 Pages - [StatsPage], [ProfilePage], [RelationPage], [MembersPage] and [RequestsPage]. /// HomeScreen manages displaying of these pages using BottomNavBar and PageView. -class HomeScreen extends StatelessWidget { - final pageController = PageController(); +class HomeScreen extends StatefulWidget { + @override + _HomeScreenState createState() => _HomeScreenState(); +} +class _HomeScreenState extends State { + final pageController = PageController(); + int _currentIndex = 0; @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ // I think its too high in the widget tree, but I couldn't find a better solution + BlocProvider( - create: (context) => ProfilePageBloc(userRepository: UserRepository.instance), + create: (context) => + ProfilePageBloc(userRepository: UserRepository.instance)..add(ProfilePageShowed()), + child: ProfilePage(), ), BlocProvider( create: (context) => HomeBloc(), @@ -38,12 +51,26 @@ class HomeScreen extends StatelessWidget { create: (context) => RelationPageBloc( relationRepository: RelationRepository.instance, taskRepository: TaskRepository.instance, - ), + )..add(RelationPageShowed()), + child: RelationPage(), ), BlocProvider( create: (context) => RequestsPageBloc( relationRepository: RelationRepository.instance, - ), + )..add(RequestsPageShowed()), + child: RequestsPage(), + ), + + BlocProvider( + create: (context) => StatsPageBloc(userRepository: UserRepository.instance) + ..add( + StatsPageShowed(), + ), + ), + BlocProvider( + create: (context) => + MembersPageBloc(userRepository: UserRepository.instance)..add(MembersPageShowed()), + child: MembersPage(), ), ], child: BlocListener( @@ -84,11 +111,11 @@ class HomeScreen extends StatelessWidget { ], ), bottomNavigationBar: BottomNavyBar( - showElevation: false, - onItemSelected: - (index) => // This triggers when the user clicks item on BottomNavyBar - BlocProvider.of(context).add(HomeEvent.fromIndex(index)), - selectedIndex: state.index, + selectedIndex: _currentIndex, + onItemSelected: (index) { + setState(() => _currentIndex = index); + pageController.jumpToPage(index); + }, items: [ BottomNavyBarItem( icon: Icon(Icons.home), diff --git a/lib/screens/home/pages/members/bloc/members_page_bloc.dart b/lib/screens/home/pages/members/bloc/members_page_bloc.dart index 02ae885..4568a4d 100644 --- a/lib/screens/home/pages/members/bloc/members_page_bloc.dart +++ b/lib/screens/home/pages/members/bloc/members_page_bloc.dart @@ -13,12 +13,19 @@ class MembersPageBloc extends Bloc { final UserRepository userRepository; int pageNumber = 1; MembersPageBloc({@required this.userRepository}) : assert(userRepository != null); - //TODO: debounce the Events in order to prevent spamming our API @override MembersPageState get initialState => MembersPageInitial(); @override Stream mapEventToState(MembersPageEvent event) async* { + if (event is MembersPageShowed) { + yield* _mapEventToMembersShowed(event); + } else if (event is MembersPageRefresh) { + yield* _mapEventToMembersRefresh(event); + } + } + + Stream _mapEventToMembersShowed(MembersPageEvent event) async* { final currentState = state; if (event is MembersPageShowed && !_hasReachedMax(currentState)) { @@ -39,11 +46,26 @@ class MembersPageBloc extends Bloc { ); } } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("MembersPageBloc: Failure catched: $failure.message"); yield MembersPageFailure(failure.message); } } } + + Stream _mapEventToMembersRefresh(MembersPageEvent event) async* { + final currentState = state; + + if (event is MembersPageRefresh && !_hasReachedMax(currentState)) { + try { + yield MembersPageLoading(); + final List users = await userRepository.getVerifiedUsers(pageNumber); + yield MembersPageSuccess(users: users, hasReachedMax: false); + } on Failure catch (failure) { + Logger.root.severe("MembersPageBloc: Failure catched: $failure.message"); + yield state; + } + } + } } bool _hasReachedMax(MembersPageState state) => state is MembersPageSuccess && state.hasReachedMax; diff --git a/lib/screens/home/pages/members/bloc/members_page_event.dart b/lib/screens/home/pages/members/bloc/members_page_event.dart index b6bae10..f20a69e 100644 --- a/lib/screens/home/pages/members/bloc/members_page_event.dart +++ b/lib/screens/home/pages/members/bloc/members_page_event.dart @@ -8,3 +8,5 @@ abstract class MembersPageEvent extends Equatable { } class MembersPageShowed extends MembersPageEvent {} + +class MembersPageRefresh extends MembersPageEvent {} diff --git a/lib/screens/home/pages/members/members_page.dart b/lib/screens/home/pages/members/members_page.dart index 5e163f9..f14e9a5 100644 --- a/lib/screens/home/pages/members/members_page.dart +++ b/lib/screens/home/pages/members/members_page.dart @@ -1,74 +1,82 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mentorship_client/remote/models/user.dart'; -import 'package:mentorship_client/remote/repositories/user_repository.dart'; import 'package:mentorship_client/screens/home/pages/members/bloc/bloc.dart'; import 'package:mentorship_client/screens/home/pages/members/widgets/member_list_tile.dart'; import 'package:mentorship_client/screens/member_profile/member_profile.dart'; -class MembersPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - MembersPageBloc(userRepository: UserRepository.instance)..add(MembersPageShowed()), - child: _MembersPage()); - } -} - -class _MembersPage extends StatefulWidget { +class MembersPage extends StatefulWidget { @override _MembersPageState createState() => _MembersPageState(); } -class _MembersPageState extends State<_MembersPage> { +class _MembersPageState extends State { final _scrollController = ScrollController(); // ignore: dart.core.Sink MembersPageBloc _membersPageBloc; final _scrollThreshold = 10.0; + Completer _refreshCompleter; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); _membersPageBloc = BlocProvider.of(context); + _refreshCompleter = Completer(); } @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is MembersPageFailure) { - return Center( - child: Text('failed to get users'), - ); - } - if (state is MembersPageSuccess) { - if (state.users.isEmpty) { + return BlocConsumer(listener: (context, state) { + if (state is MembersPageShowed) { + _refreshCompleter?.complete(); + _refreshCompleter = Completer(); + } + }, builder: (context, state) { + return BlocBuilder( + builder: (context, state) { + if (state is MembersPageFailure) { return Center( - child: Text('no users'), + child: Text('Failed to get users'), + ); + } + if (state is MembersPageSuccess) { + if (state.users.isEmpty) { + return Center( + child: Text('No users'), + ); + } + return RefreshIndicator( + onRefresh: () { + BlocProvider.of(context).add( + MembersPageRefresh(), + ); + return _refreshCompleter.future; + }, + child: ListView.builder( + itemBuilder: (BuildContext context, int index) { + bool _reachedEnd = index > state.users.length - 1; + User user = (!_reachedEnd) ? state.users[index] : null; + return _reachedEnd + ? BottomLoader() + : InkWell( + onTap: () => _openMemberProfileScreen(context, user), + child: MemberListTile(user: user), + ); + }, + itemCount: state.hasReachedMax ? (state.users.length) : (state.users.length + 1), + controller: _scrollController, + ), ); } - return ListView.builder( - itemBuilder: (BuildContext context, int index) { - bool _reachedEnd = index > state.users.length - 1; - User user = (!_reachedEnd) ? state.users[index] : null; - return _reachedEnd - ? BottomLoader() - : InkWell( - onTap: () => _openMemberProfileScreen(context, user), - child: MemberListTile(user: user), - ); - }, - itemCount: state.hasReachedMax ? (state.users.length) : (state.users.length + 1), - controller: _scrollController, + return Center( + child: CircularProgressIndicator(), ); - } - return Center( - child: CircularProgressIndicator(), - ); - }, - ); + }, + ); + }); } void _openMemberProfileScreen(BuildContext context, User user) { diff --git a/lib/screens/home/pages/profile/bloc/profile_page_bloc.dart b/lib/screens/home/pages/profile/bloc/profile_page_bloc.dart index 24b54c2..aa5ed7b 100644 --- a/lib/screens/home/pages/profile/bloc/profile_page_bloc.dart +++ b/lib/screens/home/pages/profile/bloc/profile_page_bloc.dart @@ -21,16 +21,42 @@ class ProfilePageBloc extends Bloc { @override Stream mapEventToState(ProfilePageEvent event) async* { + if (event is ProfilePageShowed) { + yield* mapEventToProfileShowed(event); + } else if (event is ProfilePageRefresh) { + yield* mapEventToRefreshRequested(event); + } else { + yield* mapEventToProfileEditing(event); + } + } + + Stream mapEventToProfileShowed(ProfilePageEvent event) async* { if (event is ProfilePageShowed) { yield ProfilePageLoading(); try { _user = await userRepository.getCurrentUser(); yield ProfilePageSuccess(_user, message: event.message); } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("ProfilePageBloc: Failure catched: $failure.message"); yield ProfilePageFailure(message: failure.message); } } + } + + Stream mapEventToRefreshRequested(ProfilePageEvent event) async* { + if (event is ProfilePageRefresh) { + yield ProfilePageLoading(); + try { + _user = await userRepository.getCurrentUser(); + yield ProfilePageSuccess(_user, message: event.message); + } on Failure catch (failure) { + Logger.root.severe("ProfilePageBloc: Failure catched: $failure.message"); + yield state; + } + } + } + + Stream mapEventToProfileEditing(ProfilePageEvent event) async* { if (event is ProfilePageEditStarted) { yield ProfilePageEditing(_user); } @@ -54,7 +80,7 @@ class ProfilePageBloc extends Bloc { CustomResponse response = await userRepository.updateUser(updatedUser); add(ProfilePageShowed(message: response.message)); } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("ProfilePageBloc: Failure catched: $failure.message"); add(ProfilePageShowed(message: failure.message)); } } diff --git a/lib/screens/home/pages/profile/bloc/profile_page_event.dart b/lib/screens/home/pages/profile/bloc/profile_page_event.dart index 2f2be55..d81c85a 100644 --- a/lib/screens/home/pages/profile/bloc/profile_page_event.dart +++ b/lib/screens/home/pages/profile/bloc/profile_page_event.dart @@ -14,6 +14,12 @@ class ProfilePageShowed extends ProfilePageEvent { ProfilePageShowed({this.message}); } +class ProfilePageRefresh extends ProfilePageEvent { + final String message; + + ProfilePageRefresh({this.message}); +} + class ProfilePageEditStarted extends ProfilePageEvent {} class ProfilePageEditSubmitted extends ProfilePageEvent { diff --git a/lib/screens/home/pages/profile/profile_page.dart b/lib/screens/home/pages/profile/profile_page.dart index 3ebd0e6..70f3f06 100644 --- a/lib/screens/home/pages/profile/profile_page.dart +++ b/lib/screens/home/pages/profile/profile_page.dart @@ -2,15 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mentorship_client/extensions/context.dart'; import 'package:mentorship_client/remote/models/user.dart'; +import 'package:mentorship_client/remote/repositories/user_repository.dart'; import 'package:mentorship_client/screens/home/pages/profile/bloc/bloc.dart'; import 'package:mentorship_client/widgets/loading_indicator.dart'; +import 'dart:async'; class ProfilePage extends StatefulWidget { @override _ProfilePageState createState() => _ProfilePageState(); } -class _ProfilePageState extends State { +class _ProfilePageState extends State with SingleTickerProviderStateMixin { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _usernameController = TextEditingController(); // not changeable @@ -25,11 +27,19 @@ class _ProfilePageState extends State { bool _availableToMentor; bool _needsMentoring; bool editing = false; + AnimationController _animationController; + + Completer _refreshCompleter; @override void initState() { - context.bloc()..add(ProfilePageShowed()); + _animationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 500), + upperBound: 0.5, + ); super.initState(); + _refreshCompleter = Completer(); } @override @@ -48,6 +58,8 @@ class _ProfilePageState extends State { final bloc = BlocProvider.of(context); if (state is ProfilePageEditing) { + showProgressIndicator(context); + _formKey.currentState.save(); user.availableToMentor = _availableToMentor; user.needsMentoring = _needsMentoring; @@ -64,7 +76,9 @@ class _ProfilePageState extends State { user.interests = _interestsController.text; bloc.add(ProfilePageEditSubmitted(user)); + _animationController.reverse(); } else if (state is ProfilePageSuccess) { + _animationController.forward(); bloc.add(ProfilePageEditStarted()); } }, @@ -74,10 +88,11 @@ class _ProfilePageState extends State { ), ); }), - body: BlocListener( + body: BlocConsumer( listener: (context, state) { if (state.message != null) { context.showSnackBar(state.message); + Navigator.of(context).pop(); } if (state is ProfilePageEditing) { _nameController.text = state.user.name; @@ -89,182 +104,208 @@ class _ProfilePageState extends State { _occupationController.text = state.user.occupation; _organizationController.text = state.user.organization; _skillsController.text = state.user.skills; - _interestsController.text = state.user.interests; if (_availableToMentor == null) _availableToMentor = state.user.availableToMentor; if (_needsMentoring == null) _needsMentoring = state.user.needsMentoring; } + if (state is ProfilePageShowed) { + _refreshCompleter?.complete(); + _refreshCompleter = Completer(); + } }, - child: BlocBuilder(builder: (context, state) { - if (state is ProfilePageSuccess) { - _nameController.text = state.user.name; - _usernameController.text = state.user.username; - _emailController.text = state.user.email; - _bioController.text = state.user.bio; - _slackController.text = state.user.slackUsername; - _locationController.text = state.user.location; - _occupationController.text = state.user.occupation; - _organizationController.text = state.user.organization; - _skillsController.text = state.user.skills; - _interestsController.text = state.user.interests; - if (_availableToMentor == null) _availableToMentor = state.user.availableToMentor; - if (_needsMentoring == null) _needsMentoring = state.user.needsMentoring; + builder: (context, state) { + return BlocBuilder(builder: (context, state) { + if (state is ProfilePageSuccess) { + _nameController.text = state.user.name; + _usernameController.text = state.user.username; + _emailController.text = state.user.email; + _bioController.text = state.user.bio; + _slackController.text = state.user.slackUsername; + _locationController.text = state.user.location; + _occupationController.text = state.user.occupation; + _organizationController.text = state.user.organization; + _skillsController.text = state.user.skills; + _interestsController.text = state.user.interests; + if (_availableToMentor == null) _availableToMentor = state.user.availableToMentor; + if (_needsMentoring == null) _needsMentoring = state.user.needsMentoring; - return _createPage(context, state.user, false); - } - if (state is ProfilePageEditing) { - print(_interestsController.text); - return _createPage(context, state.user, true); - } + return RefreshIndicator( + onRefresh: () { + BlocProvider.of(context).add( + ProfilePageRefresh(), + ); + return _refreshCompleter.future; + }, + child: _createPage( + context, + state.user, + false, + ), + ); + } + if (state is ProfilePageEditing) { + print(_interestsController.text); + return _createPage(context, state.user, true); + } - if (state is ProfilePageFailure) { - return Text(state.message); - } - if (state is ProfilePageLoading) { - return LoadingIndicator(); - } + if (state is ProfilePageFailure) { + return Text(state.message); + } + if (state is ProfilePageLoading) { + return LoadingIndicator(); + } - if (state is ProfilePageInitial) { - return LoadingIndicator(); - } else - return Text("Error: Unknown ProfilePageState"); - }), + if (state is ProfilePageInitial) { + return LoadingIndicator(); + } else + return Text("Error: Unknown ProfilePageState"); + }); + }, ), ); } Widget _createPage(BuildContext context, User user, bool editing) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - shrinkWrap: true, - children: [ - SizedBox(height: 24), - Center( - child: ClipOval( - child: Container( - color: Colors.deepPurple, - width: MediaQuery.of(context).size.width / 2, - height: MediaQuery.of(context).size.width / 2, + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Opacity( + opacity: 0.5 + _animationController.value, + child: child, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + shrinkWrap: true, + children: [ + SizedBox(height: 24), + Center( + child: ClipOval( + child: Container( + color: Colors.deepPurple, + width: MediaQuery.of(context).size.width / 2, + height: MediaQuery.of(context).size.width / 2, + ), ), ), - ), - Form( - key: _formKey, - child: Column( - children: [ - Container( - width: MediaQuery.of(context).size.width / 2, - child: TextFormField( - controller: _nameController, - enabled: editing, + Form( + key: _formKey, + child: Column( + children: [ + Container( + width: MediaQuery.of(context).size.width / 2, + child: TextFormField( + controller: _nameController, + enabled: editing, + onSaved: (value) { + user.name = value; + }, + textAlign: TextAlign.center, + decoration: const InputDecoration( + border: const UnderlineInputBorder(), + ), + ), + ), + TextFormField( + controller: _usernameController, + enabled: false, onSaved: (value) { user.name = value; }, - textAlign: TextAlign.center, decoration: const InputDecoration( + labelText: "Username", border: const UnderlineInputBorder(), ), ), - ), - TextFormField( - controller: _usernameController, - enabled: false, - onSaved: (value) { - user.name = value; - }, - decoration: const InputDecoration( - labelText: "Username", - border: const UnderlineInputBorder(), + TextFormField( + controller: _emailController, + enabled: false, + onSaved: (value) { + user.email = value; + }, + decoration: const InputDecoration( + labelText: "Email", + border: const UnderlineInputBorder(), + ), ), - ), - TextFormField( - controller: _emailController, - enabled: false, - onSaved: (value) { - user.email = value; - }, - decoration: const InputDecoration( - labelText: "Email", - border: const UnderlineInputBorder(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Available to mentor"), + Checkbox( + value: _availableToMentor, + onChanged: editing + ? (value) { + setState(() { + _availableToMentor = value; + }); + } + : null, + ), + ], ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Available to mentor"), - Checkbox( - value: _availableToMentor, - onChanged: editing - ? (value) { - setState(() { - _availableToMentor = value; - }); - } - : null, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Needs mentoring"), - Checkbox( - value: _needsMentoring, - onChanged: editing - ? (value) { - setState(() { - _needsMentoring = value; - }); - } - : null, - ), - ], - ), - _buildTextFormField( - "Bio", - editing, - _bioController, - (value) { - user.bio = value; - }, - ), - _buildTextFormField("Slack username", editing, _slackController, (value) { - user.slackUsername = value; - }), - _buildTextFormField("Location", editing, _locationController, (value) { - user.location = value; - }), - _buildTextFormField("Occupation", editing, _occupationController, (value) { - user.occupation = value; - }), - _buildTextFormField("Organization", editing, _organizationController, (value) { - user.organization = value; - }), - _buildTextFormField("Skills", editing, _skillsController, (value) { - user.skills = value; - }), - _buildTextFormField("Interests", editing, _interestsController, (value) { - user.interests = value; - }), - ], - ), - ) - ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Needs mentoring"), + Checkbox( + value: _needsMentoring, + onChanged: editing + ? (value) { + setState(() { + _needsMentoring = value; + }); + } + : null, + ), + ], + ), + _buildTextFormField( + "Bio", + editing, + _bioController, + (value) { + user.bio = value; + }, + ), + _buildTextFormField("Slack username", editing, _slackController, (value) { + user.slackUsername = value; + }), + _buildTextFormField("Location", editing, _locationController, (value) { + user.location = value; + }), + _buildTextFormField("Occupation", editing, _occupationController, (value) { + user.occupation = value; + }), + _buildTextFormField("Organization", editing, _organizationController, (value) { + user.organization = value; + }), + _buildTextFormField("Skills", editing, _skillsController, (value) { + user.skills = value; + }), + _buildTextFormField("Interests", editing, _interestsController, (value) { + user.interests = value; + }), + ], + ), + ) + ], + ), ), ); } +} - _buildTextFormField( - String text, bool editing, TextEditingController controller, Function(String) onSaved) { - return TextFormField( - controller: controller, - enabled: editing, - onSaved: onSaved, - decoration: InputDecoration( - labelText: text, - border: UnderlineInputBorder(), - ), - ); - } +_buildTextFormField( + String text, bool editing, TextEditingController controller, Function(String) onSaved) { + return TextFormField( + controller: controller, + enabled: editing, + onSaved: onSaved, + decoration: InputDecoration( + labelText: text, + border: UnderlineInputBorder(), + ), + ); } diff --git a/lib/screens/home/pages/relation/bloc/relation_page_bloc.dart b/lib/screens/home/pages/relation/bloc/relation_page_bloc.dart index 02d5045..b37870f 100644 --- a/lib/screens/home/pages/relation/bloc/relation_page_bloc.dart +++ b/lib/screens/home/pages/relation/bloc/relation_page_bloc.dart @@ -39,6 +39,20 @@ class RelationPageBloc extends Bloc { yield RelationPageFailure(message: failure.message); } } + if (event is RelationPageRefresh) { + yield RelationPageLoading(); + try { + Relation relation = await relationRepository.getCurrentRelation(); + List tasks; + if (relation != null) { + tasks = await taskRepository.getAllTasks(relation.id); + } + yield RelationPageSuccess(relation, tasks); + } on Failure catch (failure) { + Logger.root.severe("RelationPageBloc: Failure catched: $failure.message"); + yield state; + } + } if (event is RelationPageCancelledRelation) { yield RelationPageLoading(); @@ -48,7 +62,7 @@ class RelationPageBloc extends Bloc { message: response .message); // Failure, because relation doesn't exist anymore. It's kinda dirty but works } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("RelationPageBloc: Failure catched: $failure.message"); yield RelationPageFailure(message: failure.message); } } @@ -60,7 +74,7 @@ class RelationPageBloc extends Bloc { var tasks = await taskRepository.getAllTasks(event.relation.id); yield RelationPageSuccess(event.relation, tasks, message: response.message); } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("RelationPageBloc: Failure catched: $failure.message"); yield RelationPageFailure(message: failure.message); } } @@ -72,7 +86,7 @@ class RelationPageBloc extends Bloc { var tasks = await taskRepository.getAllTasks(event.relation.id); yield RelationPageSuccess(event.relation, tasks, message: response.message); } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("RelationPageBloc: Failure catched: $failure.message"); yield RelationPageFailure(message: failure.message); } } @@ -83,7 +97,7 @@ class RelationPageBloc extends Bloc { var tasks = await taskRepository.getAllTasks(event.relation.id); yield RelationPageSuccess(event.relation, tasks, message: response.message); } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("RelationPageBloc: Failure catched: $failure.message"); yield RelationPageFailure(message: failure.message); } } diff --git a/lib/screens/home/pages/relation/bloc/relation_page_event.dart b/lib/screens/home/pages/relation/bloc/relation_page_event.dart index 7ef971c..c4c10a1 100644 --- a/lib/screens/home/pages/relation/bloc/relation_page_event.dart +++ b/lib/screens/home/pages/relation/bloc/relation_page_event.dart @@ -11,6 +11,11 @@ class RelationPageShowed extends RelationPageEvent { List get props => null; } +class RelationPageRefresh extends RelationPageEvent { + @override + List get props => null; +} + class RelationPageCancelledRelation extends RelationPageEvent { final int relationId; @@ -40,7 +45,6 @@ class TaskCompleted extends RelationPageEvent { List get props => [relation, taskId]; } - class TaskDeleted extends RelationPageEvent { final Relation relation; final int taskId; diff --git a/lib/screens/home/pages/relation/relation_page.dart b/lib/screens/home/pages/relation/relation_page.dart index bb5cabc..869cce6 100644 --- a/lib/screens/home/pages/relation/relation_page.dart +++ b/lib/screens/home/pages/relation/relation_page.dart @@ -1,25 +1,34 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mentorship_client/extensions/context.dart'; import 'package:mentorship_client/extensions/datetime.dart'; import 'package:mentorship_client/remote/models/task.dart'; import 'package:mentorship_client/remote/requests/task_request.dart'; +import 'package:mentorship_client/screens/home/bloc/bloc.dart'; +import 'package:mentorship_client/screens/home/bloc/home_bloc.dart'; import 'package:mentorship_client/screens/home/pages/relation/bloc/bloc.dart'; import 'package:mentorship_client/widgets/bold_text.dart'; import 'package:mentorship_client/widgets/loading_indicator.dart'; +import 'package:auto_size_text/auto_size_text.dart'; class RelationPage extends StatefulWidget { @override _RelationPageState createState() => _RelationPageState(); } -// TODO: Use BLOC to make state management more robust - class _RelationPageState extends State { + Completer _refreshCompleter; + @override - Widget build(BuildContext context) { - BlocProvider.of(context).add(RelationPageShowed()); + void initState() { + super.initState(); + _refreshCompleter = Completer(); + } + @override + Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( @@ -30,13 +39,17 @@ class _RelationPageState extends State { Tab(text: "Tasks".toUpperCase()), ], ), - body: BlocListener( - listener: (context, state) { - if (state.message != null) { - context.showSnackBar(state.message); - } - }, - child: BlocBuilder( + body: BlocConsumer(listener: (context, state) { + if (state.message != null && state is RelationPageSuccess) { + context.showSnackBar(state.message); + Navigator.of(context).pop(); + } + if (state is RelationPageShowed) { + _refreshCompleter?.complete(); + _refreshCompleter = Completer(); + } + }, builder: (context, state) { + return BlocBuilder( builder: (context, state) { if (state is RelationPageSuccess) { return TabBarView( @@ -49,72 +62,131 @@ class _RelationPageState extends State { if (state is RelationPageFailure) { return Center( - child: Text(state.message), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(state.message), + SizedBox( + height: MediaQuery.of(context).size.height / 40, + ), + Container( + height: MediaQuery.of(context).size.height / 17, + width: MediaQuery.of(context).size.width * 0.47, + child: RaisedButton( + color: Theme.of(context).accentColor, + onPressed: () { + //ignore: close_sinks + final bloc = BlocProvider.of(context); + + bloc.add(MembersPageSelected()); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search, + color: Colors.white, + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.3, + child: AutoSizeText( + "Find members", + style: TextStyle( + color: Colors.white, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ) + ], + ), ); } return LoadingIndicator(); }, - ), - ), + ); + }), ), ); } Widget _buildDetailsTab(BuildContext context, RelationPageSuccess state) { - return Padding( - padding: EdgeInsets.all(8), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - alignment: Alignment.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BoldText("Mentor: ", state.relation.mentor.name), - BoldText("Mentee: ", state.relation.mentee.name), - BoldText( - "End date: ", DateTimeX.fromTimestamp(state.relation.endsOn).toDateString()), - BoldText("Notes: ", state.relation.notes), - ], - ), - ), - RaisedButton( - color: Theme.of(context).accentColor, - child: Text("Cancel".toUpperCase(), style: TextStyle(color: Colors.white)), - onPressed: () { - //ignore: close_sinks - final bloc = BlocProvider.of(context); - - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text("Cancel Relation"), - content: Text("Are you sure you want to cancel the relation"), - actions: [ - FlatButton( - child: Text("Yes"), - onPressed: () { - bloc.add(RelationPageCancelledRelation(state.relation.id)); - Navigator.of(context).pop(); - }, - ), - FlatButton( - child: Text("No"), - onPressed: () { - Navigator.of(context).pop(); - }, + return RefreshIndicator( + onRefresh: () { + BlocProvider.of(context).add( + RelationPageRefresh(), + ); + return _refreshCompleter.future; + }, + child: ListView( + children: [ + Column( + children: [ + Padding( + padding: EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BoldText("Mentor: ", state.relation.mentor.name), + BoldText("Mentee: ", state.relation.mentee.name), + BoldText("End date: ", + DateTimeX.fromTimestamp(state.relation.endsOn).toDateString()), + BoldText("Notes: ", state.relation.notes), + ], ), - ], - ); - }, - ); - }, - ) + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.55, + ), + RaisedButton( + color: Theme.of(context).accentColor, + child: Text("Cancel".toUpperCase(), style: TextStyle(color: Colors.white)), + onPressed: () { + //ignore: close_sinks + final bloc = BlocProvider.of(context); + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text("Cancel Relation"), + content: Text("Are you sure you want to cancel the relation"), + actions: [ + FlatButton( + child: Text("Yes"), + onPressed: () { + bloc.add(RelationPageCancelledRelation(state.relation.id)); + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text("No"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + }, + ) + ], + ), + ), + ], + ), ], ), ); @@ -138,43 +210,60 @@ class _RelationPageState extends State { Task task = state.tasks[index]; //ignore: close_sinks final bloc = BlocProvider.of(context); - - return InkWell( - onTap: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text("Delete task"), - content: Text("Are you sure you want to delete the task?"), - actions: [ - FlatButton( - child: Text("Delete"), - onPressed: () { - bloc.add(TaskDeleted(state.relation, task.id)); - Navigator.of(context).pop(); - }, - ) - ], - ), - ); - }, - child: Row( - children: [ - GestureDetector( - onTap: () { - if (!task.isDone) { - context.toast("hey"); - bloc.add(TaskCompleted(state.relation, task.id)); - } else - context.toast("Task already achieved."); - }, - child: Checkbox( - value: task.isDone, + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + if (!task.isDone) { + bloc.add(TaskCompleted(state.relation, task.id)); + showProgressIndicator(context); + } else + context.toast("Task already achieved."); + }, + child: Checkbox( + value: task.isDone, + ), ), + Text(task.description), + ], + ), + IconButton( + icon: Icon( + Icons.delete, + color: Colors.grey[700], ), - Text(task.description), - ], - ), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Delete task"), + content: Text("Are you sure you want to delete the task?"), + actions: [ + FlatButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("Cancel"), + ), + FlatButton( + child: Text("Delete"), + onPressed: () { + bloc.add(TaskDeleted(state.relation, task.id)); + Navigator.of(context).pop(); + showProgressIndicator(context); + }, + ), + ], + ), + ); + }, + ), + ], ); }, ); @@ -215,6 +304,7 @@ class _RelationPageState extends State { ); Navigator.of(context).pop(); + showProgressIndicator(context); }, ) ], diff --git a/lib/screens/home/pages/requests/bloc/requests_page_bloc.dart b/lib/screens/home/pages/requests/bloc/requests_page_bloc.dart index 7f966ee..6a0dae3 100644 --- a/lib/screens/home/pages/requests/bloc/requests_page_bloc.dart +++ b/lib/screens/home/pages/requests/bloc/requests_page_bloc.dart @@ -19,15 +19,36 @@ class RequestsPageBloc extends Bloc { @override Stream mapEventToState(RequestsPageEvent event) async* { + if (event is RequestsPageShowed) { + yield* _mapEventToRequestsShowed(event); + } else if (event is RequestsPageRefresh) { + yield* _mapEventToRequestsRefresh(event); + } + } + + Stream _mapEventToRequestsShowed(RequestsPageEvent event) async* { if (event is RequestsPageShowed) { yield RequestsPageLoading(); try { List relations = await relationRepository.getAllRelationsAndRequests(); yield RequestsPageSuccess(relations); } on Failure catch (failure) { - Logger.root.severe("RequestsPageBloc: ${failure.message}"); + Logger.root.severe("RequestsPageBloc: Failure catched: $failure.message"); yield RequestsPageFailure(message: failure.message); } } } + + Stream _mapEventToRequestsRefresh(RequestsPageEvent event) async* { + if (event is RequestsPageRefresh) { + yield RequestsPageLoading(); + try { + List relations = await relationRepository.getAllRelationsAndRequests(); + yield RequestsPageSuccess(relations); + } on Failure catch (failure) { + Logger.root.severe("RequestsPageBloc: Failure catched: $failure.message"); + yield state; + } + } + } } diff --git a/lib/screens/home/pages/requests/bloc/requests_page_event.dart b/lib/screens/home/pages/requests/bloc/requests_page_event.dart index 06906b8..357442e 100644 --- a/lib/screens/home/pages/requests/bloc/requests_page_event.dart +++ b/lib/screens/home/pages/requests/bloc/requests_page_event.dart @@ -9,11 +9,7 @@ class RequestsPageShowed extends RequestsPageEvent { List get props => null; } -//class RequestsPageRelationAccepted extends RequestsPageEvent { -// final int relationId; -// -// RequestsPageRelationAccepted(this.relationId); -// -// @override -// List get props => [relationId]; -//} +class RequestsPageRefresh extends RequestsPageEvent { + @override + List get props => null; +} diff --git a/lib/screens/home/pages/requests/requests_page.dart b/lib/screens/home/pages/requests/requests_page.dart index 44998ed..d4add32 100644 --- a/lib/screens/home/pages/requests/requests_page.dart +++ b/lib/screens/home/pages/requests/requests_page.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mentorship_client/extensions/datetime.dart'; import 'package:mentorship_client/remote/models/relation.dart'; +import 'package:mentorship_client/screens/home/pages/members/bloc/bloc.dart'; import 'package:mentorship_client/screens/home/pages/requests/bloc/bloc.dart'; import 'package:mentorship_client/screens/request_detail/request_detail.dart'; import 'package:mentorship_client/widgets/bold_text.dart'; @@ -13,10 +16,16 @@ class RequestsPage extends StatefulWidget { } class _RequestsPageState extends State { + Completer _refreshCompleter; + @override - Widget build(BuildContext context) { - BlocProvider.of(context).add(RequestsPageShowed()); + void initState() { + super.initState(); + _refreshCompleter = Completer(); + } + @override + Widget build(BuildContext context) { return DefaultTabController( length: 3, child: Scaffold( @@ -28,94 +37,116 @@ class _RequestsPageState extends State { Tab(text: "All".toUpperCase()), ], ), - body: BlocBuilder(builder: (context, state) { - if (state is RequestsPageSuccess) { - state.relations.sort((rel1, rel2) => rel2.sentOn.compareTo(rel1.sentOn)); + body: BlocConsumer( + listener: (context, state) { + if (state is RequestsPageShowed) { + _refreshCompleter?.complete(); + _refreshCompleter = Completer(); + } + }, + builder: (context, state) { + return BlocBuilder( + builder: (context, state) { + if (state is RequestsPageSuccess) { + state.relations.sort((rel1, rel2) => rel2.sentOn.compareTo(rel1.sentOn)); - List pendingRelations = - state.relations.where((rel) => rel.state == 1).toList(); - List pastRelations = state.relations.where((rel) => rel.state != 1).toList(); - List allRelations = state.relations; + List pendingRelations = + state.relations.where((rel) => rel.state == 1).toList(); + List pastRelations = + state.relations.where((rel) => rel.state != 1).toList(); + List allRelations = state.relations; - return TabBarView( - children: [ - pendingRelations.length == 0 - ? NoRequestsInfo(message: "You don't have any pending mentorship requests.") - : _buildRequestsTab(context, pendingRelations), - pastRelations.length == 0 - ? NoRequestsInfo( - message: "You don't have any past mentorship requests.", - ) - : _buildRequestsTab(context, pastRelations), - allRelations.length == 0 - ? NoRequestsInfo(message: "You don't have any mentorship requests.") - : _buildRequestsTab(context, allRelations), - ], - ); - } + return TabBarView( + children: [ + pendingRelations.length == 0 + ? NoRequestsInfo( + message: "You don't have any pending mentorship requests.") + : _buildRequestsTab(context, pendingRelations), + pastRelations.length == 0 + ? NoRequestsInfo( + message: "You don't have any past mentorship requests.", + ) + : _buildRequestsTab(context, pastRelations), + allRelations.length == 0 + ? NoRequestsInfo(message: "You don't have any mentorship requests.") + : _buildRequestsTab(context, allRelations), + ], + ); + } - if (state is RequestsPageFailure) { - return Center( - child: Text(state.message), - ); - } + if (state is RequestsPageFailure) { + return Center( + child: Text(state.message), + ); + } - return LoadingIndicator(); - }), + return LoadingIndicator(); + }, + ); + }, + ), ), ); } Widget _buildRequestsTab(BuildContext context, List relations) { - return ListView.builder( - itemCount: relations.length, - itemBuilder: (context, index) { - Relation relation = relations[index]; + return RefreshIndicator( + onRefresh: () { + BlocProvider.of(context).add( + RequestsPageRefresh(), + ); + return _refreshCompleter.future; + }, + child: ListView.builder( + itemCount: relations.length, + itemBuilder: (context, index) { + Relation relation = relations[index]; - DateTime startDate = DateTimeX.fromTimestamp(relation.sentOn); - DateTime endDate = DateTimeX.fromTimestamp(relation.endsOn); + DateTime startDate = DateTimeX.fromTimestamp(relation.sentOn); + DateTime endDate = DateTimeX.fromTimestamp(relation.endsOn); - return InkWell( - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => RequestDetailScreen(relation: relation), + return InkWell( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => RequestDetailScreen(relation: relation), + ), ), - ), - child: ListTile( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: ListTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BoldText("Mentor: ", relation.mentor.name), + BoldText("Mentee: ", relation.mentee.name), + BoldText("End date: ", endDate.toDateString()), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - BoldText("Mentor: ", relation.mentor.name), - BoldText("Mentee: ", relation.mentee.name), - BoldText("End date: ", endDate.toDateString()), + Text("Sent on ${startDate.toDateString()}"), + SizedBox(height: 4), + if (relation.sentByMe) Text("Sent by me") else Text("To me") ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text("Sent on ${startDate.toDateString()}"), - SizedBox(height: 4), - if (relation.sentByMe) Text("Sent by me") else Text("To me") - ], - ) - ], - ), - BoldText("Notes: ", relation.notes), - Divider(), - ], + ) + ], + ), + BoldText("Notes: ", relation.notes), + Divider(), + ], + ), ), - ), - ); - }, + ); + }, + ), ); } } diff --git a/lib/screens/home/pages/stats/bloc/stats_page_bloc.dart b/lib/screens/home/pages/stats/bloc/stats_page_bloc.dart index 390735a..94c1a5f 100644 --- a/lib/screens/home/pages/stats/bloc/stats_page_bloc.dart +++ b/lib/screens/home/pages/stats/bloc/stats_page_bloc.dart @@ -18,15 +18,36 @@ class StatsPageBloc extends Bloc { @override Stream mapEventToState(StatsPageEvent event) async* { + if (event is StatsPageShowed) { + yield* _mapStatsRequested(event); + } else if (event is StatsPageRefresh) { + yield* _mapStatsRefreshRequested(event); + } + } + + Stream _mapStatsRequested(StatsPageEvent event) async* { if (event is StatsPageShowed) { yield StatsPageLoading(); try { final HomeStats homeStats = await userRepository.getHomeStats(); yield StatsPageSuccess(homeStats); } on Failure catch (failure) { - Logger.root.severe(failure.message); + Logger.root.severe("StatsPageBloc: Failure catched: $failure.message"); yield StatsPageFailure(failure.message); } } } + + Stream _mapStatsRefreshRequested(StatsPageEvent event) async* { + if (event is StatsPageRefresh) { + yield StatsPageLoading(); + try { + final HomeStats homeStats = await userRepository.getHomeStats(); + yield StatsPageSuccess(homeStats); + } on Failure catch (failure) { + Logger.root.severe("StatsPageBloc: Failure catched: $failure.message"); + yield state; + } + } + } } diff --git a/lib/screens/home/pages/stats/bloc/stats_page_event.dart b/lib/screens/home/pages/stats/bloc/stats_page_event.dart index 6a86fe1..85af297 100644 --- a/lib/screens/home/pages/stats/bloc/stats_page_event.dart +++ b/lib/screens/home/pages/stats/bloc/stats_page_event.dart @@ -8,3 +8,5 @@ abstract class StatsPageEvent extends Equatable { } class StatsPageShowed extends StatsPageEvent {} + +class StatsPageRefresh extends StatsPageEvent {} diff --git a/lib/screens/home/pages/stats/stats_page.dart b/lib/screens/home/pages/stats/stats_page.dart index 763fc47..a978bcb 100644 --- a/lib/screens/home/pages/stats/stats_page.dart +++ b/lib/screens/home/pages/stats/stats_page.dart @@ -4,80 +4,105 @@ import 'package:mentorship_client/remote/models/task.dart'; import 'package:mentorship_client/remote/repositories/user_repository.dart'; import 'package:mentorship_client/screens/home/pages/stats/bloc/bloc.dart'; import 'package:mentorship_client/widgets/loading_indicator.dart'; +import 'dart:async'; /// First page from the left on the HomeScreen. Displays welcome message to the user /// and provides some information on latest achievements. +/// + class StatsPage extends StatefulWidget { @override _StatsPageState createState() => _StatsPageState(); } class _StatsPageState extends State { + Completer _refreshCompleter; + + @override + void initState() { + super.initState(); + _refreshCompleter = Completer(); + } + @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - StatsPageBloc(userRepository: UserRepository.instance)..add(StatsPageShowed()), - child: BlocBuilder(builder: (context, state) { - if (state is StatsPageSuccess) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 24), - child: Text( - "Welcome, ${state.homeStats.name}!", - textScaleFactor: 2, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - Column( + return BlocConsumer( + listener: (context, state) { + if (state is StatsPageShowed) { + _refreshCompleter?.complete(); + _refreshCompleter = Completer(); + } + }, + builder: (context, state) { + return BlocBuilder(builder: (context, state) { + if (state is StatsPageSuccess) { + return RefreshIndicator( + onRefresh: () { + BlocProvider.of(context).add( + StatsPageRefresh(), + ); + return _refreshCompleter.future; + }, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: ListView( children: [ - _buildRow("Pending Requests", state.homeStats.pendingRequests), - _buildRow("Accepted Requests", state.homeStats.acceptedRequests), - _buildRow("Rejected Requests", state.homeStats.rejectedRequests), - _buildRow("Completed Relations", state.homeStats.completedRelations), Padding( - padding: const EdgeInsets.fromLTRB(0, 24, 0, 12), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - "Recent Achievements", - style: Theme.of(context).textTheme.headline6, - )), + padding: const EdgeInsets.symmetric(vertical: 24), + child: Text( + "Welcome, ${state.homeStats.name}!", + textScaleFactor: 2, + style: TextStyle(fontWeight: FontWeight.bold), + ), ), - for (Task achievement in state.homeStats.achievements) - Column( - children: [ - Row( - children: [ - Checkbox( - value: true, - onChanged: (status) => null, - hoverColor: Theme.of(context).accentColor, + Column( + children: [ + _buildRow("Pending Requests", state.homeStats.pendingRequests), + _buildRow("Accepted Requests", state.homeStats.acceptedRequests), + _buildRow("Rejected Requests", state.homeStats.rejectedRequests), + _buildRow("Completed Relations", state.homeStats.completedRelations), + Padding( + padding: const EdgeInsets.fromLTRB(0, 24, 0, 12), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + "Recent Achievements", + style: Theme.of(context).textTheme.headline6, + )), + ), + for (Task achievement in state.homeStats.achievements) + Column( + children: [ + Row( + children: [ + Checkbox( + value: true, + onChanged: (status) => null, + hoverColor: Theme.of(context).accentColor, + ), + Text(achievement.description), + ], ), - Text(achievement.description), + Divider(), ], ), - Divider(), - ], - ), + ], + ) ], - ) - ], - ), - ); - } - if (state is StatsPageFailure) { - return Text(state.message); - } + ), + ), + ); + } + if (state is StatsPageFailure) { + return Text(state.message); + } - if (state is StatsPageLoading) { - return LoadingIndicator(); - } else - return Text("an error occurred"); - }), + if (state is StatsPageLoading) { + return LoadingIndicator(); + } else + return Text("an error occurred"); + }); + }, ); } diff --git a/lib/screens/member_profile/member_profile.dart b/lib/screens/member_profile/member_profile.dart index 4cf30a2..ed8c892 100644 --- a/lib/screens/member_profile/member_profile.dart +++ b/lib/screens/member_profile/member_profile.dart @@ -3,6 +3,7 @@ import 'package:mentorship_client/remote/models/user.dart'; import 'package:mentorship_client/remote/repositories/user_repository.dart'; import 'package:mentorship_client/screens/member_profile/user_data_list.dart'; import 'package:mentorship_client/screens/send_request/send_request_screen.dart'; +import 'package:mentorship_client/widgets/loading_indicator.dart'; class MemberProfileScreen extends StatelessWidget { final User user; @@ -53,8 +54,9 @@ class MemberProfileScreen extends StatelessWidget { style: TextStyle(color: Colors.white), ), onPressed: () async { + showProgressIndicator(context); var currentUser = await UserRepository.instance.getCurrentUser(); - + Navigator.of(context).pop(); Navigator.of(context).push( PageRouteBuilder( pageBuilder: (c, anim1, anim2) => SendRequestScreen( diff --git a/lib/screens/register/register_screen.dart b/lib/screens/register/register_screen.dart index 00f430d..aecec9a 100644 --- a/lib/screens/register/register_screen.dart +++ b/lib/screens/register/register_screen.dart @@ -1,9 +1,12 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:mentorship_client/extensions/context.dart'; import 'package:mentorship_client/failure.dart'; import 'package:mentorship_client/remote/repositories/auth_repository.dart'; import 'package:mentorship_client/remote/requests/register.dart'; +import 'package:mentorship_client/widgets/loading_indicator.dart'; +import 'package:url_launcher/url_launcher.dart'; /// This screen will let the user to sign up into the system using name, username, /// email and password. @@ -39,7 +42,6 @@ class RegisterForm extends StatefulWidget { } class _RegisterFormState extends State { - final _formKey = GlobalKey(); final _nameController = TextEditingController(); @@ -53,6 +55,9 @@ class _RegisterFormState extends State { bool _needsMentoring = false; bool _acceptedTermsAndConditions = false; int _radiovalue; + bool registering = false; + bool signupButtonEnabled = true; + void _togglePasswordVisibility() { setState(() { _passwordVisible = !_passwordVisible; @@ -107,7 +112,6 @@ class _RegisterFormState extends State { @override Widget build(BuildContext context) { const spacing = 12.0; - return Form( key: _formKey, child: Column( @@ -212,18 +216,29 @@ class _RegisterFormState extends State { onChanged: _toggleTermsAndConditions, ), Flexible( - child: Text("I affirm that I have read and accept to be bound by the " - "AnitaB.org Code of Conduct, Terms and Privacy Policy. Further, " - "I consent to use of my information for the stated purpose."), + child: ConditionsText(), ), ], ), Padding( padding: EdgeInsets.all(16), - child: RaisedButton( - color: Theme.of(context).accentColor, - child: Text("Sign up"), - onPressed: () => _register(context), + child: Column( + children: [ + registering ? LoadingIndicator() : Container(), + RaisedButton( + color: Theme.of(context).accentColor, + child: Text("Sign up"), + onPressed: _acceptedTermsAndConditions && signupButtonEnabled + ? () { + setState(() { + registering = true; + signupButtonEnabled = false; + }); + _register(context); + } + : null, + ), + ], ), ), Padding( @@ -262,5 +277,79 @@ class _RegisterFormState extends State { message = exception.toString(); } context.showSnackBar(message); + setState(() { + registering = false; + signupButtonEnabled = true; + }); + } +} + +class ConditionsText extends StatelessWidget { + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + children: [ + TextSpan( + style: TextStyle( + color: Colors.black, + ), + text: "By checking this box, I affirm that I have read and accept to be bound by the " + "AnitaB.org ", + ), + TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () { + launch("https://ghc.anitab.org/code-of-conduct/"); + }, + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + text: "Code of Conduct", + ), + TextSpan( + style: TextStyle( + color: Colors.black, + ), + text: ", ", + ), + TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () { + launch("https://anitab.org/terms-of-use/"); + }, + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + text: "Terms", + ), + TextSpan( + style: TextStyle( + color: Colors.black, + ), + text: ", and ", + ), + TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () { + launch("https://anitab.org/privacy-policy/"); + }, + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + text: "Privacy Policy", + ), + TextSpan( + style: TextStyle( + color: Colors.black, + ), + text: ". Further, I consent to the use of my information for the stated purpose.", + ), + ], + ), + ); } } diff --git a/lib/screens/send_request/bloc/send_request_bloc.dart b/lib/screens/send_request/bloc/send_request_bloc.dart index a4acd2f..0001732 100644 --- a/lib/screens/send_request/bloc/send_request_bloc.dart +++ b/lib/screens/send_request/bloc/send_request_bloc.dart @@ -28,5 +28,8 @@ class SendRequestBloc extends Bloc { yield InitialSendRequestState(message: failure.message); } } + if (event is ResetSnackbarMessage) { + yield InitialSendRequestState(message: null); + } } } diff --git a/lib/screens/send_request/bloc/send_request_event.dart b/lib/screens/send_request/bloc/send_request_event.dart index 0e50904..1abe035 100644 --- a/lib/screens/send_request/bloc/send_request_event.dart +++ b/lib/screens/send_request/bloc/send_request_event.dart @@ -13,3 +13,8 @@ class RelationRequestSent extends SendRequestEvent { @override List get props => [request]; } + +class ResetSnackbarMessage extends SendRequestEvent { + @override + List get props => null; +} diff --git a/lib/screens/send_request/send_request_screen.dart b/lib/screens/send_request/send_request_screen.dart index 8e1b095..a804f44 100644 --- a/lib/screens/send_request/send_request_screen.dart +++ b/lib/screens/send_request/send_request_screen.dart @@ -6,6 +6,7 @@ import 'package:mentorship_client/remote/models/user.dart'; import 'package:mentorship_client/remote/repositories/relation_repository.dart'; import 'package:mentorship_client/remote/requests/relation_requests.dart'; import 'package:mentorship_client/screens/send_request/bloc/bloc.dart'; +import 'package:mentorship_client/widgets/loading_indicator.dart'; import 'package:toast/toast.dart'; class SendRequestScreen extends StatefulWidget { @@ -28,6 +29,8 @@ class _SendRequestScreenState extends State { @override Widget build(BuildContext context) { + //ignore: close_sinks + return BlocProvider( create: (context) => SendRequestBloc(relationRepository: RelationRepository.instance), child: Scaffold( @@ -38,6 +41,8 @@ class _SendRequestScreenState extends State { listener: (context, state) { if (state.message != null) { context.showSnackBar(state.message); + Navigator.of(context).pop(); + BlocProvider.of(context).add(ResetSnackbarMessage()); } }, child: Builder( @@ -125,7 +130,7 @@ class _SendRequestScreenState extends State { lastDate: DateTime(initialDate.year, initialDate.month, initialDate.day + 168), ); - if(newlySelectedDate != null){ + if (newlySelectedDate != null) { setState(() { _endDate = newlySelectedDate; }); @@ -152,6 +157,7 @@ class _SendRequestScreenState extends State { ), color: Theme.of(context).accentColor, onPressed: () async { + showProgressIndicator(context); int mentorId = widget.otherUser.id; int menteeId = widget.currentUser.id; if (_role == Role.mentor) { diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 22214b8..395130d 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -8,6 +8,7 @@ import 'package:mentorship_client/remote/repositories/user_repository.dart'; import 'package:mentorship_client/remote/requests/change_password.dart'; import 'package:mentorship_client/remote/responses/custom_response.dart'; import 'package:mentorship_client/screens/settings/about.dart'; +import 'package:mentorship_client/widgets/loading_indicator.dart'; class SettingsScreen extends StatelessWidget { @override @@ -52,17 +53,18 @@ class SettingsScreen extends StatelessWidget { ); } - void _showConfirmLogoutDialog(BuildContext context){ + void _showConfirmLogoutDialog(BuildContext context) { showDialog( context: context, - builder:(context) { + builder: (context) { return AlertDialog( - title : Text('Log Out'), + title: Text('Log Out'), content: Text('Are you sure you want to logout?'), actions: [ FlatButton( child: Text('Cancel'), - onPressed: ()=> Navigator.of(context).pop(),), + onPressed: () => Navigator.of(context).pop(), + ), FlatButton( child: Text('Confirm'), onPressed: () { @@ -74,16 +76,15 @@ class SettingsScreen extends StatelessWidget { ), ], ); - } + }, ); } - Future _showChangePasswordDialog(BuildContext context) async { + Future _showChangePasswordDialog(BuildContext topContext) async { final _currentPassController = TextEditingController(); final _newPassController = TextEditingController(); - showDialog( - context: context, + context: topContext, builder: (context) => AlertDialog( title: Text("Change password"), content: Column( @@ -100,6 +101,10 @@ class SettingsScreen extends StatelessWidget { ], ), actions: [ + FlatButton( + child: Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), FlatButton( child: Text("Submit"), onPressed: () async { @@ -107,20 +112,20 @@ class SettingsScreen extends StatelessWidget { currentPassword: _currentPassController.text, newPassword: _newPassController.text, ); + Navigator.of(context).pop(); + showProgressIndicator(context); try { CustomResponse response = await UserRepository.instance.changePassword(changePassword); - context.showSnackBar(response.message); + topContext.showSnackBar(response.message); } on Failure catch (failure) { - context.showSnackBar(failure.message); + topContext.showSnackBar(failure.message); } - - Navigator.of(context).pop(); + Navigator.of(topContext).pop(); }, ), ], ), - ); } } diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index 1013a5b..64b0e58 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -9,3 +9,11 @@ class LoadingIndicator extends StatelessWidget { ); } } + +Future showProgressIndicator(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + child: LoadingIndicator(), + ); +} diff --git a/pubspec.lock b/pubspec.lock index a8b07b3..4604539 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "2.2.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.39.8" + version: "0.39.7" archive: dependency: transitive description: @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.4.1" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" bloc: dependency: "direct main" description: @@ -84,7 +91,7 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.3.7" + version: "1.3.5" build_runner: dependency: "direct dev" description: @@ -112,7 +119,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "7.1.0" + version: "7.0.9" charcode: dependency: transitive description: @@ -203,7 +210,7 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.6" + version: "1.3.4" equatable: dependency: "direct main" description: @@ -217,7 +224,7 @@ packages: name: expandable url: "https://pub.dartlang.org" source: hosted - version: "4.1.3" + version: "4.1.4" fake_async: dependency: transitive description: @@ -295,7 +302,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.1" + version: "0.12.0+4" http_multi_server: dependency: transitive description: @@ -393,7 +400,7 @@ packages: name: node_io url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.0.1+2" package_config: dependency: transitive description: @@ -429,6 +436,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.2" + platform_detect: + dependency: transitive + description: + name: platform_detect + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" plugin_platform_interface: dependency: transitive description: @@ -449,7 +463,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.0.5" pub_semver: dependency: transitive description: @@ -545,7 +559,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.16" timing: dependency: transitive description: @@ -573,28 +587,28 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.4.5" + version: "5.4.10" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+5" + version: "0.0.1+7" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.6" + version: "1.0.7" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+4" + version: "0.1.1+6" vector_math: dependency: transitive description: @@ -608,7 +622,7 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+15" + version: "0.9.7+14" web_socket_channel: dependency: transitive description: @@ -629,7 +643,7 @@ packages: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.0" sdks: dart: ">=2.7.0 <3.0.0" - flutter: ">=1.17.0 <2.0.0" + flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 245c559..cb29977 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: package_info: ^0.4.1 url_launcher: ^5.4.10 expandable: ^4.1.4 - + auto_size_text: ^2.1.0 # Bloc bloc: ^4.0.0 flutter_bloc: ^4.0.1