Skip to content

Commit

Permalink
adjusted the logic for determining read posts
Browse files Browse the repository at this point in the history
  • Loading branch information
hjiangsu committed Mar 4, 2024
1 parent 441de34 commit fcc1737
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 47 deletions.
2 changes: 1 addition & 1 deletion lib/community/widgets/post_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class _PostCardState extends State<PostCard> {

return Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: (event) {
onPointerDown: (PointerDownEvent event) {
widget.onDownAction();
},
onPointerUp: (event) {
Expand Down
3 changes: 2 additions & 1 deletion lib/core/singletons/lemmy_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ enum LemmyFeature {
sortTypeControversial(0, 19, 0, preRelease: ["rc", "1"]),
sortTypeScaled(0, 19, 0, preRelease: ["rc", "1"]),
commentSortTypeControversial(0, 19, 0, preRelease: ["rc", "1"]),
blockInstance(0, 19, 0, preRelease: ["rc", "1"]);
blockInstance(0, 19, 0, preRelease: ["rc", "1"]),
multiRead(0, 19, 0, preRelease: ["rc", "1"]);

final int major;
final int minor;
Expand Down
10 changes: 4 additions & 6 deletions lib/feed/bloc/feed_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {

/// Handles post related actions on a given item within the feed
Future<void> _onFeedItemActioned(FeedItemActionedEvent event, Emitter<FeedState> emit) async {
assert(!(event.postViewMedia == null && event.postId == null));
assert(!(event.postViewMedia == null && event.postId == null && event.postIds == null));
emit(state.copyWith(status: FeedStatus.fetching));

// TODO: Check if the current account has permission to perform the PostAction
Expand Down Expand Up @@ -210,7 +210,8 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {
}
case PostAction.multiRead:
List<int> eventPostIds = event.postIds ?? [];
if (eventPostIds.length > 0) {

if (eventPostIds.isNotEmpty) {
// Optimistically read the posts
List<int> existingPostViewMediaIndexes = [];
List<int> postIds = [];
Expand All @@ -225,9 +226,6 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {
}
}

// Give a slight delay to have the UI perform any navigation first
await Future.delayed(const Duration(milliseconds: 250));

try {
for (int i = 0; i < existingPostViewMediaIndexes.length; i++) {
PostView updatedPostView = optimisticallyReadPost(postViewMedias[i], event.value);
Expand All @@ -239,7 +237,7 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {
emit(state.copyWith(status: FeedStatus.fetching));

List<int> failed = await markPostsAsRead(postIds, event.value);
if (failed.length == 0) return emit(state.copyWith(status: FeedStatus.success));
if (failed.isEmpty) return emit(state.copyWith(status: FeedStatus.success));

// Restore the original post contents if not successful
for (int i = 0; i < failed.length; i++) {
Expand Down
112 changes: 78 additions & 34 deletions lib/feed/view/feed_widget.dart
Original file line number Diff line number Diff line change
@@ -1,42 +1,77 @@
import 'dart:async';

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

import 'package:thunder/community/widgets/post_card.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/core/models/post_view_media.dart';
import 'package:thunder/feed/bloc/feed_bloc.dart';
import 'package:thunder/feed/view/feed_page.dart';
import 'package:thunder/post/enums/post_action.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';

class FeedPostList extends StatelessWidget {
class FeedPostList extends StatefulWidget {
/// Determines the number of columns to display
final bool tabletMode;

/// Determines whether to mark posts as read on scroll
final bool markPostReadOnScroll;

/// The list of posts that have been queued for removal using the dismiss read action
final List<int>? queuedForRemoval;

/// The list of posts to show on the feed
final List<PostViewMedia> postViewMedias;
int prevLastTappedIndex = -1;
int lastTappedIndex = 0;
List<int> markReadPostIds = [];

FeedPostList({
const FeedPostList({
super.key,
required this.postViewMedias,
required this.tabletMode,
required this.markPostReadOnScroll,
this.queuedForRemoval,
});

@override
State<FeedPostList> createState() => _FeedPostListState();
}

class _FeedPostListState extends State<FeedPostList> {
/// The index of the last tapped post.
/// This is used to calculate the read status of posts in the range [0, lastTappedIndex]
int lastTappedIndex = -1;

/// Whether the user is scrolling down or not. The logic for determining read posts will
/// only be applied when the user is scrolling down
bool isScrollingDown = false;

/// List of post ids to queue for being marked as read.
Set<int> markReadPostIds = <int>{};

/// List of post ids that have already previously been detected as read
Set<int> readPostIds = <int>{};

/// Timer for debouncing the read action
Timer? debounceTimer;

@override
void dispose() {
debounceTimer?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
final ThunderState thunderState = context.read<ThunderBloc>().state;
final FeedState state = context.read<FeedBloc>().state;
final bool isUserLoggedIn = context.read<AuthBloc>().state.isLoggedIn;
VisibilityDetectorController.instance.updateInterval = Duration.zero;

// Widget representing the list of posts on the feed
return SliverMasonryGrid.count(
crossAxisCount: tabletMode ? 2 : 1,
crossAxisCount: widget.tabletMode ? 2 : 1,
crossAxisSpacing: 40,
mainAxisSpacing: 0,
itemBuilder: (BuildContext context, int index) {
Expand All @@ -63,48 +98,57 @@ class FeedPostList extends StatelessWidget {
),
);
},
child: queuedForRemoval?.contains(postViewMedias[index].postView.post.id) != true
child: widget.queuedForRemoval?.contains(widget.postViewMedias[index].postView.post.id) != true
? VisibilityDetector(
key: Key('post-card-vis-' + index.toString()),
key: Key('post-card-vis-$index'),
onVisibilityChanged: (info) {
if (markPostReadOnScroll &&
isUserLoggedIn &&
index <= lastTappedIndex &&
postViewMedias[index].postView.read != true &&
lastTappedIndex > prevLastTappedIndex &&
info.visibleFraction <= 0 &&
!markReadPostIds.contains(postViewMedias[index].postView.post.id)) {
// Sometimes the event doesn't fire, so check all previous indexes up to the last one marked unread
List<int> toAdd = [postViewMedias[index].postView.post.id];
for (int i = index - 1; i >= 0; i--) {
if (postViewMedias[i].postView.read) break;
if (!markReadPostIds.contains(postViewMedias[index].postView.post.id)) {
toAdd.add(postViewMedias[i].postView.post.id);
if (!isUserLoggedIn || !widget.markPostReadOnScroll || !isScrollingDown) return;

if (index <= lastTappedIndex && info.visibleFraction == 0) {
for (int i = index; i >= 0; i--) {
// If we already checked this post's read status, or we already marked it as read, skip it
if (readPostIds.contains(widget.postViewMedias[i].postView.post.id)) continue;
if (markReadPostIds.contains(widget.postViewMedias[i].postView.post.id)) continue;

// Otherwise, check the post read status
if (widget.postViewMedias[i].postView.read == false) {
markReadPostIds.add(widget.postViewMedias[i].postView.post.id);
} else {
readPostIds.add(widget.postViewMedias[i].postView.post.id);
}
}
markReadPostIds = [...markReadPostIds, ...toAdd];

// Debounce the read action to account for quick scrolling. This reduces the number of times the read action is triggered
debounceTimer?.cancel();

debounceTimer = Timer(const Duration(milliseconds: 500), () {
if (markReadPostIds.isNotEmpty) {
context.read<FeedBloc>().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, value: true));
markReadPostIds = <int>{};
}
});
}
},
child: PostCard(
postViewMedia: postViewMedias[index],
postViewMedia: widget.postViewMedias[index],
communityMode: state.feedType == FeedType.community,
onVoteAction: (int voteType) {
context.read<FeedBloc>().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.vote, value: voteType));
context.read<FeedBloc>().add(FeedItemActionedEvent(postId: widget.postViewMedias[index].postView.post.id, postAction: PostAction.vote, value: voteType));
},
onSaveAction: (bool saved) {
context.read<FeedBloc>().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.save, value: saved));
context.read<FeedBloc>().add(FeedItemActionedEvent(postId: widget.postViewMedias[index].postView.post.id, postAction: PostAction.save, value: saved));
},
onReadAction: (bool read) {
context.read<FeedBloc>().add(FeedItemActionedEvent(postId: postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read));
context.read<FeedBloc>().add(FeedItemActionedEvent(postId: widget.postViewMedias[index].postView.post.id, postAction: PostAction.read, value: read));
},
onDownAction: () {
prevLastTappedIndex = lastTappedIndex;
lastTappedIndex = index;
if (lastTappedIndex != index) lastTappedIndex = index;
},
onUpAction: (double verticalDragDistance) {
if (markPostReadOnScroll && verticalDragDistance < 0 && markReadPostIds.length > 0) {
context.read<FeedBloc>().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, value: true));
markReadPostIds = [];
bool updatedIsScrollingDown = verticalDragDistance < 0;

if (isScrollingDown != updatedIsScrollingDown) {
isScrollingDown = updatedIsScrollingDown;
}
},
listingType: state.postListingType,
Expand All @@ -113,7 +157,7 @@ class FeedPostList extends StatelessWidget {
: null,
);
},
childCount: postViewMedias.length,
childCount: widget.postViewMedias.length,
);
}
}
1 change: 1 addition & 0 deletions lib/instance/pages/instance_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ class _InstancePageState extends State<InstancePage> {
),
if (viewType == SearchType.posts)
FeedPostList(
markPostReadOnScroll: false,
postViewMedias: state.posts ?? [],
tabletMode: tabletMode,
),
Expand Down
22 changes: 17 additions & 5 deletions lib/post/utils/post.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,30 @@ Future<List<int>> markPostsAsRead(List<int> postIds, bool read) async {
if (account?.jwt == null) throw Exception('User not logged in');

List<int> failed = [];
// TODO: Lemmy 0.19 support postIds in MarkPostAsRead
// Check what version the server is, and use that if we can
for (int i = 0; i < postIds.length; i++) {

if (LemmyClient.instance.supportsFeature(LemmyFeature.multiRead)) {
MarkPostAsReadResponse markPostAsReadResponse = await lemmy.run(MarkPostAsRead(
auth: account!.jwt!,
postId: postIds[i],
postIds: postIds,
read: read,
));

if (!markPostAsReadResponse.isSuccess()) {
failed.add(i);
failed = List<int>.generate(postIds.length, (index) => index);
}
} else {
for (int i = 0; i < postIds.length; i++) {
MarkPostAsReadResponse markPostAsReadResponse = await lemmy.run(MarkPostAsRead(
auth: account!.jwt!,
postId: postIds[i],
read: read,
));
if (!markPostAsReadResponse.isSuccess()) {
failed.add(i);
}
}
}

return failed;
}

Expand Down

0 comments on commit fcc1737

Please sign in to comment.