feat: Phase 2.1 - Enhanced thread detail with highlighted OP, thread connectors, chain metadata, and improved reply composer

This commit is contained in:
Patrick Britton 2026-02-17 03:34:14 -06:00
parent bf4ac02d4b
commit 60a42c4704

View file

@ -81,6 +81,19 @@ class _GroupThreadDetailScreenState extends State<GroupThreadDetailScreen> {
if (mounted) setState(() => _sending = false); if (mounted) setState(() => _sending = false);
} }
int _uniqueParticipants() {
final authors = <String>{};
if (_thread != null) {
final a = _thread!['author_id']?.toString() ?? _thread!['author_handle']?.toString() ?? '';
if (a.isNotEmpty) authors.add(a);
}
for (final r in _replies) {
final a = r['author_id']?.toString() ?? r['author_handle']?.toString() ?? '';
if (a.isNotEmpty) authors.add(a);
}
return authors.length;
}
String _timeAgo(String? dateStr) { String _timeAgo(String? dateStr) {
if (dateStr == null) return ''; if (dateStr == null) return '';
try { try {
@ -114,8 +127,18 @@ class _GroupThreadDetailScreenState extends State<GroupThreadDetailScreen> {
child: ListView( child: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
// Thread body // Original post (highlighted)
if (_thread != null) ...[ if (_thread != null) ...[
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.cardSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppTheme.brightNavy.withValues(alpha: 0.25), width: 1.5),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text( Text(
_thread!['title'] as String? ?? '', _thread!['title'] as String? ?? '',
style: TextStyle(color: AppTheme.navyBlue, fontSize: 18, fontWeight: FontWeight.w700), style: TextStyle(color: AppTheme.navyBlue, fontSize: 18, fontWeight: FontWeight.w700),
@ -142,14 +165,29 @@ class _GroupThreadDetailScreenState extends State<GroupThreadDetailScreen> {
style: TextStyle(color: SojornColors.postContent, fontSize: 14, height: 1.5), style: TextStyle(color: SojornColors.postContent, fontSize: 14, height: 1.5),
), ),
], ],
],
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Divider(color: AppTheme.navyBlue.withValues(alpha: 0.08)), // Chain metadata
const SizedBox(height: 8), Row(
children: [
Icon(Icons.forum_outlined, size: 14, color: AppTheme.navyBlue.withValues(alpha: 0.5)),
const SizedBox(width: 6),
Text( Text(
'${_replies.length} ${_replies.length == 1 ? 'Reply' : 'Replies'}', '${_replies.length} ${_replies.length == 1 ? 'reply' : 'replies'}',
style: TextStyle(color: AppTheme.navyBlue, fontWeight: FontWeight.w600, fontSize: 13), style: TextStyle(color: AppTheme.navyBlue, fontWeight: FontWeight.w600, fontSize: 13),
), ),
const SizedBox(height: 8), const SizedBox(width: 12),
Icon(Icons.people_outline, size: 14, color: AppTheme.navyBlue.withValues(alpha: 0.5)),
const SizedBox(width: 4),
Text(
'${_uniqueParticipants()} participants',
style: TextStyle(color: SojornColors.textDisabled, fontSize: 12),
),
],
),
const SizedBox(height: 12),
], ],
if (widget.isEncrypted && _replies.isEmpty) if (widget.isEncrypted && _replies.isEmpty)
Padding( Padding(
@ -161,11 +199,13 @@ class _GroupThreadDetailScreenState extends State<GroupThreadDetailScreen> {
), ),
), ),
), ),
// Replies // Replies with thread connector
..._replies.map((reply) => _ReplyCard( for (int i = 0; i < _replies.length; i++)
reply: reply, _ReplyCard(
timeAgo: _timeAgo(reply['created_at']?.toString()), reply: _replies[i],
)), timeAgo: _timeAgo(_replies[i]['created_at']?.toString()),
showConnector: i < _replies.length - 1,
),
], ],
), ),
), ),
@ -184,7 +224,7 @@ class _GroupThreadDetailScreenState extends State<GroupThreadDetailScreen> {
controller: _replyCtrl, controller: _replyCtrl,
style: TextStyle(color: SojornColors.postContent, fontSize: 14), style: TextStyle(color: SojornColors.postContent, fontSize: 14),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Write a reply', hintText: 'Add to this chain',
hintStyle: TextStyle(color: SojornColors.textDisabled), hintStyle: TextStyle(color: SojornColors.textDisabled),
filled: true, fillColor: AppTheme.scaffoldBg, filled: true, fillColor: AppTheme.scaffoldBg,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
@ -216,7 +256,8 @@ class _GroupThreadDetailScreenState extends State<GroupThreadDetailScreen> {
class _ReplyCard extends StatelessWidget { class _ReplyCard extends StatelessWidget {
final Map<String, dynamic> reply; final Map<String, dynamic> reply;
final String timeAgo; final String timeAgo;
const _ReplyCard({required this.reply, required this.timeAgo}); final bool showConnector;
const _ReplyCard({required this.reply, required this.timeAgo, this.showConnector = false});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -225,7 +266,40 @@ class _ReplyCard extends StatelessWidget {
final avatarUrl = reply['author_avatar_url'] as String? ?? ''; final avatarUrl = reply['author_avatar_url'] as String? ?? '';
final body = reply['body'] as String? ?? ''; final body = reply['body'] as String? ?? '';
return Container( return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Thread connector line
SizedBox(
width: 20,
child: Column(
children: [
Container(
width: 2, height: 8,
color: AppTheme.navyBlue.withValues(alpha: 0.12),
),
Container(
width: 8, height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.navyBlue.withValues(alpha: 0.15),
),
),
if (showConnector)
Expanded(
child: Container(
width: 2,
color: AppTheme.navyBlue.withValues(alpha: 0.12),
),
),
],
),
),
const SizedBox(width: 6),
// Reply content
Expanded(
child: Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -255,6 +329,10 @@ class _ReplyCard extends StatelessWidget {
Text(body, style: TextStyle(color: SojornColors.postContent, fontSize: 13, height: 1.4)), Text(body, style: TextStyle(color: SojornColors.postContent, fontSize: 13, height: 1.4)),
], ],
), ),
),
),
],
),
); );
} }
} }