sojorn/sojorn_app/lib/screens/profile/following_screen.dart
2026-02-15 00:33:24 -06:00

562 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../theme/app_theme.dart';
import '../../theme/tokens.dart';
import '../../widgets/app_scaffold.dart';
import '../../widgets/media/signed_media_image.dart';
import 'viewable_profile_screen.dart';
import 'package:timeago/timeago.dart' as timeago;
/// Following screen with community management features
/// Shows followed users with tabs and sorting options
class FollowingScreen extends ConsumerStatefulWidget {
const FollowingScreen({super.key});
@override
ConsumerState<FollowingScreen> createState() => _FollowingScreenState();
}
// Tab filter options
enum FollowingTab {
all('All'),
vouched('Vouched'),
newFollows('New');
final String displayName;
const FollowingTab(this.displayName);
}
// Sort options for following list
enum FollowingSort {
highestHarmony('Highest Harmony'),
lastActive('Last Active'),
recentlyFollowed('Recently Followed');
final String displayName;
const FollowingSort(this.displayName);
}
class _FollowingScreenState extends ConsumerState<FollowingScreen> {
FollowingTab _activeTab = FollowingTab.all;
FollowingSort _activeSort = FollowingSort.highestHarmony;
List<FollowedUser> _followedUsers = [];
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_loadFollowing();
}
void _setStateIfMounted(VoidCallback fn) {
if (!mounted) return;
setState(fn);
}
Future<void> _loadFollowing() async {
_setStateIfMounted(() {
_isLoading = true;
_error = null;
});
try {
// For now, we'll use a mock implementation since we need an API endpoint
await Future.delayed(const Duration(milliseconds: 500));
_setStateIfMounted(() {
_followedUsers = _generateMockData();
});
} catch (e) {
_setStateIfMounted(() {
_error = e.toString();
});
} finally {
_setStateIfMounted(() {
_isLoading = false;
});
}
}
List<FollowedUser> _generateMockData() {
return [
FollowedUser(
id: '1',
handle: 'alice',
displayName: 'Alice Johnson',
avatarUrl: null,
harmonyScore: 92,
tier: 'trusted',
lastActive: DateTime.now().subtract(const Duration(hours: 1)),
latestPostBody: 'Just published my latest thoughts on...',
latestPostCreatedAt: DateTime.now().subtract(const Duration(hours: 2)),
followedAt: DateTime.now().subtract(const Duration(days: 30)),
),
FollowedUser(
id: '2',
handle: 'bob',
displayName: 'Bob Smith',
avatarUrl: null,
harmonyScore: 78,
tier: 'established',
lastActive: DateTime.now().subtract(const Duration(hours: 3)),
latestPostBody: 'Working on something exciting!',
latestPostCreatedAt: DateTime.now().subtract(const Duration(hours: 4)),
followedAt: DateTime.now().subtract(const Duration(days: 15)),
),
FollowedUser(
id: '3',
handle: 'carol',
displayName: 'Carol Williams',
avatarUrl: null,
harmonyScore: 85,
tier: 'trusted',
lastActive: DateTime.now().subtract(const Duration(minutes: 30)),
latestPostBody: 'Check out this amazing discovery...',
latestPostCreatedAt: DateTime.now().subtract(const Duration(hours: 1)),
followedAt: DateTime.now().subtract(const Duration(days: 5)),
),
FollowedUser(
id: '4',
handle: 'dave',
displayName: 'Dave Brown',
avatarUrl: null,
harmonyScore: 45,
tier: 'new',
lastActive: DateTime.now().subtract(const Duration(days: 1)),
latestPostBody: 'Hello sojorn! This is my first post.',
latestPostCreatedAt: DateTime.now().subtract(const Duration(days: 2)),
followedAt: DateTime.now().subtract(const Duration(days: 1)),
),
FollowedUser(
id: '5',
handle: 'eve',
displayName: 'Eve Davis',
avatarUrl: null,
harmonyScore: 88,
tier: 'trusted',
lastActive: DateTime.now().subtract(const Duration(minutes: 15)),
latestPostBody: 'The sunset today was absolutely beautiful.',
latestPostCreatedAt:
DateTime.now().subtract(const Duration(minutes: 20)),
followedAt: DateTime.now().subtract(const Duration(days: 60)),
),
];
}
List<FollowedUser> _getFilteredAndSortedUsers() {
List<FollowedUser> filtered = _followedUsers;
switch (_activeTab) {
case FollowingTab.all:
break;
case FollowingTab.vouched:
filtered = filtered.where((u) => u.tier == 'trusted').toList();
break;
case FollowingTab.newFollows:
final weekAgo = DateTime.now().subtract(const Duration(days: 7));
filtered =
filtered.where((u) => u.followedAt.isAfter(weekAgo)).toList();
break;
}
switch (_activeSort) {
case FollowingSort.highestHarmony:
filtered.sort((a, b) => b.harmonyScore.compareTo(a.harmonyScore));
break;
case FollowingSort.lastActive:
filtered.sort((a, b) => b.lastActive.compareTo(a.lastActive));
break;
case FollowingSort.recentlyFollowed:
filtered.sort((a, b) => b.followedAt.compareTo(a.followedAt));
break;
}
return filtered;
}
Color _getTierColor(String tier) {
switch (tier) {
case 'trusted':
return AppTheme.tierTrusted;
case 'established':
return AppTheme.tierEstablished;
default:
return AppTheme.tierNew;
}
}
@override
Widget build(BuildContext context) {
final filteredUsers = _getFilteredAndSortedUsers();
return AppScaffold(
title: 'Following',
body: Column(
children: [
_buildTabBar(),
_buildSortDropdown(),
Expanded(
child: _error != null
? _buildErrorState()
: _isLoading && _followedUsers.isEmpty
? const Center(child: CircularProgressIndicator())
: filteredUsers.isEmpty
? _buildEmptyState()
: _buildUserList(filteredUsers),
),
],
),
);
}
Widget _buildTabBar() {
return Container(
decoration: BoxDecoration(
color: AppTheme.cardSurface,
border: Border(
bottom: BorderSide(color: AppTheme.egyptianBlue.withValues(alpha: 0.3)),
),
),
child: TabBar(
tabs: FollowingTab.values.map((tab) {
return Tab(text: tab.displayName);
}).toList(),
labelColor: AppTheme.navyBlue,
unselectedLabelColor: AppTheme.egyptianBlue.withValues(alpha: 0.5),
indicatorColor: AppTheme.royalPurple,
indicatorWeight: 3,
onTap: (index) {
setState(() {
_activeTab = FollowingTab.values[index];
});
},
),
);
}
Widget _buildSortDropdown() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppTheme.scaffoldBg,
border: Border(
bottom: BorderSide(color: AppTheme.egyptianBlue.withValues(alpha: 0.2)),
),
),
child: Row(
children: [
Text(
'Sort by:',
style: AppTheme.labelSmall.copyWith(
color: AppTheme.egyptianBlue,
),
),
const SizedBox(width: 8),
DropdownButton<FollowingSort>(
value: _activeSort,
icon: Icon(
Icons.arrow_drop_down,
color: AppTheme.royalPurple,
),
elevation: 2,
style: AppTheme.labelSmall.copyWith(
color: AppTheme.navyBlue,
),
underline: Container(
height: 1,
color: AppTheme.royalPurple.withValues(alpha: 0.3),
),
onChanged: (FollowingSort? newValue) {
if (newValue != null) {
setState(() {
_activeSort = newValue;
});
}
},
items: FollowingSort.values.map((sort) {
return DropdownMenuItem<FollowingSort>(
value: sort,
child: Text(sort.displayName),
);
}).toList(),
),
],
),
);
}
Widget _buildUserList(List<FollowedUser> users) {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return _UserListItem(
user: user,
tierColor: _getTierColor(user.tier),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => UnifiedProfileScreen(handle: user.handle),
),
);
},
onPostTap: () {},
);
},
);
}
Widget _buildErrorState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_error ?? 'Something went wrong',
style: AppTheme.bodyMedium.copyWith(
color: AppTheme.error,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppTheme.spacingMd),
ElevatedButton(
onPressed: _loadFollowing,
child: const Text('Retry'),
),
],
),
);
}
Widget _buildEmptyState() {
String message;
switch (_activeTab) {
case FollowingTab.all:
message = "You're not following anyone yet.";
break;
case FollowingTab.vouched:
message = 'No vouched users in your following list.';
break;
case FollowingTab.newFollows:
message = 'No new follows in the last 7 days.';
break;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 64,
color: AppTheme.egyptianBlue.withValues(alpha: 0.5),
),
const SizedBox(height: AppTheme.spacingMd),
Text(
message,
style: AppTheme.bodyMedium.copyWith(
color: AppTheme.egyptianBlue,
),
textAlign: TextAlign.center,
),
],
),
);
}
}
/// Model representing a followed user with their latest post preview
class FollowedUser {
final String id;
final String handle;
final String displayName;
final String? avatarUrl;
final int harmonyScore;
final String tier;
final DateTime lastActive;
final String? latestPostBody;
final DateTime? latestPostCreatedAt;
final DateTime followedAt;
const FollowedUser({
required this.id,
required this.handle,
required this.displayName,
this.avatarUrl,
required this.harmonyScore,
required this.tier,
required this.lastActive,
this.latestPostBody,
this.latestPostCreatedAt,
required this.followedAt,
});
}
/// Individual user list item in the following screen
class _UserListItem extends StatelessWidget {
final FollowedUser user;
final Color tierColor;
final VoidCallback onTap;
final VoidCallback onPostTap;
const _UserListItem({
required this.user,
required this.tierColor,
required this.onTap,
required this.onPostTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 24,
backgroundColor: tierColor,
child: user.avatarUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(24),
child: SignedMediaImage(
url: user.avatarUrl!,
width: 48,
height: 48,
fit: BoxFit.cover,
),
)
: Text(
user.displayName.isNotEmpty
? user.displayName[0].toUpperCase()
: '?',
style: const TextStyle(
color: SojornColors.basicWhite,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
user.displayName,
style: TextStyle(
color: AppTheme.navyBlue,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(width: 8),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: tierColor,
shape: BoxShape.circle,
),
),
],
),
Text(
'@${user.handle}',
style: AppTheme.labelSmall.copyWith(
color: AppTheme.egyptianBlue,
),
),
const SizedBox(height: 6),
if (user.latestPostBody != null &&
user.latestPostBody!.isNotEmpty)
GestureDetector(
onTap: onPostTap,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.scaffoldBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.egyptianBlue.withValues(alpha: 0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_truncateText(user.latestPostBody!, 100),
style: AppTheme.bodyMedium.copyWith(
color: AppTheme.navyText,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
user.latestPostCreatedAt != null
? timeago.format(user.latestPostCreatedAt!)
: '',
style: AppTheme.labelSmall.copyWith(
color: AppTheme.egyptianBlue.withValues(alpha: 0.7),
),
),
],
),
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.access_time,
size: 12,
color: AppTheme.egyptianBlue.withValues(alpha: 0.6),
),
const SizedBox(width: 4),
Text(
'Active ${timeago.format(user.lastActive)}',
style: AppTheme.labelSmall.copyWith(
color: AppTheme.egyptianBlue.withValues(alpha: 0.7),
),
),
const SizedBox(width: 12),
Icon(
Icons.shield,
size: 12,
color: tierColor,
),
const SizedBox(width: 4),
Text(
'${user.harmonyScore}',
style: AppTheme.labelSmall.copyWith(
color: tierColor,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
Icon(
Icons.chevron_right,
color: AppTheme.egyptianBlue.withValues(alpha: 0.5),
),
],
),
),
);
}
String _truncateText(String text, int maxLength) {
if (text.length <= maxLength) return text;
return '${text.substring(0, maxLength)}...';
}
}