Skip to content

Commit

Permalink
Ability to view entire post as a different account (#1409)
Browse files Browse the repository at this point in the history
  • Loading branch information
micahmo authored Jun 5, 2024
1 parent f3586b9 commit 8c8ce53
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 188 deletions.
2 changes: 1 addition & 1 deletion lib/comment/view/create_comment_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ class _CreateCommentPageState extends State<CreateCommentPage> {
child: UserSelector(
profileModalHeading: l10n.selectAccountToCommentAs,
postActorId: widget.postViewMedia?.postView.post.apId,
onPostChanged: (postView) => postId = postView.post.id,
onPostChanged: (postViewMedia) => postId = postViewMedia.postView.post.id,
parentCommentActorId: widget.parentCommentView?.comment.apId,
onParentCommentChanged: (parentCommentView) {
postId = parentCommentView.post.id;
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2369,6 +2369,10 @@
"@viewOriginal": {
"description": "Action for viewing original text (as opposed to raw markdown)"
},
"viewPostAsDifferentAccount": "View post as different account",
"@viewPostAsDifferentAccount": {
"description": "Action for viewing a post as a different account"
},
"viewPostSource": "View post source",
"@viewPostSource": {
"description": "Menu item for viewing a post's source"
Expand Down
245 changes: 133 additions & 112 deletions lib/post/pages/post_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:thunder/account/models/account.dart';

import 'package:thunder/comment/enums/comment_action.dart';
import 'package:thunder/comment/models/comment_node.dart';
import 'package:thunder/comment/widgets/comment_card.dart';
import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/core/models/post_view_media.dart';
import 'package:thunder/post/bloc/post_bloc.dart';
import 'package:thunder/post/utils/comment_action_helpers.dart';
Expand All @@ -17,6 +19,7 @@ import 'package:thunder/shared/cross_posts.dart';
import 'package:thunder/shared/text/scalable_text.dart';
import 'package:thunder/shared/text/selectable_text_modal.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
import 'package:thunder/user/utils/restore_user.dart';

/// A page that displays the post details and comments associated with a post.
class PostPage extends StatefulWidget {
Expand Down Expand Up @@ -51,129 +54,147 @@ class _PostPageState extends State<PostPage> {
/// Keeps track of which comments should be collapsed. When a comment is collapsed, its child comments are hidden.
List<int> collapsedComments = [];

/// The active account that was selected when the page was opened
Account? originalUser;

/// Whether the user changed during the course of viewing the post
bool userChanged = false;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
final thunderState = context.read<ThunderBloc>().state;
bool hideTopBarOnScroll = thunderState.hideTopBarOnScroll;

return Scaffold(
body: SafeArea(
top: hideTopBarOnScroll, // Don't apply to top of screen to allow for the status bar colour to extend
bottom: false,
child: BlocConsumer<PostBloc, PostState>(
listener: (context, state) {
if (state.status == PostStatus.success && state.postView != widget.initialPostViewMedia) {
widget.onPostUpdated?.call(state.postView!);
setState(() {});
}
},
builder: (context, state) {
if (state.status == PostStatus.initial) {
// This is required because listener does not get called on initial build
context.read<PostBloc>().add(GetPostEvent(postView: widget.initialPostViewMedia));
}

List<CommentNode> flattenedComments = CommentNode.flattenCommentTree(state.commentNodes);

return CustomScrollView(
controller: scrollController,
slivers: [
PostPageAppBar(
viewSource: viewSource,
onViewSource: (value) => setState(() => viewSource = value),
onReset: () async => await scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOutCubicEmphasized),
onCreateCrossPost: () {
createCrossPost(
context,
title: state.postView?.postView.post.name ?? '',
url: state.postView?.postView.post.url,
text: state.postView?.postView.post.body,
postUrl: state.postView?.postView.post.apId,
);
},
onSelectText: () {
showSelectableTextModal(
context,
title: state.postView?.postView.post.name ?? '',
text: state.postView?.postView.post.body ?? '',
);
},
),
SliverToBoxAdapter(
child: PostSubview(
useDisplayNames: false,
postViewMedia: state.postView ?? widget.initialPostViewMedia,
crossPosts: state.crossPosts,
originalUser ??= context.read<AuthBloc>().state.account;

return PopScope(
onPopInvoked: (_) {
if (context.mounted) {
restoreUser(context, originalUser);
}
},
child: Scaffold(
body: SafeArea(
top: hideTopBarOnScroll, // Don't apply to top of screen to allow for the status bar colour to extend
bottom: false,
child: BlocConsumer<PostBloc, PostState>(
listener: (context, state) {
if (state.status == PostStatus.success && state.postView != widget.initialPostViewMedia) {
if (!userChanged) {
widget.onPostUpdated?.call(state.postView!);
}
setState(() {});
}
},
builder: (context, state) {
if (state.status == PostStatus.initial) {
// This is required because listener does not get called on initial build
context.read<PostBloc>().add(GetPostEvent(postView: widget.initialPostViewMedia));
}

List<CommentNode> flattenedComments = CommentNode.flattenCommentTree(state.commentNodes);

return CustomScrollView(
controller: scrollController,
slivers: [
PostPageAppBar(
viewSource: viewSource,
),
),
if (state.status == PostStatus.loading)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(child: CircularProgressIndicator()),
)
else
SuperSliverList.builder(
itemCount: flattenedComments.length,
listController: listController,
itemBuilder: (BuildContext context, int index) {
CommentNode commentNode = flattenedComments[index];
CommentView commentView = commentNode.commentView!;

bool isCollapsed = collapsedComments.contains(commentView.comment.id);
bool isHidden = collapsedComments.any((int id) => commentView.comment.path.contains('$id') && id != commentView.comment.id);

return CommentCard(
commentView: commentView,
replyCount: commentNode.replies.length,
level: commentNode.depth,
collapsed: isCollapsed,
hidden: isHidden,
onVoteAction: (int commentId, int voteType) => context.read<PostBloc>().add(CommentActionEvent(commentId: commentId, action: CommentAction.vote, value: voteType)),
onSaveAction: (int commentId, bool saved) => context.read<PostBloc>().add(CommentActionEvent(commentId: commentId, action: CommentAction.save, value: saved)),
onDeleteAction: (int commentId, bool deleted) => context.read<PostBloc>().add(CommentActionEvent(commentId: commentId, action: CommentAction.delete, value: deleted)),
onReplyEditAction: (CommentView commentView, bool isEdit) async => context.read<PostBloc>().add(CommentItemUpdatedEvent(commentView: commentView)),
onReportAction: (int commentId) => showReportCommentActionBottomSheet(context, commentId: commentId),
onCollapseCommentChange: (int commentId, bool collapsed) {
if (collapsed) {
collapsedComments.add(commentId);
} else {
collapsedComments.remove(commentId);
}

setState(() {});
},
onViewSource: (value) => setState(() => viewSource = value),
onReset: () async => await scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOutCubicEmphasized),
onCreateCrossPost: () {
createCrossPost(
context,
title: state.postView?.postView.post.name ?? '',
url: state.postView?.postView.post.url,
text: state.postView?.postView.post.body,
postUrl: state.postView?.postView.post.apId,
);
},
onSelectText: () {
showSelectableTextModal(
context,
title: state.postView?.postView.post.name ?? '',
text: state.postView?.postView.post.body ?? '',
);
},
onUserChanged: () => userChanged = true,
onPostChanged: (newPostViewMedia) => context.read<PostBloc>().add(GetPostEvent(postView: newPostViewMedia)),
),
SliverToBoxAdapter(
child: state.hasReachedCommentEnd == true
? Container(
color: theme.dividerColor.withOpacity(0.1),
padding: const EdgeInsets.symmetric(vertical: 32.0),
child: ScalableText(
flattenedComments.isEmpty ? l10n.noComments : l10n.reachedTheBottom,
fontScale: thunderState.metadataFontSizeScale,
textAlign: TextAlign.center,
style: theme.textTheme.titleSmall,
),
)
: Visibility(
visible: state.status == PostStatus.success,
child: Container(
height: 100.0,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: const CircularProgressIndicator(),
SliverToBoxAdapter(
child: PostSubview(
useDisplayNames: false,
postViewMedia: state.postView ?? widget.initialPostViewMedia,
crossPosts: state.crossPosts,
viewSource: viewSource,
),
),
if (state.status == PostStatus.loading)
const SliverFillRemaining(
hasScrollBody: false,
child: Center(child: CircularProgressIndicator()),
)
else
SuperSliverList.builder(
itemCount: flattenedComments.length,
listController: listController,
itemBuilder: (BuildContext context, int index) {
CommentNode commentNode = flattenedComments[index];
CommentView commentView = commentNode.commentView!;

bool isCollapsed = collapsedComments.contains(commentView.comment.id);
bool isHidden = collapsedComments.any((int id) => commentView.comment.path.contains('$id') && id != commentView.comment.id);

return CommentCard(
commentView: commentView,
replyCount: commentNode.replies.length,
level: commentNode.depth,
collapsed: isCollapsed,
hidden: isHidden,
onVoteAction: (int commentId, int voteType) => context.read<PostBloc>().add(CommentActionEvent(commentId: commentId, action: CommentAction.vote, value: voteType)),
onSaveAction: (int commentId, bool saved) => context.read<PostBloc>().add(CommentActionEvent(commentId: commentId, action: CommentAction.save, value: saved)),
onDeleteAction: (int commentId, bool deleted) => context.read<PostBloc>().add(CommentActionEvent(commentId: commentId, action: CommentAction.delete, value: deleted)),
onReplyEditAction: (CommentView commentView, bool isEdit) async => context.read<PostBloc>().add(CommentItemUpdatedEvent(commentView: commentView)),
onReportAction: (int commentId) => showReportCommentActionBottomSheet(context, commentId: commentId),
onCollapseCommentChange: (int commentId, bool collapsed) {
if (collapsed) {
collapsedComments.add(commentId);
} else {
collapsedComments.remove(commentId);
}

setState(() {});
},
);
},
),
SliverToBoxAdapter(
child: state.hasReachedCommentEnd == true
? Container(
color: theme.dividerColor.withOpacity(0.1),
padding: const EdgeInsets.symmetric(vertical: 32.0),
child: ScalableText(
flattenedComments.isEmpty ? l10n.noComments : l10n.reachedTheBottom,
fontScale: thunderState.metadataFontSizeScale,
textAlign: TextAlign.center,
style: theme.textTheme.titleSmall,
),
)
: Visibility(
visible: state.status == PostStatus.success,
child: Container(
height: 100.0,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: const CircularProgressIndicator(),
),
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 100)),
],
);
},
),
const SliverToBoxAdapter(child: SizedBox(height: 100)),
],
);
},
),
),
),
);
Expand Down
33 changes: 33 additions & 0 deletions lib/post/widgets/post_page_app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import 'package:flutter/services.dart';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:thunder/core/models/post_view_media.dart';

import 'package:thunder/core/singletons/lemmy_client.dart';
import 'package:thunder/post/bloc/post_bloc.dart';
import 'package:thunder/shared/comment_sort_picker.dart';
import 'package:thunder/shared/sort_picker.dart';
import 'package:thunder/shared/thunder_popup_menu_item.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
import 'package:thunder/user/widgets/user_selector.dart';

/// Holds the app bar for the post page.
class PostPageAppBar extends StatelessWidget {
Expand All @@ -28,13 +30,21 @@ class PostPageAppBar extends StatelessWidget {
/// Callback when the user wants to select text
final Function()? onSelectText;

/// Callback for when the user changes
final void Function()? onUserChanged;

/// Callback for when the post changes
final void Function(PostViewMedia)? onPostChanged;

const PostPageAppBar({
super.key,
this.viewSource = false,
this.onViewSource,
this.onReset,
this.onCreateCrossPost,
this.onSelectText,
this.onUserChanged,
this.onPostChanged,
});

@override
Expand All @@ -55,6 +65,8 @@ class PostPageAppBar extends StatelessWidget {
onReset: onReset,
onCreateCrossPost: onCreateCrossPost,
onSelectText: onSelectText,
onUserChanged: onUserChanged,
onPostChanged: onPostChanged,
)
],
);
Expand Down Expand Up @@ -107,13 +119,21 @@ class PostAppBarActions extends StatelessWidget {
/// Callback when the user wants to select text
final Function()? onSelectText;

/// Callback for when the user changes
final void Function()? onUserChanged;

/// Callback for when the post changes
final void Function(PostViewMedia)? onPostChanged;

const PostAppBarActions({
super.key,
this.viewSource = false,
this.onViewSource,
this.onReset,
this.onCreateCrossPost,
this.onSelectText,
this.onUserChanged,
this.onPostChanged,
});

@override
Expand Down Expand Up @@ -168,6 +188,19 @@ class PostAppBarActions extends StatelessWidget {
icon: Icons.select_all_rounded,
title: l10n.selectText,
),
ThunderPopupMenuItem(
onTap: () async {
await temporarilySwitchAccount(
context,
profileModalHeading: l10n.viewPostAsDifferentAccount,
onUserChanged: onUserChanged,
postActorId: context.read<PostBloc>().state.postView?.postView.post.apId,
onPostChanged: onPostChanged,
);
},
icon: Icons.people_alt_rounded,
title: l10n.viewPostAsDifferentAccount,
),
],
),
],
Expand Down
Loading

0 comments on commit 8c8ce53

Please sign in to comment.