Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to view entire post as a different account #1409

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2341,6 +2341,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
Loading