···
+
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
+
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
+
import 'package:share_plus/share_plus.dart';
import '../../constants/app_colors.dart';
import '../../models/comment.dart';
import '../../models/post.dart';
+
import '../../providers/auth_provider.dart';
import '../../providers/comments_provider.dart';
+
import '../../providers/vote_provider.dart';
+
import '../../utils/community_handle_utils.dart';
import '../../utils/error_messages.dart';
import '../../widgets/comment_thread.dart';
import '../../widgets/comments_header.dart';
+
import '../../widgets/icons/share_icon.dart';
import '../../widgets/loading_error_states.dart';
+
import '../../widgets/post_action_bar.dart';
import '../../widgets/post_card.dart';
···
Widget build(BuildContext context) {
backgroundColor: AppColors.background,
+
bottomNavigationBar: _buildActionBar(),
+
/// Build community title with avatar and handle
+
Widget _buildCommunityTitle() {
+
final community = widget.post.post.community;
+
final displayHandle = CommunityHandleUtils.formatHandleForDisplay(
+
mainAxisSize: MainAxisSize.min,
+
if (community.avatar != null && community.avatar!.isNotEmpty)
+
borderRadius: BorderRadius.circular(16),
+
child: CachedNetworkImage(
+
imageUrl: community.avatar!,
+
placeholder: (context, url) => _buildFallbackAvatar(community),
+
(context, url, error) => _buildFallbackAvatar(community),
+
_buildFallbackAvatar(community),
+
const SizedBox(width: 8),
+
// Community handle with styled parts
+
if (displayHandle != null)
+
Flexible(child: _buildStyledHandle(displayHandle))
+
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
+
overflow: TextOverflow.ellipsis,
+
/// Build styled community handle with color-coded parts
+
Widget _buildStyledHandle(String displayHandle) {
+
// Format: !gaming@coves.social
+
final atIndex = displayHandle.indexOf('@');
+
final communityPart = displayHandle.substring(0, atIndex);
+
final instancePart = displayHandle.substring(atIndex);
+
style: const TextStyle(
+
color: AppColors.communityName,
+
fontWeight: FontWeight.w600,
+
color: AppColors.textSecondary.withValues(alpha: 0.8),
+
fontWeight: FontWeight.w600,
+
overflow: TextOverflow.ellipsis,
+
/// Build fallback avatar with first letter
+
Widget _buildFallbackAvatar(CommunityRef community) {
+
final firstLetter = community.name.isNotEmpty ? community.name[0] : '?';
+
decoration: BoxDecoration(
+
color: AppColors.primary,
+
borderRadius: BorderRadius.circular(16),
+
firstLetter.toUpperCase(),
+
style: const TextStyle(
+
color: AppColors.textPrimary,
+
fontWeight: FontWeight.bold,
+
/// Handle share button tap
+
Future<void> _handleShare() async {
+
await HapticFeedback.lightImpact();
+
// TODO: Generate proper deep link URL when deep linking is implemented
+
final postUri = widget.post.post.uri;
+
final title = widget.post.post.title ?? 'Check out this post';
+
await Share.share('$title\n\n$postUri', subject: title);
+
/// Build bottom action bar with comment input and buttons
+
Widget _buildActionBar() {
+
return Consumer<VoteProvider>(
+
builder: (context, voteProvider, child) {
+
final isVoted = voteProvider.isLiked(widget.post.post.uri);
+
final adjustedScore = voteProvider.getAdjustedScore(
+
widget.post.post.stats.score,
+
// Create a modified post with adjusted score for display
+
final displayPost = FeedViewPost(
+
uri: widget.post.post.uri,
+
cid: widget.post.post.cid,
+
rkey: widget.post.post.rkey,
+
author: widget.post.post.author,
+
community: widget.post.post.community,
+
createdAt: widget.post.post.createdAt,
+
indexedAt: widget.post.post.indexedAt,
+
text: widget.post.post.text,
+
title: widget.post.post.title,
+
upvotes: widget.post.post.stats.upvotes,
+
downvotes: widget.post.post.stats.downvotes,
+
commentCount: widget.post.post.stats.commentCount,
+
embed: widget.post.post.embed,
+
facets: widget.post.post.facets,
+
reason: widget.post.reason,
+
// TODO: Open comment composer
+
ScaffoldMessenger.of(context).showSnackBar(
+
content: Text('Comment composer coming soon!'),
+
behavior: SnackBarBehavior.floating,
+
// Check authentication
+
final authProvider = context.read<AuthProvider>();
+
if (!authProvider.isAuthenticated) {
+
ScaffoldMessenger.of(context).showSnackBar(
+
content: Text('Sign in to vote on posts'),
+
behavior: SnackBarBehavior.floating,
+
// Light haptic feedback on both like and unlike
+
await HapticFeedback.lightImpact();
+
final messenger = ScaffoldMessenger.of(context);
+
await voteProvider.toggleVote(
+
postUri: widget.post.post.uri,
+
postCid: widget.post.post.cid,
+
} on Exception catch (e) {
+
messenger.showSnackBar(
+
content: Text('Failed to vote: $e'),
+
behavior: SnackBarBehavior.floating,
+
// TODO: Add save functionality
+
ScaffoldMessenger.of(context).showSnackBar(
+
content: Text('Save feature coming soon!'),
+
behavior: SnackBarBehavior.floating,
/// Build main content area
···
+
// Content with RefreshIndicator and floating SliverAppBar
color: AppColors.primary,
+
child: CustomScrollView(
controller: _scrollController,
+
// Floating app bar that hides on scroll down, shows on scroll up
+
backgroundColor: AppColors.background,
+
surfaceTintColor: Colors.transparent,
+
foregroundColor: AppColors.textPrimary,
+
title: _buildCommunityTitle(),
+
icon: const ShareIcon(color: AppColors.textPrimary),
+
onPressed: _handleShare,
+
// Post + comments + loading indicator
+
delegate: SliverChildBuilderDelegate(
+
// Reuse PostCard (hide comment button in detail view)
+
// Use ValueListenableBuilder to only rebuild when time changes
+
commentsProvider.currentTimeNotifier,
+
// Comments header with sort dropdown
+
commentCount: comments.length,
+
currentSort: _currentSort,
+
onSortChanged: _onSortChanged,
+
// Loading indicator or error at the end
+
if (index == comments.length + 1) {
+
return const InlineLoading();
+
message: ErrorMessages.getUserFriendly(error),
+
// Comment item - use existing CommentThread widget
+
final comment = comments[index - 1];
+
commentsProvider.currentTimeNotifier,
+
(isLoadingMore || error != null ? 1 : 0),
/// Post header widget that only rebuilds when time changes
···
/// Extracted to prevent unnecessary rebuilds when comment list changes.
/// Uses ValueListenableBuilder to listen only to time updates.
class _PostHeader extends StatelessWidget {
+
const _PostHeader({required this.post, required this.currentTimeNotifier});
final ValueNotifier<DateTime?> currentTimeNotifier;
···
currentTime: currentTime,
showCommentButton: false,