From 9a5a24bfc32807d02a4fcd196302020616903393 Mon Sep 17 00:00:00 2001 From: Benjamin Canape Date: Mon, 5 Feb 2024 00:06:46 +0100 Subject: [PATCH] improve profile pictures download --- .../view_model/activity_item_view_model.dart | 10 ++-- .../activity/widgets/activity_comments.dart | 59 +++++++++++-------- .../activity_item_user_informations.dart | 15 +++-- .../common/core/utils/user_utils.dart | 1 + .../core/widgets/infinite_scroll_list.dart | 13 ++-- .../infinite_scroll_list_view_model.dart | 15 ++--- .../state/infinite_scroll_list_state.dart | 16 ++--- .../common/user/screens/profile_screen.dart | 8 ++- .../profile_picture_view_model.dart | 32 ++++++++++ .../user/view_model/profile_view_model.dart | 9 ++- .../state/profile_picture_state.dart | 22 +++++++ .../view_model/community_view_model.dart | 4 +- .../settings/screens/edit_profile_screen.dart | 22 +++++-- .../view_model/edit_profile_view_model.dart | 20 +++++-- 14 files changed, 162 insertions(+), 84 deletions(-) create mode 100644 lib/presentation/common/user/view_model/profile_picture_view_model.dart create mode 100644 lib/presentation/common/user/view_model/state/profile_picture_state.dart diff --git a/lib/presentation/common/activity/view_model/activity_item_view_model.dart b/lib/presentation/common/activity/view_model/activity_item_view_model.dart index d348e450..0d9eae96 100644 --- a/lib/presentation/common/activity/view_model/activity_item_view_model.dart +++ b/lib/presentation/common/activity/view_model/activity_item_view_model.dart @@ -1,10 +1,8 @@ -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../user/view_model/profile_picture_view_model.dart'; import '../../../../data/repositories/activity_repository_impl.dart'; import '../../../../domain/entities/activity.dart'; -import '../../../../data/repositories/user_repository_impl.dart'; import '../../../../main.dart'; import '../../../my_activities/screens/activity_details_screen.dart'; import 'state/activity_item_state.dart'; @@ -30,8 +28,10 @@ class ActivityItemViewModel extends StateNotifier { } /// Get the profile picture of the user - Future getProfilePicture(String userId) async { - return ref.read(userRepositoryProvider).downloadProfilePicture(userId); + void getProfilePicture(String userId) async { + ref + .read(profilePictureViewModelProvider(userId).notifier) + .getProfilePicture(userId); } /// Retrieves the details of an activity. diff --git a/lib/presentation/common/activity/widgets/activity_comments.dart b/lib/presentation/common/activity/widgets/activity_comments.dart index 137c0eb4..3d8c9a64 100644 --- a/lib/presentation/common/activity/widgets/activity_comments.dart +++ b/lib/presentation/common/activity/widgets/activity_comments.dart @@ -4,6 +4,7 @@ import 'package:comment_box/comment/comment.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../user/view_model/profile_picture_view_model.dart'; import '../../../../core/utils/storage_utils.dart'; import '../../../../domain/entities/activity.dart'; @@ -19,18 +20,19 @@ class ActivityComments extends HookConsumerWidget { final GlobalKey formKey; final currentUserPictureDataProvider = - FutureProvider.family((ref, activity) async { + FutureProvider.family((ref, activity) async { final user = await StorageUtils.getUser(); final provider = ref.read(activityItemViewModelProvider(activity.id).notifier); - return user != null ? provider.getProfilePicture(user.id) : null; + user != null ? provider.getProfilePicture(user.id) : null; + return user?.id; }); final commentUserPictureDataProvider = - FutureProvider.family((ref, user) async { + FutureProvider.family((ref, user) async { final provider = ref.read(activityItemViewModelProvider(user.id).notifier); - return provider.getProfilePicture(user.id); + provider.getProfilePicture(user.id); }); ActivityComments({ @@ -39,7 +41,10 @@ class ActivityComments extends HookConsumerWidget { required this.formKey, }); - Widget buildCommentList(WidgetRef ref, List comments) { + Widget buildCommentList( + WidgetRef ref, + List comments, + ) { return Expanded( child: ListView.builder( itemCount: comments.length, @@ -49,10 +54,13 @@ class ActivityComments extends HookConsumerWidget { } Widget buildCommentItem(WidgetRef ref, ActivityComment comment) { + final profilePicture = ref + .watch(profilePictureViewModelProvider(comment.user.id)) + .profilePicture; return Padding( padding: const EdgeInsets.fromLTRB(2.0, 8.0, 2.0, 0.0), child: ListTile( - leading: buildUserAvatar(ref, comment.user), + leading: buildUserAvatar(ref, comment.user, profilePicture), title: GestureDetector( child: Text( UserUtils.getNameOrUsername(comment.user), @@ -82,12 +90,11 @@ class ActivityComments extends HookConsumerWidget { } Widget buildCommentChild( - WidgetRef ref, - AppLocalizations appLocalizations, - ActivityItemCommentsViewModel provider, - List comments, - bool displayPreviousComments, - ) { + WidgetRef ref, + AppLocalizations appLocalizations, + ActivityItemCommentsViewModel provider, + List comments, + bool displayPreviousComments) { final lastComment = comments.isNotEmpty ? comments.last : null; return Column( @@ -101,7 +108,7 @@ class ActivityComments extends HookConsumerWidget { ); } - Widget buildUserAvatar(WidgetRef ref, User user) { + Widget buildUserAvatar(WidgetRef ref, User user, Uint8List? profilePicture) { return GestureDetector( child: Container( height: 50.0, @@ -111,8 +118,9 @@ class ActivityComments extends HookConsumerWidget { borderRadius: const BorderRadius.all(Radius.circular(50)), ), child: ref.watch(commentUserPictureDataProvider(user)).when( - data: (pic) => pic != null - ? CircleAvatar(radius: 50, backgroundImage: MemoryImage(pic)) + data: (_) => profilePicture != null + ? CircleAvatar( + radius: 50, backgroundImage: MemoryImage(profilePicture)) : UserUtils.personIcon, loading: () => UserUtils.personIcon, error: (_, __) => UserUtils.personIcon, @@ -135,7 +143,17 @@ class ActivityComments extends HookConsumerWidget { height: state.comments.isNotEmpty ? 210 : 80, child: CommentBox( userImage: currentUserPictureProvider.when( - data: (pic) => pic != null ? MemoryImage(pic) : null, + data: (userId) { + if (userId != null) { + final profilePicture = ref + .watch(profilePictureViewModelProvider(userId)) + .profilePicture; + return profilePicture != null + ? MemoryImage(profilePicture) + : null; + } + return null; + }, loading: () => null, error: (_, __) => null, ), @@ -145,13 +163,8 @@ class ActivityComments extends HookConsumerWidget { backgroundColor: ColorUtils.white, textColor: ColorUtils.mainMedium, sendWidget: Icon(Icons.send_sharp, size: 30, color: ColorUtils.main), - child: buildCommentChild( - ref, - appLocalizations, - commentsProvider, - state.comments, - state.displayPreviousComments, - ), + child: buildCommentChild(ref, appLocalizations, commentsProvider, + state.comments, state.displayPreviousComments), ), ); } diff --git a/lib/presentation/common/activity/widgets/activity_item_user_informations.dart b/lib/presentation/common/activity/widgets/activity_item_user_informations.dart index 5ebe10b5..bfe01550 100644 --- a/lib/presentation/common/activity/widgets/activity_item_user_informations.dart +++ b/lib/presentation/common/activity/widgets/activity_item_user_informations.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../user/view_model/profile_picture_view_model.dart'; import '../../../../domain/entities/activity.dart'; import '../../core/utils/color_utils.dart'; @@ -13,18 +14,19 @@ class ActivityItemUserInformation extends HookConsumerWidget { final Activity activity; final futureDataProvider = - FutureProvider.family((ref, activity) async { + FutureProvider.family((ref, activity) async { final provider = ref.read(activityItemViewModelProvider(activity.id).notifier); String userId = activity.user.id; - return provider.getProfilePicture(userId); + provider.getProfilePicture(userId); }); ActivityItemUserInformation({super.key, required this.activity}); - Widget buildProfilePicture(AsyncValue futureProvider) { + Widget buildProfilePicture( + AsyncValue futureProvider, Uint8List? profilePicture) { return futureProvider.when( - data: (profilePicture) { + data: (_) { return ClipRRect( borderRadius: BorderRadius.circular(50), child: Container( @@ -45,6 +47,9 @@ class ActivityItemUserInformation extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final futureProvider = ref.watch(futureDataProvider(activity)); + final profilePicture = ref + .watch(profilePictureViewModelProvider(activity.user.id)) + .profilePicture; return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Container( @@ -60,7 +65,7 @@ class ActivityItemUserInformation extends HookConsumerWidget { onPressed: () => UserUtils.goToProfile(activity.user), child: Row( children: [ - buildProfilePicture(futureProvider), + buildProfilePicture(futureProvider, profilePicture), const SizedBox(width: 20), Flexible( child: Text( diff --git a/lib/presentation/common/core/utils/user_utils.dart b/lib/presentation/common/core/utils/user_utils.dart index 391ecb2c..8b967d33 100644 --- a/lib/presentation/common/core/utils/user_utils.dart +++ b/lib/presentation/common/core/utils/user_utils.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import '../../../../domain/entities/user.dart'; diff --git a/lib/presentation/common/core/widgets/infinite_scroll_list.dart b/lib/presentation/common/core/widgets/infinite_scroll_list.dart index 1e143e93..228bd7c8 100644 --- a/lib/presentation/common/core/widgets/infinite_scroll_list.dart +++ b/lib/presentation/common/core/widgets/infinite_scroll_list.dart @@ -35,9 +35,9 @@ class InfiniteScrollList extends HookConsumerWidget { Future loadMoreData(InfiniteScrollListState state, InfiniteScrollListViewModel provider) async { - double newPos = scrollController.position.pixels; var editData = false; if (!state.isLoading && hasMoreData(state.data, total)) { + double position = scrollController.position.pixels; provider.setIsLoading(true); editData = true; @@ -45,19 +45,16 @@ class InfiniteScrollList extends HookConsumerWidget { final newData = await loadData(state.pageNumber); if (state.data is List>) { - provider.setData( - newData.list, - newPos, - ); + provider.setData(newData.list); } else { - provider.addData(newData.list, newPos); + provider.addData(newData.list); } } finally { provider.setIsLoading(false); if (editData) { WidgetsBinding.instance.addPostFrameCallback((_) { - scrollController.jumpTo(newPos); + scrollController.jumpTo(position); }); } } @@ -102,7 +99,7 @@ class InfiniteScrollList extends HookConsumerWidget { })); Future.delayed(const Duration(milliseconds: 10), () { - state.data.isNotEmpty ? '' : provider.setData(initialData, 0); + state.data.isNotEmpty ? '' : provider.setData(initialData); }); return state.isLoading diff --git a/lib/presentation/common/core/widgets/view_model/infinite_scroll_list_view_model.dart b/lib/presentation/common/core/widgets/view_model/infinite_scroll_list_view_model.dart index c7810ee9..e060cc89 100644 --- a/lib/presentation/common/core/widgets/view_model/infinite_scroll_list_view_model.dart +++ b/lib/presentation/common/core/widgets/view_model/infinite_scroll_list_view_model.dart @@ -25,9 +25,8 @@ class InfiniteScrollListViewModel } /// Set data in the state - void setData(List data, double pos) { - state = state.copyWith( - data: data, pageNumber: state.pageNumber + 1, position: pos); + void setData(List data) { + state = state.copyWith(data: data, pageNumber: state.pageNumber + 1); } /// Replace data in the state @@ -36,11 +35,10 @@ class InfiniteScrollListViewModel } /// Add data in the state - void addData(List data, double pos) { + void addData(List data) { var currentData = state.data; currentData.addAll(data); - state = state.copyWith( - data: currentData, pageNumber: state.pageNumber + 1, position: pos); + state = state.copyWith(data: currentData, pageNumber: state.pageNumber + 1); } /// Set pageNumber in the state @@ -48,11 +46,6 @@ class InfiniteScrollListViewModel state = state.copyWith(pageNumber: pageNumber); } - /// Set position in the state - void setPosition(double position) { - state = state.copyWith(position: position); - } - /// reset state void reset() { state = InfiniteScrollListState.initial(); diff --git a/lib/presentation/common/core/widgets/view_model/state/infinite_scroll_list_state.dart b/lib/presentation/common/core/widgets/view_model/state/infinite_scroll_list_state.dart index 7c5dab25..61dfda98 100644 --- a/lib/presentation/common/core/widgets/view_model/state/infinite_scroll_list_state.dart +++ b/lib/presentation/common/core/widgets/view_model/state/infinite_scroll_list_state.dart @@ -3,29 +3,21 @@ class InfiniteScrollListState { final List data; final bool isLoading; final int pageNumber; - final double position; const InfiniteScrollListState( - {required this.data, - required this.isLoading, - required this.pageNumber, - required this.position}); + {required this.data, required this.isLoading, required this.pageNumber}); /// Factory method to create the initial state. factory InfiniteScrollListState.initial() { return const InfiniteScrollListState( - data: [], isLoading: false, pageNumber: 0, position: 0.0); + data: [], isLoading: false, pageNumber: 0); } InfiniteScrollListState copyWith( - {List? data, - bool? isLoading, - int? pageNumber, - double? position}) { + {List? data, bool? isLoading, int? pageNumber}) { return InfiniteScrollListState( data: data ?? this.data, isLoading: isLoading ?? this.isLoading, - pageNumber: pageNumber ?? this.pageNumber, - position: position ?? this.position); + pageNumber: pageNumber ?? this.pageNumber); } } diff --git a/lib/presentation/common/user/screens/profile_screen.dart b/lib/presentation/common/user/screens/profile_screen.dart index 364caa52..587c468b 100644 --- a/lib/presentation/common/user/screens/profile_screen.dart +++ b/lib/presentation/common/user/screens/profile_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../view_model/profile_picture_view_model.dart'; import '../../../../domain/entities/activity.dart'; import '../../../../domain/entities/enum/friend_request_status.dart'; @@ -40,6 +41,9 @@ class ProfileScreen extends HookConsumerWidget { var provider = ref.watch(profileViewModelProvider(user.id).notifier); final futureProvider = ref.watch(futureDataProvider(user)); + var profilePicture = + ref.watch(profilePictureViewModelProvider(user.id)).profilePicture; + var activitiesStateProvider = ref.watch(activitiesDataFutureProvider(user)); return state.isLoading @@ -60,9 +64,9 @@ class ProfileScreen extends HookConsumerWidget { alignment: Alignment.center, width: 150, height: 150, - child: state.profilePicture != null + child: profilePicture != null ? Image.memory( - state.profilePicture!, + profilePicture, fit: BoxFit.cover, ) : const Icon(Icons.person, size: 100), diff --git a/lib/presentation/common/user/view_model/profile_picture_view_model.dart b/lib/presentation/common/user/view_model/profile_picture_view_model.dart new file mode 100644 index 00000000..b900eeba --- /dev/null +++ b/lib/presentation/common/user/view_model/profile_picture_view_model.dart @@ -0,0 +1,32 @@ +import 'dart:typed_data'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../../data/repositories/user_repository_impl.dart'; +import 'state/profile_picture_state.dart'; + +/// Provider for the profile picture view model. +final profilePictureViewModelProvider = StateNotifierProvider.family< + ProfilePictureViewModel, + ProfilePictureState, + String>((ref, userId) => ProfilePictureViewModel(ref, userId)); + +class ProfilePictureViewModel extends StateNotifier { + late final Ref ref; + final String userId; + + ProfilePictureViewModel(this.ref, this.userId) + : super(ProfilePictureState.initial()); + + Future getProfilePicture(String userId) async { + if (state.loaded == false) { + ref.read(userRepositoryProvider).downloadProfilePicture(userId).then( + (value) => + state = state.copyWith(profilePicture: value, loaded: true)); + } + } + + void editProfilePicture(Uint8List? image) { + state = state.copyWith(profilePicture: image); + } +} diff --git a/lib/presentation/common/user/view_model/profile_view_model.dart b/lib/presentation/common/user/view_model/profile_view_model.dart index fa1ced63..cbb6c74f 100644 --- a/lib/presentation/common/user/view_model/profile_view_model.dart +++ b/lib/presentation/common/user/view_model/profile_view_model.dart @@ -3,12 +3,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../../core/utils/storage_utils.dart'; import '../../../../data/repositories/activity_repository_impl.dart'; import '../../../../data/repositories/friend_request_repository_impl.dart'; -import '../../../../data/repositories/user_repository_impl.dart'; import '../../../../domain/entities/activity.dart'; import '../../../../domain/entities/enum/friend_request_status.dart'; import '../../../../domain/entities/page.dart'; import '../../core/enums/infinite_scroll_list.enum.dart'; import '../../core/widgets/view_model/infinite_scroll_list_view_model.dart'; +import 'profile_picture_view_model.dart'; import 'state/profile_state.dart'; /// Provider for the profile view model. @@ -65,11 +65,10 @@ class ProfileViewModel extends StateNotifier { state = state.copyWith(status: FriendRequestStatus.rejected); } - Future getProfilePicture(String userId) async { + void getProfilePicture(String userId) { ref - .read(userRepositoryProvider) - .downloadProfilePicture(userId) - .then((value) => state = state.copyWith(profilePicture: value)); + .read(profilePictureViewModelProvider(userId).notifier) + .getProfilePicture(userId); } void refreshList() { diff --git a/lib/presentation/common/user/view_model/state/profile_picture_state.dart b/lib/presentation/common/user/view_model/state/profile_picture_state.dart new file mode 100644 index 00000000..dbdf5f8d --- /dev/null +++ b/lib/presentation/common/user/view_model/state/profile_picture_state.dart @@ -0,0 +1,22 @@ +import 'dart:typed_data'; + +/// The state class for profile picture. +class ProfilePictureState { + final bool loaded; + final Uint8List? profilePicture; // the profile picture + + const ProfilePictureState( + {required this.loaded, required this.profilePicture}); + + /// Factory method to create the initial state. + factory ProfilePictureState.initial() { + return const ProfilePictureState(loaded: false, profilePicture: null); + } + + /// Method to create a copy of the state with updated values. + ProfilePictureState copyWith({bool? loaded, Uint8List? profilePicture}) { + return ProfilePictureState( + loaded: loaded ?? this.loaded, + profilePicture: profilePicture ?? this.profilePicture); + } +} diff --git a/lib/presentation/community/view_model/community_view_model.dart b/lib/presentation/community/view_model/community_view_model.dart index a1e2258e..2bd813a4 100644 --- a/lib/presentation/community/view_model/community_view_model.dart +++ b/lib/presentation/community/view_model/community_view_model.dart @@ -1,12 +1,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../common/core/enums/infinite_scroll_list.enum.dart'; -import '../../common/core/widgets/view_model/infinite_scroll_list_view_model.dart'; import '../../../data/repositories/activity_repository_impl.dart'; import '../../../data/repositories/user_repository_impl.dart'; import '../../../domain/entities/activity.dart'; import '../../../domain/entities/page.dart'; import '../../../domain/entities/user.dart'; +import '../../common/core/enums/infinite_scroll_list.enum.dart'; +import '../../common/core/widgets/view_model/infinite_scroll_list_view_model.dart'; import 'state/community_state.dart'; /// Provider for the community view model. diff --git a/lib/presentation/settings/screens/edit_profile_screen.dart b/lib/presentation/settings/screens/edit_profile_screen.dart index 5cc0a5f8..384132b7 100644 --- a/lib/presentation/settings/screens/edit_profile_screen.dart +++ b/lib/presentation/settings/screens/edit_profile_screen.dart @@ -1,7 +1,11 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../common/user/view_model/profile_picture_view_model.dart'; +import '../../../domain/entities/user.dart'; import '../../common/core/utils/color_utils.dart'; import '../../common/core/utils/form_utils.dart'; import '../../common/core/utils/ui_utils.dart'; @@ -14,11 +18,13 @@ class EditProfileScreen extends HookConsumerWidget { EditProfileScreen({super.key}); - final editProfileFutureProvider = FutureProvider((ref) async { + final editProfileFutureProvider = FutureProvider((ref) async { final editProfileProvider = ref.watch(editProfileViewModelProvider.notifier); - editProfileProvider.getCurrentUser(); - editProfileProvider.getProfilePicture(); + User? user = await editProfileProvider.getCurrentUser(); + editProfileProvider.getProfilePicture(ref); + + return user; }); @override @@ -29,7 +35,13 @@ class EditProfileScreen extends HookConsumerWidget { var editProfileStateProvider = ref.watch(editProfileFutureProvider); return editProfileStateProvider.when( - data: (_) { + data: (user) { + Uint8List? profilePicture; + user != null + ? profilePicture = ref + .watch(profilePictureViewModelProvider(user.id)) + .profilePicture + : profilePicture = null; return Scaffold( resizeToAvoidBottomInset: true, body: state.isEditing @@ -66,7 +78,7 @@ class EditProfileScreen extends HookConsumerWidget { : Container(), const SizedBox(height: 10), UploadFileWidget( - image: state.profilePicture, + image: profilePicture, callbackFunc: provider.chooseNewProfilePicture), // Firstname TextFormField diff --git a/lib/presentation/settings/view_model/edit_profile_view_model.dart b/lib/presentation/settings/view_model/edit_profile_view_model.dart index 4751e512..ec70eb72 100644 --- a/lib/presentation/settings/view_model/edit_profile_view_model.dart +++ b/lib/presentation/settings/view_model/edit_profile_view_model.dart @@ -9,6 +9,7 @@ import '../../../data/model/response/user_response.dart'; import '../../../data/repositories/user_repository_impl.dart'; import '../../../domain/entities/user.dart'; import '../../../main.dart'; +import '../../common/user/view_model/profile_picture_view_model.dart'; import 'state/edit_profile_state.dart'; final editProfileViewModelProvider = @@ -31,28 +32,35 @@ class EditProfileViewModel extends StateNotifier { state = state.copyWith(lastname: lastname); } - Future getCurrentUser() async { + Future getCurrentUser() async { User? user = await StorageUtils.getUser(); if (user != null) { state = state.copyWith(firstname: user.firstname, lastname: user.lastname); } + return user; } Future chooseNewProfilePicture(Uint8List image) async { ref .read(userRepositoryProvider) .uploadProfilePicture(image) - .then((_) => state = state.copyWith(profilePicture: image)); + .then((_) async { + User? currentUser = await StorageUtils.getUser(); + if (currentUser != null) { + ref + .watch(profilePictureViewModelProvider(currentUser.id).notifier) + .editProfilePicture(image); + } + }); } - Future getProfilePicture() async { + Future getProfilePicture(FutureProviderRef ref) async { User? currentUser = await StorageUtils.getUser(); if (currentUser != null) { ref - .read(userRepositoryProvider) - .downloadProfilePicture(currentUser.id) - .then((value) => {state = state.copyWith(profilePicture: value)}); + .read(profilePictureViewModelProvider(currentUser.id).notifier) + .getProfilePicture(currentUser.id); } }