From 2f3fe6a375a15882588b8af68ff5a08aa58af5a0 Mon Sep 17 00:00:00 2001 From: Beshoy Sorial Date: Mon, 15 Dec 2025 18:11:00 +0200 Subject: [PATCH 1/7] Rename .Dockerignore to .dockerignore --- .Dockerignore => .dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .Dockerignore => .dockerignore (95%) diff --git a/.Dockerignore b/.dockerignore similarity index 95% rename from .Dockerignore rename to .dockerignore index 8fb4718..08c683d 100644 --- a/.Dockerignore +++ b/.dockerignore @@ -17,4 +17,4 @@ ios/.symlinks # Git files .git .gitignore -README.md \ No newline at end of file +README.md From 178b666edee4e9c3f6dde4b0a66a8ec28f3d244d Mon Sep 17 00:00:00 2001 From: Hazem-Emam-404 Date: Mon, 15 Dec 2025 19:23:09 +0200 Subject: [PATCH 2/7] some updates --- .../home/view/screens/create_post_screen.dart | 3 + .../view/screens/reply_thread_screen.dart | 33 ++--- .../home/view/screens/tweet_screen.dart | 11 +- .../repositories/profile_repo_impl.dart | 20 +-- .../view/screens/explore_profile_screen.dart | 5 +- .../view/widgets/profile/profile_header.dart | 39 +++++- .../explore_profile_screen_body.dart | 121 ++++++++++++------ .../shared_tweet_components.dart | 42 ++++-- .../settings/view_model/providers.dart | 98 +++++++------- .../trends/view/screens/explore_screen.dart | 9 +- .../view/widgets/explore_category_tab.dart | 7 +- .../view/widgets/explore_screen_body.dart | 93 ++++++++++---- .../trends/view_model/trends_view_model.dart | 9 +- 13 files changed, 328 insertions(+), 162 deletions(-) diff --git a/lib/features/home/view/screens/create_post_screen.dart b/lib/features/home/view/screens/create_post_screen.dart index 2ba716e..9720083 100644 --- a/lib/features/home/view/screens/create_post_screen.dart +++ b/lib/features/home/view/screens/create_post_screen.dart @@ -12,6 +12,7 @@ import 'package:lite_x/features/home/services/hashtag_service.dart'; import 'package:lite_x/features/home/view/widgets/hashtag_suggestions_overlay.dart'; import 'package:lite_x/features/home/view/widgets/mention_suggestion_overlay.dart'; import 'package:lite_x/features/home/models/user_suggestion.dart'; +import 'package:lite_x/features/profile/view_model/providers.dart'; enum PostPrivacy { everyone, @@ -142,6 +143,8 @@ class _CreatePostScreenState extends ConsumerState { _textController.clear(); _selectedMedia.clear(); }); + if (mounted) + ref.refresh(profilePostsProvider(ref.read(myUserNameProvider))); Navigator.pop(context, true); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/features/home/view/screens/reply_thread_screen.dart b/lib/features/home/view/screens/reply_thread_screen.dart index 0c89d8c..234355e 100644 --- a/lib/features/home/view/screens/reply_thread_screen.dart +++ b/lib/features/home/view/screens/reply_thread_screen.dart @@ -570,13 +570,12 @@ class _ReplyThreadScreenState extends ConsumerState { hashtag, ); if (matchingHashtag != null) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => TempHashtagScreen( - hashtagId: matchingHashtag.id, - hashtagText: matchingHashtag.tagText, - ), - ), + context.push( + "/hashtagTweetsScreen", + extra: [ + matchingHashtag.id, + matchingHashtag.tagText, + ], ); } }, @@ -710,13 +709,9 @@ class _ReplyThreadScreenState extends ConsumerState { hashtag, ); if (matchingHashtag != null) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => TempHashtagScreen( - hashtagId: matchingHashtag.id, - hashtagText: matchingHashtag.tagText, - ), - ), + context.push( + "/hashtagTweetsScreen", + extra: [matchingHashtag.id, matchingHashtag.tagText], ); } }, @@ -820,13 +815,9 @@ class _ReplyThreadScreenState extends ConsumerState { hashtag, ); if (matchingHashtag != null) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => TempHashtagScreen( - hashtagId: matchingHashtag.id, - hashtagText: matchingHashtag.tagText, - ), - ), + context.push( + "/hashtagTweetsScreen", + extra: [matchingHashtag.id, matchingHashtag.tagText], ); } }, diff --git a/lib/features/home/view/screens/tweet_screen.dart b/lib/features/home/view/screens/tweet_screen.dart index 7b96e50..b06ca3a 100644 --- a/lib/features/home/view/screens/tweet_screen.dart +++ b/lib/features/home/view/screens/tweet_screen.dart @@ -3,6 +3,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:lite_x/core/providers/current_user_provider.dart'; import 'package:lite_x/features/home/models/tweet_model.dart'; @@ -78,15 +79,7 @@ class _TweetDetailScreenState extends ConsumerState { void _openHashtagPosts({required String hashtagId, required String tagText}) { try { - print( - '🏷️ Opening temp hashtag screen: id="$hashtagId", text="$tagText"', - ); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => - TempHashtagScreen(hashtagId: hashtagId, hashtagText: tagText), - ), - ); + context.push("/hashtagTweetsScreen", extra: [hashtagId, tagText]); } catch (e, stackTrace) { print('❌ Error opening hashtag screen: $e'); print('Stack trace: $stackTrace'); diff --git a/lib/features/profile/repositories/profile_repo_impl.dart b/lib/features/profile/repositories/profile_repo_impl.dart index ce38d9a..4a704a9 100644 --- a/lib/features/profile/repositories/profile_repo_impl.dart +++ b/lib/features/profile/repositories/profile_repo_impl.dart @@ -348,6 +348,7 @@ class ProfileRepoImpl implements ProfileRepo { await _dio.post("api/blocks/$username"); final myUsername = ref.read(myUserNameProvider); ref.refresh(getBlockedUsersProvider(myUsername)); + ref.refresh(profileDataProvider(username)); return const Right(()); } catch (e) { @@ -360,6 +361,7 @@ class ProfileRepoImpl implements ProfileRepo { await _dio.delete("api/blocks/$username"); final myUsername = ref.read(myUserNameProvider); ref.refresh(getBlockedUsersProvider(myUsername)); + ref.refresh(profileDataProvider(username)); return const Right(()); } catch (e) { return Left(Failure("couldn't unblock user")); @@ -796,17 +798,19 @@ class ProfileRepoImpl implements ProfileRepo { res = await _dio.get("api/explore", queryParameters: queryParams); final List jsonList = res.data["data"] ?? []; - final String? nextCursor = res.data["cursor"] as String?; - + final dynamic nextCursor = res.data["cursor"]; final tweets = convertJsonListToTweetList(jsonList, false); - print("cursor: " + cursor.toString() + "999999999999999999"); - - return Right(PaginatedTweets(tweets: tweets, nextCursor: nextCursor)); - } catch (e) { - print( - "fail-----------------------------------------------____ + ${e.toString()}", + return Right( + PaginatedTweets( + tweets: tweets, + nextCursor: nextCursor == null || nextCursor == 0 ? null : nextCursor, + ), ); + } catch (e) { + // print( + // "fail-----------------------------------------------____ + ${e.toString()}", + // ); return Left(Failure('Failed to load ${categoryName} tweets')); } } diff --git a/lib/features/profile/view/screens/explore_profile_screen.dart b/lib/features/profile/view/screens/explore_profile_screen.dart index e2584cd..bc24a53 100644 --- a/lib/features/profile/view/screens/explore_profile_screen.dart +++ b/lib/features/profile/view/screens/explore_profile_screen.dart @@ -41,7 +41,10 @@ class _ExploreProfileScreenState extends ConsumerState { ), title: GestureDetector( onTap: () { - context.push("/searchScreen",extra: {'showResults': false}); + context.push( + "/searchScreen", + extra: {'showResults': false}, + ); }, child: Row( children: [ diff --git a/lib/features/profile/view/widgets/profile/profile_header.dart b/lib/features/profile/view/widgets/profile/profile_header.dart index a272e3c..b8ad4be 100644 --- a/lib/features/profile/view/widgets/profile/profile_header.dart +++ b/lib/features/profile/view/widgets/profile/profile_header.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; +import 'package:lite_x/core/routes/Route_Constants.dart'; +import 'package:lite_x/features/chat/view_model/conversions/Conversations_view_model.dart'; import 'package:lite_x/features/profile/models/profile_model.dart'; import 'package:lite_x/features/profile/models/shared.dart'; import 'package:lite_x/features/profile/view/widgets/profile/block_button.dart'; @@ -205,7 +207,42 @@ class _ProfileAvatarRow extends ConsumerWidget { } else if (profileData.isBlockedByMe) { return BlockButton(profileData: profileData, showDataFunc: showDataFunc); } else { - return Follow_Following_Button(profileData: profileData); + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!isMe) + Padding( + padding: const EdgeInsets.only(top: 60), + child: GestureDetector( + onTap: () async { + final result = await ref + .read(conversationsViewModelProvider.notifier) + .createChat( + isDMChat: true, + recipientIds: [profileData.id], + ); + result.fold((l) => print("Error"), (chatModel) { + context.pushNamed( + RouteConstants.ChatScreen, + pathParameters: {'chatId': chatModel.id}, + extra: { + 'title': profileData.displayName, + 'subtitle': "${profileData.username}", + 'avatarUrl': profileData.avatarId, + 'isGroup': false, + 'recipientFollowersCount': profileData.followersCount, + }, + ); + }); + }, + child: Icon(Icons.mail_outline), + ), + ), + if (!isMe) SizedBox(width: 10), + Follow_Following_Button(profileData: profileData), + ], + ); } } diff --git a/lib/features/profile/view/widgets/profile_search/explore_profile_screen_body.dart b/lib/features/profile/view/widgets/profile_search/explore_profile_screen_body.dart index 4b602ec..b20424b 100644 --- a/lib/features/profile/view/widgets/profile_search/explore_profile_screen_body.dart +++ b/lib/features/profile/view/widgets/profile_search/explore_profile_screen_body.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lite_x/core/providers/current_user_provider.dart'; @@ -47,43 +49,88 @@ class _ExploreprofilescreenbodyState ); }, (pm) { - return DefaultTabController( - length: 6, - child: Scaffold( - appBar: TabBar( - indicatorColor: Color(0xFF1DA1F2), - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - tabAlignment: TabAlignment.start, - dividerHeight: 0.25, - labelColor: Colors.white, - unselectedLabelStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - labelStyle: TextStyle(fontWeight: FontWeight.bold), - unselectedLabelColor: Colors.grey, - tabs: [ - Tab(text: 'For You'), - Tab(text: 'Trending'), - Tab(text: 'Global'), - Tab(text: 'News'), - Tab(text: 'Sports'), - Tab(text: 'Entertainment'), - ], - ), - body: TabBarView( - children: [ - _BuildForYouTab(pm), - _BuildTrendingTab(pm), - _BuildGlobalTab(pm), - _BuildNewsTab(pm), - _BuildSportsTab(pm), - _BuildEntertainmentTab(pm), - ], - ), - ), - ); + return Platform.isAndroid + ? DefaultTabController( + length: 6, + child: Scaffold( + appBar: TabBar( + indicatorColor: Color(0xFF1DA1F2), + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerHeight: 0.25, + labelColor: Colors.white, + unselectedLabelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + labelStyle: TextStyle(fontWeight: FontWeight.bold), + unselectedLabelColor: Colors.grey, + tabs: [ + Tab(text: 'For You'), + Tab(text: 'Trending'), + Tab(text: 'Global'), + Tab(text: 'News'), + Tab(text: 'Sports'), + Tab(text: 'Entertainment'), + ], + ), + body: TabBarView( + children: [ + _BuildForYouTab(pm), + _BuildTrendingTab(pm), + _BuildGlobalTab(pm), + _BuildNewsTab(pm), + _BuildSportsTab(pm), + _BuildEntertainmentTab(pm), + ], + ), + ), + ) + : DefaultTabController( + length: 6, + child: Column( + children: [ + Material( + color: Theme.of(context).scaffoldBackgroundColor, + child: TabBar( + indicatorColor: Color(0xFF1DA1F2), + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerHeight: 0.25, + labelColor: Colors.white, + unselectedLabelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + labelStyle: TextStyle(fontWeight: FontWeight.bold), + unselectedLabelColor: Colors.grey, + tabs: [ + Tab(text: 'For You'), + Tab(text: 'Trending'), + Tab(text: 'Global'), + Tab(text: 'News'), + Tab(text: 'Sports'), + Tab(text: 'Entertainment'), + ], + ), + ), + Expanded( + child: TabBarView( + children: [ + _BuildForYouTab(pm), + _BuildTrendingTab(pm), + _BuildGlobalTab(pm), + _BuildNewsTab(pm), + _BuildSportsTab(pm), + _BuildEntertainmentTab(pm), + ], + ), + ), + ], + ), + ); }, ); }, diff --git a/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart b/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart index 00dd7d4..8b0ec31 100644 --- a/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart +++ b/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart @@ -719,18 +719,38 @@ class _ExpandableLinkedTextState extends State { context.push("/profilescreen/${token.substring(1)}"); } catch (e) {} } else if (token.contains("#")) { - final hash_word = token.substring(1); - final hash_map = hashs.firstWhere( - (h) => h["hashtagName"] == hash_word.toLowerCase(), - ); - if (hash_map["id"] != null && - hash_map["hashtagName"] != null && - hash_map["id"]!.isNotEmpty && - hash_map["hashtagName"]!.isNotEmpty) - context.push( - "/hashtagTweetsScreen", - extra: [hash_map["id"], hash_map["hashtagName"]], + try { + final hash_word = token.substring(1); + final hash_map = hashs.firstWhere( + (h) => h["hashtagName"] == hash_word.toLowerCase(), ); + if (hash_map != null && + hash_map["id"] != null && + hash_map["hashtagName"] != null && + hash_map["id"]!.isNotEmpty && + hash_map["hashtagName"]!.isNotEmpty) + context.push( + "/hashtagTweetsScreen", + extra: [hash_map["id"], hash_map["hashtagName"]], + ); + else { + if (mounted) + showSmallPopUpMessage( + context: context, + message: "hastag id isn't found", + borderColor: Colors.red, + icon: Icon(Icons.error), + ); + } + } catch (e) { + if (mounted) + showSmallPopUpMessage( + context: context, + message: "hastag id isn't found", + borderColor: Colors.red, + icon: Icon(Icons.error), + ); + } } }, ), diff --git a/lib/features/settings/view_model/providers.dart b/lib/features/settings/view_model/providers.dart index 54d3d6b..2acc147 100644 --- a/lib/features/settings/view_model/providers.dart +++ b/lib/features/settings/view_model/providers.dart @@ -5,6 +5,7 @@ import 'package:lite_x/core/providers/current_user_provider.dart'; import 'package:lite_x/core/providers/dio_interceptor.dart'; import 'package:lite_x/features/profile/models/shared.dart'; import 'package:lite_x/features/profile/models/user_model.dart'; +import 'package:lite_x/features/profile/view_model/providers.dart'; import 'package:lite_x/features/settings/models/settings_model.dart'; import 'package:lite_x/features/settings/repositories/settings_repo.dart'; import 'package:lite_x/features/settings/repositories/settings_repo_impl.dart'; @@ -14,75 +15,84 @@ import 'package:lite_x/features/settings/view_model/muted_users_notifier.dart'; import 'package:lite_x/features/settings/view_model/blocked_users_notifier.dart'; final settingsRepoProvider = Provider((ref) { - final dio = ref.watch(dioProvider); - return SettingsRepoImpl(dio); + final dio = ref.watch(dioProvider); + return SettingsRepoImpl(dio); }); final settingsBasicDataNotifierProvider = StateNotifierProvider.autoDispose - .family(( - ref, - username, -) { - final repo = ref.watch(settingsRepoProvider); - return SettingsBasicDataNotifier(settingsRepo: repo, username: username); -}); - -final blockedAccountsProvider = FutureProvider>>((ref) async { - final repo = ref.watch(settingsRepoProvider); - final user = ref.watch(currentUserProvider); - if (user == null) { - return Left(Failure('No current user')); - } - return repo.getBlockedAccounts(user.username); -}); + .family(( + ref, + username, + ) { + final repo = ref.watch(settingsRepoProvider); + return SettingsBasicDataNotifier(settingsRepo: repo, username: username); + }); -final mutedAccountsProvider = FutureProvider>>((ref) async { - final repo = ref.watch(settingsRepoProvider); - final user = ref.watch(currentUserProvider); - if (user == null) { - return Left(Failure('No current user')); - } - return repo.getMutedAccounts(user.username); -}); +final blockedAccountsProvider = + FutureProvider>>((ref) async { + final repo = ref.watch(settingsRepoProvider); + final user = ref.watch(currentUserProvider); + if (user == null) { + return Left(Failure('No current user')); + } + return repo.getBlockedAccounts(user.username); + }); -final mutedUsersNotifierProvider = StateNotifierProvider.autoDispose((ref) { +final mutedAccountsProvider = FutureProvider>>(( + ref, +) async { final repo = ref.watch(settingsRepoProvider); - return MutedUsersNotifier(settingsRepo: repo); + final user = ref.watch(currentUserProvider); + if (user == null) { + return Left(Failure('No current user')); + } + return repo.getMutedAccounts(user.username); }); -final blockedUsersNotifierProvider = StateNotifierProvider.autoDispose((ref) { - final repo = ref.watch(settingsRepoProvider); - return BlockedUsersNotifier(settingsRepo: repo); -}); +final mutedUsersNotifierProvider = + StateNotifierProvider.autoDispose(( + ref, + ) { + final repo = ref.watch(settingsRepoProvider); + return MutedUsersNotifier(settingsRepo: repo); + }); + +final blockedUsersNotifierProvider = + StateNotifierProvider.autoDispose(( + ref, + ) { + final repo = ref.watch(settingsRepoProvider); + return BlockedUsersNotifier(settingsRepo: repo); + }); final blockControllerProvider = Provider((ref) { - final repo = ref.watch(settingsRepoProvider); - return (String username) => repo.blockAccount(username); + final repo = ref.watch(profileRepoProvider); + return (String username) => repo.blockUser(username, ref); }); final unblockControllerProvider = Provider((ref) { - final repo = ref.watch(settingsRepoProvider); - return (String username) => repo.unblockAccount(username); + final repo = ref.watch(profileRepoProvider); + return (String username) => repo.unBlockUser(username, ref); }); final muteControllerProvider = Provider((ref) { - final repo = ref.watch(settingsRepoProvider); - return (String username) => repo.muteAccount(username); + final repo = ref.watch(profileRepoProvider); + return (String username) => repo.muteUser(username, ref); }); final unMuteControllerProvider = Provider((ref) { - final repo = ref.watch(settingsRepoProvider); - return (String username) => repo.unMuteAccount(username); + final repo = ref.watch(profileRepoProvider); + return (String username) => repo.unMuteUser(username, ref); }); final followMutedAccountControllerProvider = Provider((ref) { - final repo = ref.watch(settingsRepoProvider); - return (String username) => repo.followUser(username); + final repo = ref.watch(settingsRepoProvider); + return (String username) => repo.followUser(username); }); final unFollowMutedAccountControllerProvider = Provider((ref) { - final repo = ref.watch(settingsRepoProvider); - return (String username) => repo.unFollowUser(username); + final repo = ref.watch(settingsRepoProvider); + return (String username) => repo.unFollowUser(username); }); final updateSettingsControllerProvider = Provider((ref) { diff --git a/lib/features/trends/view/screens/explore_screen.dart b/lib/features/trends/view/screens/explore_screen.dart index cbfe118..b138049 100644 --- a/lib/features/trends/view/screens/explore_screen.dart +++ b/lib/features/trends/view/screens/explore_screen.dart @@ -24,7 +24,14 @@ class _ExploreScreenState extends ConsumerState { if (context.canPop()) context.pop(); }, ), - title: Text("Explore",style: TextStyle(color: Colors.white,fontSize: 18,fontWeight: FontWeight.w900),), + title: Text( + "Explore", + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), actions: [Icon(Icons.settings_outlined)], actionsPadding: EdgeInsets.only(right: 20), ), diff --git a/lib/features/trends/view/widgets/explore_category_tab.dart b/lib/features/trends/view/widgets/explore_category_tab.dart index b3de1de..e98c3fd 100644 --- a/lib/features/trends/view/widgets/explore_category_tab.dart +++ b/lib/features/trends/view/widgets/explore_category_tab.dart @@ -19,9 +19,12 @@ class ExploreCategoryTab extends ConsumerWidget { final state = ref.watch(exploreCategoryProvider(categoryName)); final notifier = ref.read(exploreCategoryProvider(categoryName).notifier); - // Load initial data on first build + // Load initial data on first build (only once) WidgetsBinding.instance.addPostFrameCallback((_) { - if (state.tweets.isEmpty && !state.isLoading && !state.hasError) { + if (state.tweets.isEmpty && + !state.isLoading && + !state.hasError && + !state.didInitialLoadAttempt) { notifier.loadInitial(); } }); diff --git a/lib/features/trends/view/widgets/explore_screen_body.dart b/lib/features/trends/view/widgets/explore_screen_body.dart index 100c66e..ebf3ff0 100644 --- a/lib/features/trends/view/widgets/explore_screen_body.dart +++ b/lib/features/trends/view/widgets/explore_screen_body.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lite_x/core/providers/current_user_provider.dart'; @@ -42,31 +44,72 @@ class _ExploreScreenbodyState extends ConsumerState { ); }, (pm) { - return DefaultTabController( - length: _tabs.length, - child: Scaffold( - appBar: TabBar( - indicatorColor: Color(0xFF1DA1F2), - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - tabAlignment: TabAlignment.start, - dividerHeight: 0.25, - labelColor: Colors.white, - unselectedLabelStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - labelStyle: TextStyle(fontWeight: FontWeight.bold), - unselectedLabelColor: Colors.grey, - tabs: _tabs.map((x) => Tab(text: x)).toList(), - ), - body: TabBarView( - children: _tabs - .map((x) => ExploreCategoryTab(categoryName: x, pm: pm)) - .toList(), - ), - ), - ); + return Platform.isAndroid + ? DefaultTabController( + length: _tabs.length, + child: Scaffold( + appBar: TabBar( + indicatorColor: Color(0xFF1DA1F2), + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerHeight: 0.25, + labelColor: Colors.white, + unselectedLabelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + labelStyle: TextStyle(fontWeight: FontWeight.bold), + unselectedLabelColor: Colors.grey, + tabs: _tabs.map((x) => Tab(text: x)).toList(), + ), + body: TabBarView( + children: _tabs + .map( + (x) => + ExploreCategoryTab(categoryName: x, pm: pm), + ) + .toList(), + ), + ), + ) + : DefaultTabController( + length: _tabs.length, + child: Column( + children: [ + Material( + color: Theme.of(context).scaffoldBackgroundColor, + child: TabBar( + indicatorColor: Color(0xFF1DA1F2), + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerHeight: 0.25, + labelColor: Colors.white, + unselectedLabelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + labelStyle: TextStyle(fontWeight: FontWeight.bold), + unselectedLabelColor: Colors.grey, + tabs: _tabs.map((x) => Tab(text: x)).toList(), + ), + ), + Expanded( + child: TabBarView( + children: _tabs + .map( + (x) => ExploreCategoryTab( + categoryName: x, + pm: pm, + ), + ) + .toList(), + ), + ), + ], + ), + ); }, ); }, diff --git a/lib/features/trends/view_model/trends_view_model.dart b/lib/features/trends/view_model/trends_view_model.dart index ed28742..a25af1d 100644 --- a/lib/features/trends/view_model/trends_view_model.dart +++ b/lib/features/trends/view_model/trends_view_model.dart @@ -15,14 +15,13 @@ class TrendsViewModel extends ChangeNotifier { } } - - class ExploreCategoryState { final List tweets; final String? nextCursor; final bool isLoading; final bool isLoadingMore; final bool hasError; + final bool didInitialLoadAttempt; ExploreCategoryState({ required this.tweets, @@ -30,6 +29,7 @@ class ExploreCategoryState { required this.isLoading, required this.isLoadingMore, required this.hasError, + this.didInitialLoadAttempt = false, }); ExploreCategoryState copyWith({ @@ -38,6 +38,7 @@ class ExploreCategoryState { bool? isLoading, bool? isLoadingMore, bool? hasError, + bool? didInitialLoadAttempt, }) { return ExploreCategoryState( tweets: tweets ?? this.tweets, @@ -45,6 +46,8 @@ class ExploreCategoryState { isLoading: isLoading ?? this.isLoading, isLoadingMore: isLoadingMore ?? this.isLoadingMore, hasError: hasError ?? this.hasError, + didInitialLoadAttempt: + didInitialLoadAttempt ?? this.didInitialLoadAttempt, ); } } @@ -81,6 +84,7 @@ class ExploreCategoryNotifier extends StateNotifier { hasError: false, tweets: [], nextCursor: null, + didInitialLoadAttempt: true, ); try { @@ -154,6 +158,7 @@ class ExploreCategoryNotifier extends StateNotifier { isLoading: false, isLoadingMore: false, hasError: false, + didInitialLoadAttempt: false, ); loadInitial(); } From 57c713b67d1ce5641e750b04f7d7e56fb5372a00 Mon Sep 17 00:00:00 2001 From: Beshoy Sorial Date: Mon, 15 Dec 2025 19:35:54 +0200 Subject: [PATCH 3/7] test: remove lint and test stages from Dockerfile Removed linting and testing stages from Dockerfile. --- Dockerfile | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1b68dbf..fce9b47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,28 +21,6 @@ RUN sdkmanager \ -# ---------------- LINT STAGE ---------------- -FROM base AS lint -WORKDIR /app - -COPY pubspec.* ./ -RUN flutter pub get - -COPY . . -RUN flutter analyze || true - - -# ---------------- TEST STAGE ---------------- -FROM lint AS test -WORKDIR /app - -# Run unit/widget tests -RUN flutter test --no-pub - -# --------------- E2E TEST --------------------- -FROM base AS e2e -RUN flutter test integration_test || true - # ---------------- BUILD APK STAGE ---------------- FROM base AS build-apk From f5bf9a1746a3eee81be20ed6d10b58c7119bd187 Mon Sep 17 00:00:00 2001 From: Beshoy Sorial Date: Mon, 15 Dec 2025 20:35:30 +0200 Subject: [PATCH 4/7] Split APK builds by ABI for release --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fce9b47..2168fa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,4 @@ RUN sdkmanager \ # ---------------- BUILD APK STAGE ---------------- FROM base AS build-apk -RUN flutter build apk --release +RUN flutter build apk --release --split-per-abi From 2e9a074ab6d6b2100a4d31965870bcf83fe0e77e Mon Sep 17 00:00:00 2001 From: Beshoy Sorial Date: Mon, 15 Dec 2025 20:40:43 +0200 Subject: [PATCH 5/7] Update .dockerignore --- .dockerignore | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/.dockerignore b/.dockerignore index 08c683d..2eba3e1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,20 +1,53 @@ -# Ignore build artifacts -build +# Ignore all build artifacts +build/ +# But keep the release APK +!build/app/outputs/flutter-apk/app-release.apk +!build/app/outputs/flutter-apk/ + +# Dart/Flutter .dart_tool -.idea -.vscode +.flutter-plugins +.flutter-plugins-dependencies +.packages +pubspec.lock -# Android / iOS build cache +# Android build cache +android/.gradle/ +android/app/build/ android/.gradle android/app/build +android/local.properties +android/**/*.iml + +# iOS +ios/Pods/ +ios/build/ +ios/.symlinks/ ios/Pods ios/build ios/.symlinks +# IDEs +.idea/ +.vscode/ +.idea +.vscode +*.iml + # System files .DS_Store +**/.DS_Store -# Git files -.git +# Git +.git/ .gitignore +.git README.md + +# Gradle cache +.gradle/ +**/.gradle/ + +# Other large directories +node_modules/ +coverage/ From b862b498fbc9024e12760751ea9b5f1f933fbd2e Mon Sep 17 00:00:00 2001 From: Beshoy Sorial Date: Mon, 15 Dec 2025 21:07:12 +0200 Subject: [PATCH 6/7] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2168fa9..2784ae0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,4 @@ RUN sdkmanager \ # ---------------- BUILD APK STAGE ---------------- FROM base AS build-apk -RUN flutter build apk --release --split-per-abi +RUN flutter build apk --release --target-platform android-arm64 From 84f7a694e12576dc92539f9328102a92c20452df Mon Sep 17 00:00:00 2001 From: Hazem-Emam-404 Date: Mon, 15 Dec 2025 22:19:11 +0200 Subject: [PATCH 7/7] some updates --- lib/core/routes/AppRouter.dart | 11 + lib/core/routes/Route_Constants.dart | 1 + .../home/view/screens/home_screen.dart | 5 +- .../home/view/widgets/tweet_widget.dart | 7 +- lib/features/profile/models/shared.dart | 37 ++-- .../profile/repositories/profile_repo.dart | 3 + .../repositories/profile_repo_impl.dart | 61 +++--- .../view/widgets/profile/profile_header.dart | 35 +-- .../likers_retweeters_screen.dart | 207 ++++++++++++++++++ .../shared_tweet_components.dart | 21 +- .../profile/view_model/providers.dart | 18 ++ 11 files changed, 337 insertions(+), 69 deletions(-) create mode 100644 lib/features/profile/view/widgets/profile_tweets/likers_retweeters_screen.dart diff --git a/lib/core/routes/AppRouter.dart b/lib/core/routes/AppRouter.dart index 33ff550..4869240 100644 --- a/lib/core/routes/AppRouter.dart +++ b/lib/core/routes/AppRouter.dart @@ -26,6 +26,7 @@ import 'package:lite_x/features/chat/view/screens/conversations_screen.dart'; import 'package:lite_x/features/explore/view/explore_screen.dart'; import 'package:lite_x/features/home/models/tweet_model.dart'; import 'package:lite_x/features/home/view/screens/quote_composer_screen.dart'; +import 'package:lite_x/features/profile/view/widgets/profile_tweets/likers_retweeters_screen.dart'; import 'package:lite_x/features/trends/view/screens/explore_screen.dart'; import 'package:lite_x/features/trends/view/screens/shatag_tweets_screen.dart'; import 'package:lite_x/features/trends/view/screens/trends_screen.dart'; @@ -488,6 +489,16 @@ class Approuter { transitionsBuilder: _slideRightTransitionBuilder, ), ), + GoRoute( + name: RouteConstants.LikersRetweetersScreen, + path: "/likersRetweetersScreen/:tweetId", + pageBuilder: (context, state) => CustomTransitionPage( + child: LikersRetweetersScreen( + tweetId: state.pathParameters["tweetId"] as String, + ), + transitionsBuilder: _slideRightTransitionBuilder, + ), + ), ], redirect: (context, state) { return null; diff --git a/lib/core/routes/Route_Constants.dart b/lib/core/routes/Route_Constants.dart index 6d7219c..ec33f04 100644 --- a/lib/core/routes/Route_Constants.dart +++ b/lib/core/routes/Route_Constants.dart @@ -61,4 +61,5 @@ class RouteConstants { static String WhoToFollowScreen = "WhoToFollowScreen"; static String notifications = "notifications"; static String MentionedTweetsScreen = "MentionedTweetsScreen"; + static String LikersRetweetersScreen = "LikersRetweetersScreen"; } diff --git a/lib/features/home/view/screens/home_screen.dart b/lib/features/home/view/screens/home_screen.dart index e3ff619..45241f3 100644 --- a/lib/features/home/view/screens/home_screen.dart +++ b/lib/features/home/view/screens/home_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:lite_x/core/providers/current_user_provider.dart'; import 'package:lite_x/core/view/screen/app_shell.dart'; import 'package:lite_x/features/home/models/tweet_model.dart'; @@ -394,7 +395,9 @@ class _HomeScreenState extends ConsumerState ref.read(homeViewModelProvider.notifier).toggleLike(tweet.id); }, onShare: () {}, - onReach: () {}, + onReach: () { + context.push("/likersRetweetersScreen/${tweet.id}"); + }, onSave: () { ref.read(homeViewModelProvider.notifier).toggleBookmark(tweet.id); }, diff --git a/lib/features/home/view/widgets/tweet_widget.dart b/lib/features/home/view/widgets/tweet_widget.dart index 5d90a47..330fdfb 100644 --- a/lib/features/home/view/widgets/tweet_widget.dart +++ b/lib/features/home/view/widgets/tweet_widget.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:lite_x/features/profile/view/screens/profile_screen.dart'; +import 'package:lite_x/features/profile/view_model/providers.dart'; import 'expandable_text.dart'; import 'media_gallery.dart'; import '../../models/tweet_model.dart'; -class TweetWidget extends StatelessWidget { +class TweetWidget extends ConsumerWidget { final String userDisplayName; final String username; final String timeAgo; @@ -84,7 +87,7 @@ class TweetWidget extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return InkWell( onTap: onTap, child: Container( diff --git a/lib/features/profile/models/shared.dart b/lib/features/profile/models/shared.dart index 85c5fca..e8a67b9 100644 --- a/lib/features/profile/models/shared.dart +++ b/lib/features/profile/models/shared.dart @@ -625,10 +625,6 @@ class _InterActionsRowOfTweetState ref.refresh( profilePostsProvider(widget.tweet.userUserName), ); - final currUser = ref.watch(currentUserProvider); - if (currUser != null) { - ref.refresh(profilePostsProvider(currUser.username)); - } } }, ); @@ -672,21 +668,26 @@ class _InterActionsRowOfTweetState onTap: () { // TODO: do activity action }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - "assets/svg/activity.svg", - width: 20, - height: 20, - colorFilter: ColorFilter.mode(Colors.grey, BlendMode.srcIn), - ), - if (widget.tweet.activityNumber > 0) - Text( - Shared.formatCount(widget.tweet.activityNumber), - style: TextStyle(color: Colors.grey, fontSize: 15), + child: GestureDetector( + onTap: () { + context.push("/likersRetweetersScreen/${widget.tweet.id}"); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + "assets/svg/activity.svg", + width: 20, + height: 20, + colorFilter: ColorFilter.mode(Colors.grey, BlendMode.srcIn), ), - ], + if (widget.tweet.activityNumber > 0) + Text( + Shared.formatCount(widget.tweet.activityNumber), + style: TextStyle(color: Colors.grey, fontSize: 15), + ), + ], + ), ), ), Row( diff --git a/lib/features/profile/repositories/profile_repo.dart b/lib/features/profile/repositories/profile_repo.dart index c1afc5e..f5c9c6a 100644 --- a/lib/features/profile/repositories/profile_repo.dart +++ b/lib/features/profile/repositories/profile_repo.dart @@ -63,6 +63,9 @@ abstract class ProfileRepo { String username, ); + Future>> getRetweeters(String tweetId); + Future>> getLikers(String tweetId); + Future> getProfileTweet(String tweetId); Future> createTweet(CreateTweetModel createTweetModel); Future>> getTweetReplies( diff --git a/lib/features/profile/repositories/profile_repo_impl.dart b/lib/features/profile/repositories/profile_repo_impl.dart index 4a704a9..921e88a 100644 --- a/lib/features/profile/repositories/profile_repo_impl.dart +++ b/lib/features/profile/repositories/profile_repo_impl.dart @@ -44,7 +44,6 @@ class ProfileRepoImpl implements ProfileRepo { return Right(profileData); } on DioException catch (e) { - print("can't get profile data"); if (userName == currentUsername) { await storageService.init(); final localData = await storageService.getProfileData(currentUsername); @@ -100,7 +99,6 @@ class ProfileRepoImpl implements ProfileRepo { "address": newModel.location, "dateOfBirth": dob, }; - print(json.toString()); final res = await _dio.patch("api/users/${newModel.id}", data: json); return Right(ProfileModel.fromJson(res.data)); @@ -130,7 +128,6 @@ class ProfileRepoImpl implements ProfileRepo { String tweetId, ) async { try { - // print("Start api ---------------------**"); final res = await _dio.get("api/tweets/$tweetId"); final Map json = res.data as Map; @@ -207,8 +204,6 @@ class ProfileRepoImpl implements ProfileRepo { // user interactions Future>> getFollowers(String userName) async { - // await Future.delayed(const Duration(seconds: 1)); - try { final response = await _dio.get("api/followers/$userName"); List jsonList = response.data['users'] as List; @@ -217,7 +212,6 @@ class ProfileRepoImpl implements ProfileRepo { .toList(); return Right(usersList); } catch (e) { - // print("\n--------------\n$e\n-------------------\n"); return Left(Failure("couldn't get user followers")); } } @@ -232,7 +226,6 @@ class ProfileRepoImpl implements ProfileRepo { .map((p) { p["photo"] = p["profileMedia"]?["id"]; p["isFollowing"] = p["isFollowed"]; - print(p.toString()); return UserModel.fromJson(p as Map); }) .toList() @@ -260,7 +253,6 @@ class ProfileRepoImpl implements ProfileRepo { .toList(); return Right(usersList); } catch (e) { - // print("\n--------------\n$e\n-------------------\n"); return Left(Failure("couldn't get user followings")); } } @@ -279,7 +271,6 @@ class ProfileRepoImpl implements ProfileRepo { .toList(); return Right(verified); } catch (e) { - // print("\n--------------\n$e\n-------------------\n"); return Left(Failure("couldn't get user verified followers")); } } @@ -299,7 +290,6 @@ class ProfileRepoImpl implements ProfileRepo { .toList(); return Right(youKnow); } catch (e) { - // print("\n--------------\n$e\n-------------------\n"); return Left(Failure("couldn't get user verified followers")); } } @@ -613,7 +603,6 @@ class ProfileRepoImpl implements ProfileRepo { ), ); } catch (e) { - print(e); return Left(Failure("couldn't change email, Please try again later")); } } @@ -623,8 +612,6 @@ class ProfileRepoImpl implements ProfileRepo { String code, ) async { try { - print(newEmail); - print(code); await _dio.post( "api/auth/verify-new-email", data: {"email": newEmail, "code": code.toString()}, @@ -656,13 +643,6 @@ class ProfileRepoImpl implements ProfileRepo { String confirmNewPassword, ) async { try { - print( - { - "oldPassword": oldPassword, - "newPassword": newPassword, - "confirmPassword": confirmNewPassword, - }.toString(), - ); await _dio.post( "api/auth/change-password", data: { @@ -694,7 +674,6 @@ class ProfileRepoImpl implements ProfileRepo { .map((p) { p["photo"] = p["profileMedia"]?["id"]; p["isFollowing"] = p["isFollowed"]; - print(p.toString()); return UserModel.fromJson(p as Map); }) .toList() @@ -786,7 +765,6 @@ class ProfileRepoImpl implements ProfileRepo { try { Response res; final queryParams = {}; - print("cursor: " + cursor.toString() + "999999999999999999"); if (categoryName != "general") { queryParams['category'] = categoryName; } @@ -808,10 +786,43 @@ class ProfileRepoImpl implements ProfileRepo { ), ); } catch (e) { - // print( - // "fail-----------------------------------------------____ + ${e.toString()}", - // ); return Left(Failure('Failed to load ${categoryName} tweets')); } } + + Future>> getLikers(String tweetId) async { + try { + final response = await _dio.get("api/tweets/${tweetId}/likes"); + List jsonList = response.data['data'] as List; + final List peoplemodels = jsonList + .map((p) { + p["photo"] = p["profileMedia"]?["id"]; + p["isFollowing"] = p["isFollowed"]; + return UserModel.fromJson(p as Map); + }) + .toList() + .cast(); + return Right(peoplemodels); + } catch (e) { + return Left(Failure("couldn't get tweet likers")); + } + } + + Future>> getRetweeters(String tweetId) async { + try { + final response = await _dio.get("api/tweets/${tweetId}/retweets"); + List jsonList = response.data['data'] as List; + final List peoplemodels = jsonList + .map((p) { + p["photo"] = p["profileMedia"]?["id"]; + p["isFollowing"] = p["isFollowed"]; + return UserModel.fromJson(p as Map); + }) + .toList() + .cast(); + return Right(peoplemodels); + } catch (e) { + return Left(Failure("couldn't get tweet retweeters")); + } + } } diff --git a/lib/features/profile/view/widgets/profile/profile_header.dart b/lib/features/profile/view/widgets/profile/profile_header.dart index b8ad4be..403cc78 100644 --- a/lib/features/profile/view/widgets/profile/profile_header.dart +++ b/lib/features/profile/view/widgets/profile/profile_header.dart @@ -215,26 +215,29 @@ class _ProfileAvatarRow extends ConsumerWidget { Padding( padding: const EdgeInsets.only(top: 60), child: GestureDetector( - onTap: () async { - final result = await ref + onTap: () { + ref .read(conversationsViewModelProvider.notifier) .createChat( isDMChat: true, recipientIds: [profileData.id], - ); - result.fold((l) => print("Error"), (chatModel) { - context.pushNamed( - RouteConstants.ChatScreen, - pathParameters: {'chatId': chatModel.id}, - extra: { - 'title': profileData.displayName, - 'subtitle': "${profileData.username}", - 'avatarUrl': profileData.avatarId, - 'isGroup': false, - 'recipientFollowersCount': profileData.followersCount, - }, - ); - }); + ) + .then((result) { + result.fold((l) => print("Error"), (chatModel) { + context.pushNamed( + RouteConstants.ChatScreen, + pathParameters: {'chatId': chatModel.id}, + extra: { + 'title': profileData.displayName, + 'subtitle': "${profileData.username}", + 'avatarUrl': profileData.avatarId, + 'isGroup': false, + 'recipientFollowersCount': + profileData.followersCount, + }, + ); + }); + }); }, child: Icon(Icons.mail_outline), ), diff --git a/lib/features/profile/view/widgets/profile_tweets/likers_retweeters_screen.dart b/lib/features/profile/view/widgets/profile_tweets/likers_retweeters_screen.dart new file mode 100644 index 0000000..7d7a8e3 --- /dev/null +++ b/lib/features/profile/view/widgets/profile_tweets/likers_retweeters_screen.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lite_x/features/profile/models/shared.dart'; +import 'package:lite_x/features/profile/models/user_model.dart'; +import 'package:lite_x/features/profile/view/widgets/following_followers/follower_card.dart'; +import 'package:lite_x/features/profile/view_model/providers.dart'; + +class LikersRetweetersScreen extends ConsumerStatefulWidget { + final String tweetId; + + const LikersRetweetersScreen({super.key, required this.tweetId}); + + @override + ConsumerState createState() => + _LikersRetweetersScreenState(); +} + +class _LikersRetweetersScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + ref.refresh(likersProvider(widget.tweetId)); + ref.refresh(retweetersProvider(widget.tweetId)); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (context.canPop()) { + context.pop(); + } + }, + ), + title: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Interactions', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(width: 5), + Icon(Icons.stacked_bar_chart_sharp), + ], + ), + bottom: TabBar( + controller: _tabController, + indicatorColor: const Color(0xFF1DA1F2), + indicatorSize: TabBarIndicatorSize.label, + isScrollable: false, + tabAlignment: TabAlignment.fill, + dividerHeight: 0.25, + labelColor: Colors.white, + unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.bold), + labelStyle: const TextStyle(fontWeight: FontWeight.bold), + unselectedLabelColor: Colors.grey, + tabs: const [ + Tab(text: 'Likers'), + Tab(text: 'Retweeters'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _LikersTab(tweetId: widget.tweetId), + _RetweetersTab(tweetId: widget.tweetId), + ], + ), + ); + } +} + +class _LikersTab extends ConsumerWidget { + final String tweetId; + + const _LikersTab({required this.tweetId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final likersAsync = ref.watch(likersProvider(tweetId)); + + return likersAsync.when( + data: (res) => res.fold( + (l) => _buildErrorWidget(l.message, likersProvider(tweetId), ref), + (data) { + if (data.isEmpty) + return _buildEmptyWidget(likersProvider(tweetId), ref); + return RefreshIndicator( + onRefresh: () async { + // ignore: unused_result + ref.refresh(likersProvider(tweetId)); + }, + child: ListView.builder( + itemBuilder: (context, index) => + FollowerCard(user: data[index], isMe: true), + itemCount: data.length, + ), + ); + }, + ), + error: (err, _) => _buildErrorWidget( + "can't get likers list for now...", + likersProvider(tweetId), + ref, + ), + loading: () => _buildLoadingWidget(), + ); + } +} + +class _RetweetersTab extends ConsumerWidget { + final String tweetId; + + const _RetweetersTab({required this.tweetId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final retweetersAsync = ref.watch(retweetersProvider(tweetId)); + + return retweetersAsync.when( + data: (res) => res.fold( + (l) => _buildErrorWidget(l.message, retweetersProvider(tweetId), ref), + (data) { + if (data.isEmpty) + return _buildEmptyWidget(retweetersProvider(tweetId), ref); + return RefreshIndicator( + onRefresh: () async { + // ignore: unused_result + ref.refresh(likersProvider(tweetId)); + }, + child: ListView.builder( + itemBuilder: (context, index) => + FollowerCard(user: data[index], isMe: true), + itemCount: data.length, + ), + ); + }, + ), + error: (err, _) => _buildErrorWidget( + "can't get likers list for now...", + retweetersProvider(tweetId), + ref, + ), + loading: () => _buildLoadingWidget(), + ); + } +} + +Widget _buildErrorWidget(String text, dynamic provider, WidgetRef ref) { + return RefreshIndicator( + child: ListView( + children: [ + SizedBox(height: 100), + Center(child: Text(text)), + ], + ), + onRefresh: () async { + // ignore: unused_result + ref.refresh(provider); + }, + ); +} + +Widget _buildLoadingWidget() { + return Center(child: CircularProgressIndicator()); +} + +Widget _buildEmptyWidget(dynamic provider, WidgetRef ref) { + return RefreshIndicator( + child: ListView( + children: [ + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.all(24), + child: Text( + "Nothing to see here -- yet.", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 35, + ), + ), + ), + ], + ), + onRefresh: () async { + // ignore: unused_result + ref.refresh(provider); + }, + ); +} diff --git a/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart b/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart index 8b0ec31..2b4ceb6 100644 --- a/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart +++ b/lib/features/profile/view/widgets/profile_tweets/shared_tweet_components.dart @@ -112,13 +112,20 @@ class BasicTweetWidget extends ConsumerWidget implements ProfileTweet { Row( mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset( - "assets/svg/grok.svg", - width: 20, - height: 20, - colorFilter: const ColorFilter.mode( - Colors.grey, - BlendMode.srcIn, + GestureDetector( + onTap: () { + context.push( + "/tweetDetailsScreen/${this.profilePostModel.id}", + ); + }, + child: SvgPicture.asset( + "assets/svg/grok.svg", + width: 20, + height: 20, + colorFilter: const ColorFilter.mode( + Colors.grey, + BlendMode.srcIn, + ), ), ), SizedBox(width: 8), diff --git a/lib/features/profile/view_model/providers.dart b/lib/features/profile/view_model/providers.dart index 22cd78c..29734fe 100644 --- a/lib/features/profile/view_model/providers.dart +++ b/lib/features/profile/view_model/providers.dart @@ -60,6 +60,24 @@ final followersProvider = return repo.getFollowers(username); }); +final likersProvider = + FutureProvider.family>, String>(( + ref, + tweetId, + ) async { + final repo = ref.watch(profileRepoProvider); + return repo.getLikers(tweetId); + }); + +final retweetersProvider = + FutureProvider.family>, String>(( + ref, + tweetId, + ) async { + final repo = ref.watch(profileRepoProvider); + return repo.getRetweeters(tweetId); + }); + final followingsProvider = FutureProvider.family>, String>(( ref,