Skip to content

Commit

Permalink
posts shimmers, like use case
Browse files Browse the repository at this point in the history
  • Loading branch information
dinaraparanid committed Jan 17, 2025
1 parent 8da8cc5 commit 8c5de49
Show file tree
Hide file tree
Showing 23 changed files with 205 additions and 61 deletions.
File renamed without changes.
8 changes: 8 additions & 0 deletions lib/core/data/firestore/firestore_query_ext.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:cloud_firestore/cloud_firestore.dart';

extension FirestoreQueryExt on Query<Map<String, dynamic>> {
Future<QueryDocumentSnapshot<Map<String, dynamic>>?> get firstOrNull async {
final res = await get();
return res.docs.firstOrNull;
}
}
18 changes: 15 additions & 3 deletions lib/core/data/post/data_source/like_api_impl.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fpdart/fpdart.dart';
import 'package:poster/core/data/paging/firestore_paging.dart';
import 'package:poster/core/data/firestore/firestore_paging.dart';
import 'package:poster/core/data/firestore/firestore_query_ext.dart';
import 'package:poster/core/domain/paging/page_data.dart';
import 'package:poster/core/domain/paging/paging_config.dart';
import 'package:poster/core/domain/post/data_source/like_api.dart';
Expand All @@ -12,17 +13,28 @@ const _collectionPostsLikes = 'posts_likes';

final class LikeApiImpl with LikeApi {
@override
Future<Either<Exception, Like>> likePost({
Future<Either<Exception, bool>> switchLikeForPost({
required String email,
required String postId,
}) => tryFuture(() async {
final doc = await FirebaseFirestore.instance
.collection(_collectionPostsLikes)
.where(Like.fieldPostId, isEqualTo: postId)
.where(Like.fieldUserEmail, isEqualTo: email)
.firstOrNull;

if (doc != null) {
await doc.reference.delete();
return false;
}

final like = Like(email: email, postId: postId);

await FirebaseFirestore.instance
.collection(_collectionPostsLikes)
.add(like.toJson());

return like;
return true;
});

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/core/data/post/data_source/post_api_impl.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fpdart/fpdart.dart';
import 'package:poster/core/data/paging/firestore_paging.dart';
import 'package:poster/core/data/firestore/firestore_paging.dart';
import 'package:poster/core/domain/paging/page_data.dart';
import 'package:poster/core/domain/paging/paging_config.dart';
import 'package:poster/core/domain/post/data_source/post_api.dart';
Expand Down
2 changes: 2 additions & 0 deletions lib/core/data/post/di/post_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:poster/core/di/provide_singleton.dart';
import 'package:poster/core/domain/post/data_source/like_api.dart';
import 'package:poster/core/domain/post/data_source/post_api.dart';
import 'package:poster/core/domain/post/repository/post_repository.dart';
import 'package:poster/core/domain/post/use_case/like_use_case.dart';
import 'package:poster/core/domain/profile/use_case/subscribe_on_profile_changes_use_case.dart';

extension PostModule on GetIt {
Expand All @@ -19,5 +20,6 @@ extension PostModule on GetIt {
provideSingleton<SubscribeOnProfileChangesUseCase>(() =>
SubscribeOnProfileChangesUseCase(repository: this())
),
provideSingleton<LikeUseCase>(() => LikeUseCase(repository: this())),
];
}
4 changes: 2 additions & 2 deletions lib/core/data/post/repository/post_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ final class PostRepositoryImpl extends PostRepository {
);

@override
Future<Either<Exception, Like>> likePost({
Future<Either<Exception, bool>> switchLikeForPost({
required String email,
required String postId,
}) => _likeApi.likePost(email: email, postId: postId);
}) => _likeApi.switchLikeForPost(email: email, postId: postId);

@override
Future<Either<Exception, int>> usersLikedPostCount({
Expand Down
2 changes: 1 addition & 1 deletion lib/core/data/profile/data_source/follower_api_impl.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fpdart/fpdart.dart';
import 'package:poster/core/data/firebase/firestore/utils/firestore_ext.dart';
import 'package:poster/core/data/paging/firestore_paging.dart';
import 'package:poster/core/data/firestore/firestore_paging.dart';
import 'package:poster/core/domain/paging/page_data.dart';
import 'package:poster/core/domain/paging/paging_config.dart';
import 'package:poster/core/domain/profile/data_source/follower_api.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/core/domain/post/data_source/like_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:poster/core/domain/post/entity/like.dart';
import 'package:poster/core/domain/post/entity/user.dart';

mixin LikeApi {
Future<Either<Exception, Like>> likePost({
Future<Either<Exception, bool>> switchLikeForPost({
required String email,
required String postId,
});
Expand Down
20 changes: 20 additions & 0 deletions lib/core/domain/post/use_case/like_use_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:fpdart/fpdart.dart';
import 'package:poster/core/domain/post/repository/post_repository.dart';
import 'package:poster/core/utils/functions/try_future.dart';

final class LikeUseCase {
final PostRepository _repository;

LikeUseCase({required PostRepository repository}) : _repository = repository;

Future<Either<Exception, void>> switchLikeForPost({
required String email,
required String postId,
required void Function(bool) onSuccess,
required void Function(Exception) onError,
}) => tryFuture(() async {
_repository
.switchLikeForPost(email: email, postId: postId)
.then((res) => res.fold(onError, onSuccess));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,27 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:poster/core/domain/post/entity/post.dart';
import 'package:poster/core/presentation/foundation/clickable.dart';
import 'package:poster/core/presentation/foundation/profile_cover_stub.dart';
import 'package:poster/core/presentation/post/post_shape.dart';
import 'package:poster/core/presentation/theme/app.dart';
import 'package:poster/core/presentation/theme/images.dart';

final class PostBlock extends StatelessWidget {
final class PostItem extends StatelessWidget {
final Post post;
final void Function() onLike;
final void Function() onShare;

const PostBlock({
const PostItem({
super.key,
required this.post,
required this.onLike,
required this.onShare,
});

@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();

return Container(
width: double.infinity,
constraints: BoxConstraints(
maxWidth: theme.dimensions.size.extraEnormous,
),
padding: EdgeInsets.only(
top: theme.dimensions.padding.medium,
left: theme.dimensions.padding.medium,
right: theme.dimensions.padding.medium,
),
decoration: BoxDecoration(
color: theme.colors.background.post,
borderRadius: BorderRadius.all(
Radius.circular(theme.dimensions.radius.small),
)
),
return PostShape(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down Expand Up @@ -65,7 +53,7 @@ final class PostBlock extends StatelessWidget {
avatarSize: Size(
theme.dimensions.size.small,
theme.dimensions.size.small,
)
),
),

SizedBox(width: theme.dimensions.padding.medium),
Expand Down Expand Up @@ -101,14 +89,12 @@ final class PostBlock extends StatelessWidget {
),
);

Widget ActionsRow({
required AppTheme theme,
}) => Row(
Widget ActionsRow({required AppTheme theme}) => Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AppClickable(
onClick: () => print('TODO: Share'),
onClick: onShare,
border: const CircleBorder(),
child: Image.asset(
AppImages.loadPng('ic_share').value,
Expand Down
34 changes: 18 additions & 16 deletions lib/core/presentation/post/post_paging_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:poster/core/domain/post/entity/post.dart';
import 'package:poster/core/presentation/foundation/app_empty_stub.dart';
import 'package:poster/core/presentation/post/post_block.dart';
import 'package:poster/core/presentation/post/post_item.dart';
import 'package:poster/core/presentation/post/post_shimmer.dart';
import 'package:poster/core/presentation/theme/app.dart';
import 'package:super_paging/super_paging.dart';

final class PostPagingList extends StatelessWidget {
static const _shimmerItemsAmount = 10;

final Pager<int, Post> pager;
final void Function(String postId) onPostLike;
final void Function(String postText) onPostShare;

const PostPagingList({
super.key,
required this.pager,
required this.onPostLike,
required this.onPostShare,
});

@override
Expand All @@ -28,14 +33,13 @@ final class PostPagingList extends StatelessWidget {
right: theme.dimensions.padding.medium,
left: theme.dimensions.padding.medium
),
separatorBuilder: (context, index) => SizedBox(
height: theme.dimensions.padding.big,
),
separatorBuilder: (_, __) => ItemSeparator(theme: theme),
itemBuilder: (context, index) {
final post = pager.items.elementAt(index);
return PostBlock(
return PostItem(
post: post,
onLike: () => onPostLike(post.id),
onShare: () => onPostShare(post.data.text),
);
},
emptyBuilder: (_) => const AppEmptyStub(),
Expand All @@ -44,15 +48,13 @@ final class PostPagingList extends StatelessWidget {
);
}

Widget LoadingStub({required AppTheme theme}) => Container(
alignment: Alignment.center,
width: double.infinity,
height: double.infinity,
padding: EdgeInsets.only(
bottom: theme.dimensions.padding.extraEnormous,
),
child: CircularProgressIndicator(
color: theme.colors.indicator.primary,
),
);
Widget LoadingStub({required AppTheme theme}) =>
ListView.separated(
itemCount: _shimmerItemsAmount,
itemBuilder: (_, __) => const PostShimmer(),
separatorBuilder: (_, __) => ItemSeparator(theme: theme),
);

Widget ItemSeparator({required AppTheme theme}) =>
SizedBox(height: theme.dimensions.padding.big);
}
32 changes: 32 additions & 0 deletions lib/core/presentation/post/post_shape.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:poster/core/presentation/theme/mod.dart';

final class PostShape extends StatelessWidget {
final Widget child;
const PostShape({super.key, required this.child});

@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();

return Container(
width: double.infinity,
constraints: BoxConstraints(
maxWidth: theme.dimensions.size.extraEnormous,
),
padding: EdgeInsets.only(
top: theme.dimensions.padding.medium,
left: theme.dimensions.padding.medium,
right: theme.dimensions.padding.medium,
),
decoration: BoxDecoration(
color: theme.colors.background.post,
borderRadius: BorderRadius.all(
Radius.circular(theme.dimensions.radius.small),
),
),
child: child,
);
}
}
28 changes: 28 additions & 0 deletions lib/core/presentation/post/post_shimmer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:poster/core/presentation/post/post_shape.dart';
import 'package:poster/core/presentation/theme/app.dart';
import 'package:shimmer/shimmer.dart';

final class PostShimmer extends StatelessWidget {
static const _height = 177.0;

const PostShimmer({super.key});

@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();

return Shimmer.fromColors(
baseColor: theme.colors.background.post,
highlightColor: theme.colors.background.primary,
child: const PostShape(
child: SizedBox(
width: double.infinity,
height: _height,
),
),
);
}
}
1 change: 1 addition & 0 deletions lib/feature/feed/di/feed_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extension FeedModule on GetIt {
),
provideSingleton<FeedBlocFactory>(() => FeedBlocFactory(
profileChangesUseCase: this(),
likeUseCase: this(),
feedPostPagingSourceFactory: this(),
)),
];
Expand Down
27 changes: 21 additions & 6 deletions lib/feature/feed/presentation/bloc/feed_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:poster/core/domain/paging/paging_config.dart';
import 'package:poster/core/domain/post/entity/post.dart';
import 'package:poster/core/domain/post/use_case/like_use_case.dart';
import 'package:poster/core/domain/profile/use_case/subscribe_on_profile_changes_use_case.dart';
import 'package:poster/core/presentation/foundation/ui_state.dart';
import 'package:poster/feature/feed/data/feed_post_paging_source_factory.dart';
Expand All @@ -13,6 +14,7 @@ final class FeedBloc extends Bloc<FeedEvent, FeedState> {

FeedBloc({
required SubscribeOnProfileChangesUseCase profileChangesUseCase,
required LikeUseCase likeUseCase,
required FeedPostPagingSourceFactory pagingSourceFactory,
}) : pager = paging_lib.Pager(
config: const paging_lib.PagingConfig(pageSize: PagingConfig.defaultPageSize),
Expand All @@ -27,13 +29,26 @@ final class FeedBloc extends Bloc<FeedEvent, FeedState> {

on<Like>(
(event, emit) async {
final username = state.profileState.getOrNull?.username;
if (username == null) return;
final email = state.profileState.getOrNull?.email;
if (email == null) return;

// TODO: like
//await postRepository.likePost(username: username, messageId: event.postId);
// TODO: error handling
add(Refresh());
likeUseCase.switchLikeForPost(
email: email,
postId: event.postId,
onSuccess: (_) {
// TODO: Success snackbar
add(Refresh());
},
onError: (_) {
// TODO: Error Snackbar
},
);
}
);

on<Share>(
(event, emit) async {
// TODO: Share
}
);

Expand Down
Loading

0 comments on commit 8c5de49

Please sign in to comment.