Skip to content

Commit

Permalink
Optimistically mark replies as read (#1314)
Browse files Browse the repository at this point in the history
* Optimistically mark replies as read

* Fix localizations
  • Loading branch information
micahmo authored May 1, 2024
1 parent 6d325b7 commit 7293229
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 104 deletions.
46 changes: 30 additions & 16 deletions lib/inbox/bloc/inbox_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'package:stream_transform/stream_transform.dart';
import 'package:thunder/account/models/account.dart';
import 'package:thunder/core/auth/helpers/fetch_account.dart';
import 'package:thunder/core/singletons/lemmy_client.dart';
import 'package:thunder/utils/global_context.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

part 'inbox_event.dart';
part 'inbox_state.dart';
Expand All @@ -32,7 +34,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {
void _init() {
on<GetInboxEvent>(
_getInboxEvent,
transformer: throttleDroppable(throttleDuration),
transformer: restartable(),
);
on<MarkReplyAsReadEvent>(
_markReplyAsReadEvent,
Expand Down Expand Up @@ -72,7 +74,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;

if (event.reset) {
emit(state.copyWith(status: InboxStatus.loading));
emit(state.copyWith(status: InboxStatus.loading, errorMessage: ''));
// Fetch all the things
PrivateMessagesResponse privateMessagesResponse = await lemmy.run(
GetPrivateMessages(
Expand Down Expand Up @@ -116,7 +118,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {
status: InboxStatus.success,
privateMessages: cleanDeletedMessages(privateMessagesResponse.privateMessages),
mentions: cleanDeletedMentions(getPersonMentionsResponse.mentions),
replies: getRepliesResponse.replies,
replies: getRepliesResponse.replies.toList(), // Copy this list so that it is modifyable
showUnreadOnly: !event.showAll,
inboxMentionPage: 2,
inboxReplyPage: 2,
Expand All @@ -134,7 +136,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {

// Prevent duplicate requests if we're done fetching
if (state.hasReachedInboxReplyEnd && state.hasReachedInboxMentionEnd && state.hasReachedInboxPrivateMessageEnd) return;
emit(state.copyWith(status: InboxStatus.refreshing));
emit(state.copyWith(status: InboxStatus.refreshing, errorMessage: ''));

// Fetch all the things
PrivateMessagesResponse privateMessagesResponse = await lemmy.run(
Expand Down Expand Up @@ -213,7 +215,19 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {

Future<void> _markReplyAsReadEvent(MarkReplyAsReadEvent event, emit) async {
try {
emit(state.copyWith(status: InboxStatus.refreshing));
emit(state.copyWith(status: InboxStatus.refreshing, errorMessage: ''));

bool matchMarkedComment(CommentReplyView commentView) => commentView.commentReply.id == event.commentReplyId;

// Optimistically remove the reply from the list
// or change the status (depending on whether we're showing all)
final CommentReplyView commentReplyView = state.replies.firstWhere(matchMarkedComment);
int index = state.replies.indexOf(commentReplyView);
if (event.showAll) {
state.replies[index] = commentReplyView.copyWith(commentReply: commentReplyView.commentReply.copyWith(read: event.read));
} else if (event.read) {
state.replies.remove(commentReplyView);
}

Account? account = await fetchActiveProfileAccount();
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;
Expand All @@ -228,15 +242,13 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {
read: event.read,
));

// Remove the post from the current reply list, or just mark it as read
List<CommentReplyView> replies = List.from(state.replies);
bool matchMarkedComment(CommentReplyView commentView) => commentView.commentReply.id == response.commentReplyView.commentReply.id;
if (event.showAll) {
final CommentReplyView markedComment = replies.firstWhere(matchMarkedComment);
final int index = replies.indexOf(markedComment);
replies[index] = markedComment.copyWith(comment: response.commentReplyView.comment);
} else {
replies.removeWhere(matchMarkedComment);
if (response.commentReplyView.commentReply.read != event.read) {
return emit(
state.copyWith(
status: InboxStatus.failure,
errorMessage: event.read ? AppLocalizations.of(GlobalContext.context)!.errorMarkingReplyRead : AppLocalizations.of(GlobalContext.context)!.errorMarkingReplyUnread,
),
);
}

GetUnreadCountResponse getUnreadCountResponse = await lemmy.run(
Expand All @@ -249,7 +261,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {

return emit(state.copyWith(
status: InboxStatus.success,
replies: replies,
replies: state.replies,
totalUnreadCount: totalUnreadCount,
repliesUnreadCount: getUnreadCountResponse.replies,
mentionsUnreadCount: getUnreadCountResponse.mentions,
Expand All @@ -268,6 +280,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {
privateMessages: state.privateMessages,
mentions: state.mentions,
replies: state.replies,
errorMessage: '',
));

Account? account = await fetchActiveProfileAccount();
Expand All @@ -291,7 +304,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {

Future<void> _createCommentEvent(CreateInboxCommentReplyEvent event, Emitter<InboxState> emit) async {
try {
emit(state.copyWith(status: InboxStatus.refreshing));
emit(state.copyWith(status: InboxStatus.refreshing, errorMessage: ''));

Account? account = await fetchActiveProfileAccount();
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;
Expand All @@ -318,6 +331,7 @@ class InboxBloc extends Bloc<InboxEvent, InboxState> {
try {
emit(state.copyWith(
status: InboxStatus.refreshing,
errorMessage: '',
));
Account? account = await fetchActiveProfileAccount();
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;
Expand Down
22 changes: 10 additions & 12 deletions lib/inbox/pages/inbox_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import 'package:thunder/inbox/widgets/inbox_private_messages_view.dart';
import 'package:thunder/inbox/widgets/inbox_replies_view.dart';
import 'package:thunder/post/bloc/post_bloc.dart';
import 'package:thunder/shared/dialogs.dart';
import 'package:thunder/shared/error_message.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:thunder/shared/snackbar.dart';

enum InboxType { replies, mentions, messages }

Expand Down Expand Up @@ -162,22 +162,20 @@ class _InboxPageState extends State<InboxPage> {
);
case InboxStatus.refreshing:
case InboxStatus.success:
case InboxStatus.failure:
if (state.errorMessage?.isNotEmpty == true) {
showSnackbar(
state.errorMessage!,
trailingIcon: Icons.refresh_rounded,
trailingAction: () => context.read<InboxBloc>().add(GetInboxEvent(reset: true, showAll: showAll)),
);
}

if (inboxType == InboxType.mentions) return InboxMentionsView(mentions: state.mentions);
if (inboxType == InboxType.messages) return InboxPrivateMessagesView(privateMessages: state.privateMessages);
if (inboxType == InboxType.replies) return InboxRepliesView(replies: state.replies, showAll: showAll);
case InboxStatus.empty:
return Center(child: Text(l10n.emptyInbox));
case InboxStatus.failure:
return ErrorMessage(
message: state.errorMessage,
actions: [
(
text: l10n.refreshContent,
action: () => context.read<InboxBloc>().add(const GetInboxEvent()),
loading: false,
),
],
);
}

return Container();
Expand Down
130 changes: 54 additions & 76 deletions lib/inbox/widgets/inbox_replies_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ class InboxRepliesView extends StatefulWidget {
}

class _InboxRepliesViewState extends State<InboxRepliesView> {
List<int> inboxRepliesBeingMarkedAsRead = [];
List<int> inboxRepliesMarkedAsRead = [];

@override
void initState() {
super.initState();
Expand All @@ -54,82 +51,63 @@ class _InboxRepliesViewState extends State<InboxRepliesView> {
Widget build(BuildContext context) {
final DateTime now = DateTime.now().toUtc();

if (widget.replies.isEmpty || widget.replies.map((reply) => reply.commentReply.id).every((id) => inboxRepliesMarkedAsRead.contains(id))) {
return Align(alignment: Alignment.topCenter, heightFactor: (MediaQuery.of(context).size.height / 27), child: const Text('No replies'));
if (widget.replies.isEmpty) {
return Align(alignment: Alignment.topCenter, heightFactor: (MediaQuery.of(context).size.height / 27), child: Text(l10n.noReplies));
}

return BlocListener<InboxBloc, InboxState>(
listener: (context, state) {
if (state.status == InboxStatus.success && inboxRepliesBeingMarkedAsRead.isNotEmpty && state.inboxReplyMarkedAsRead != null) {
inboxRepliesBeingMarkedAsRead.remove(state.inboxReplyMarkedAsRead);
inboxRepliesMarkedAsRead.add(state.inboxReplyMarkedAsRead!);
setState(() {});
}
},
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.replies.length,
itemBuilder: (context, index) {
if (widget.showAll || !inboxRepliesMarkedAsRead.contains(widget.replies[index].commentReply.id)) {
return Column(
children: [
Divider(
height: 1.0,
thickness: 1.0,
color: ElevationOverlay.applySurfaceTint(
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceTint,
10,
),
return ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.replies.length,
itemBuilder: (context, index) {
return Column(
children: [
Divider(
height: 1.0,
thickness: 1.0,
color: ElevationOverlay.applySurfaceTint(
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceTint,
10,
),
),
CommentReference(
comment: widget.replies[index].toCommentView(),
now: now,
onVoteAction: (int commentId, int voteType) => context.read<PostBloc>().add(VoteCommentEvent(commentId: commentId, score: voteType)),
onSaveAction: (int commentId, bool save) => context.read<PostBloc>().add(SaveCommentEvent(commentId: commentId, save: save)),
onDeleteAction: (int commentId, bool deleted) => context.read<PostBloc>().add(DeleteCommentEvent(deleted: deleted, commentId: commentId)),
onReportAction: (int commentId) {
showReportCommentActionBottomSheet(
context,
commentId: commentId,
);
},
onReplyEditAction: (CommentView commentView, bool isEdit) async => navigateToCreateCommentPage(
context,
commentView: isEdit ? commentView : null,
parentCommentView: isEdit ? null : commentView,
onCommentSuccess: (commentView) {
context.read<PostBloc>().add(UpdateCommentEvent(commentView: commentView, isEdit: isEdit));
},
),
isOwnComment: widget.replies[index].creator.id == context.read<AuthBloc>().state.account?.userId,
child: IconButton(
onPressed: () {
context.read<InboxBloc>().add(MarkReplyAsReadEvent(commentReplyId: widget.replies[index].commentReply.id, read: !widget.replies[index].commentReply.read, showAll: widget.showAll));
},
icon: Icon(
Icons.check,
semanticLabel: l10n.markAsRead,
color: widget.replies[index].commentReply.read ? Colors.green : null,
),
CommentReference(
comment: widget.replies[index].toCommentView(),
now: now,
onVoteAction: (int commentId, int voteType) => context.read<PostBloc>().add(VoteCommentEvent(commentId: commentId, score: voteType)),
onSaveAction: (int commentId, bool save) => context.read<PostBloc>().add(SaveCommentEvent(commentId: commentId, save: save)),
onDeleteAction: (int commentId, bool deleted) => context.read<PostBloc>().add(DeleteCommentEvent(deleted: deleted, commentId: commentId)),
onReportAction: (int commentId) {
showReportCommentActionBottomSheet(
context,
commentId: commentId,
);
},
onReplyEditAction: (CommentView commentView, bool isEdit) async => navigateToCreateCommentPage(
context,
commentView: isEdit ? commentView : null,
parentCommentView: isEdit ? null : commentView,
onCommentSuccess: (commentView) {
context.read<PostBloc>().add(UpdateCommentEvent(commentView: commentView, isEdit: isEdit));
},
),
isOwnComment: widget.replies[index].creator.id == context.read<AuthBloc>().state.account?.userId,
child: widget.replies[index].commentReply.read == false && !inboxRepliesMarkedAsRead.contains(widget.replies[index].commentReply.id)
? !inboxRepliesBeingMarkedAsRead.contains(widget.replies[index].commentReply.id)
? IconButton(
onPressed: () {
setState(() => inboxRepliesBeingMarkedAsRead.add(widget.replies[index].commentReply.id));
context.read<InboxBloc>().add(MarkReplyAsReadEvent(commentReplyId: widget.replies[index].commentReply.id, read: true, showAll: widget.showAll));
},
icon: const Icon(
Icons.check,
semanticLabel: 'Mark as read',
),
visualDensity: VisualDensity.compact,
)
: const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator()),
)
: null,
),
],
);
}
return Container();
},
),
visualDensity: VisualDensity.compact,
),
),
],
);
},
);
}

Expand Down
16 changes: 16 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,14 @@
"@endSearch": {},
"errorDownloadingMedia": "Could not download the media file to share: {errorMessage}",
"@errorDownloadingMedia": {},
"errorMarkingReplyRead": "There was an error marking the reply as read.",
"@errorMarkingReplyRead": {
"description": "Error message for marking a reply read"
},
"errorMarkingReplyUnread": "There was an error marking the reply as unread.",
"@errorMarkingReplyUnread": {
"description": "Error message for marking a reply unread"
},
"exceptionProcessingUri": "An error occurred while processing the link. It may not be available on your instance.",
"@exceptionProcessingUri": {
"description": "An unspecified error during link processing."
Expand Down Expand Up @@ -929,6 +937,10 @@
"@markAllAsRead": {
"description": "The mark all as read action"
},
"markAsRead": "Mark as read",
"@markAsRead": {
"description": "Label for the action to mark a message as read"
},
"markPostAsReadOnMediaView": "Mark Read After Viewing Media",
"@markPostAsReadOnMediaView": {
"description": "Toggle to mark posts as read after viewing media."
Expand Down Expand Up @@ -1085,6 +1097,10 @@
},
"noPostsFound": "No posts found.",
"@noPostsFound": {},
"noReplies": "No replies",
"@noReplies": {
"description": "Label for when there are no replies in the list"
},
"noResultsFound": "No results found.",
"@noResultsFound": {},
"noSubscriptions": "No Subscriptions",
Expand Down

0 comments on commit 7293229

Please sign in to comment.