Add followers/following screen and comprehensive privacy settings

Features:
- Create FollowersFollowingScreen with tabs for followers/following lists
- Make follower/following counts tappable on profile to navigate to connections
- Create comprehensive PrivacySettingsScreen with sections for:
  - Account privacy (private account toggle)
  - Post visibility defaults
  - Interaction controls (who can message/comment)
  - Discovery settings (search visibility)
  - Circle (close friends) management placeholder
  - Data export and blocked users
- Update ProfilePrivacySettings model with additional fields
- Connect to new backend API endpoints
This commit is contained in:
Patrick Britton 2026-02-04 16:46:20 -06:00
parent 9fec6754d9
commit adeffe691e
4 changed files with 1085 additions and 29 deletions

View file

@ -1,31 +1,51 @@
class ProfilePrivacySettings {
final String userId;
final String profileVisibility;
final String postsVisibility;
final String savedVisibility;
final String followRequestPolicy;
final String defaultPostVisibility;
final bool isPrivateProfile;
String userId;
String profileVisibility;
String postsVisibility;
String savedVisibility;
String followRequestPolicy;
String defaultVisibility;
bool isPrivate;
bool allowChains;
String whoCanMessage;
String whoCanComment;
bool showActivityStatus;
bool showInSearch;
bool showInSuggestions;
const ProfilePrivacySettings({
required this.userId,
required this.profileVisibility,
required this.postsVisibility,
required this.savedVisibility,
required this.followRequestPolicy,
required this.defaultPostVisibility,
required this.isPrivateProfile,
ProfilePrivacySettings({
this.userId = '',
this.profileVisibility = 'public',
this.postsVisibility = 'public',
this.savedVisibility = 'private',
this.followRequestPolicy = 'everyone',
this.defaultVisibility = 'public',
this.isPrivate = false,
this.allowChains = true,
this.whoCanMessage = 'everyone',
this.whoCanComment = 'everyone',
this.showActivityStatus = true,
this.showInSearch = true,
this.showInSuggestions = true,
});
factory ProfilePrivacySettings.fromJson(Map<String, dynamic> json) {
return ProfilePrivacySettings(
userId: json['user_id'] as String,
userId: json['user_id'] as String? ?? '',
profileVisibility: json['profile_visibility'] as String? ?? 'public',
postsVisibility: json['posts_visibility'] as String? ?? 'public',
savedVisibility: json['saved_visibility'] as String? ?? 'private',
followRequestPolicy: json['follow_request_policy'] as String? ?? 'everyone',
defaultPostVisibility: json['default_post_visibility'] as String? ?? 'public',
isPrivateProfile: json['is_private_profile'] as bool? ?? false,
defaultVisibility: json['default_post_visibility'] as String? ??
json['default_visibility'] as String? ?? 'public',
isPrivate: json['is_private_profile'] as bool? ??
json['is_private'] as bool? ?? false,
allowChains: json['allow_chains'] as bool? ?? true,
whoCanMessage: json['who_can_message'] as String? ?? 'everyone',
whoCanComment: json['who_can_comment'] as String? ?? 'everyone',
showActivityStatus: json['show_activity_status'] as bool? ?? true,
showInSearch: json['show_in_search'] as bool? ?? true,
showInSuggestions: json['show_in_suggestions'] as bool? ?? true,
);
}
@ -36,8 +56,14 @@ class ProfilePrivacySettings {
'posts_visibility': postsVisibility,
'saved_visibility': savedVisibility,
'follow_request_policy': followRequestPolicy,
'default_post_visibility': defaultPostVisibility,
'is_private_profile': isPrivateProfile,
'default_post_visibility': defaultVisibility,
'is_private_profile': isPrivate,
'allow_chains': allowChains,
'who_can_message': whoCanMessage,
'who_can_comment': whoCanComment,
'show_activity_status': showActivityStatus,
'show_in_search': showInSearch,
'show_in_suggestions': showInSuggestions,
};
}
@ -46,8 +72,14 @@ class ProfilePrivacySettings {
String? postsVisibility,
String? savedVisibility,
String? followRequestPolicy,
String? defaultPostVisibility,
bool? isPrivateProfile,
String? defaultVisibility,
bool? isPrivate,
bool? allowChains,
String? whoCanMessage,
String? whoCanComment,
bool? showActivityStatus,
bool? showInSearch,
bool? showInSuggestions,
}) {
return ProfilePrivacySettings(
userId: userId,
@ -55,8 +87,14 @@ class ProfilePrivacySettings {
postsVisibility: postsVisibility ?? this.postsVisibility,
savedVisibility: savedVisibility ?? this.savedVisibility,
followRequestPolicy: followRequestPolicy ?? this.followRequestPolicy,
defaultPostVisibility: defaultPostVisibility ?? this.defaultPostVisibility,
isPrivateProfile: isPrivateProfile ?? this.isPrivateProfile,
defaultVisibility: defaultVisibility ?? this.defaultVisibility,
isPrivate: isPrivate ?? this.isPrivate,
allowChains: allowChains ?? this.allowChains,
whoCanMessage: whoCanMessage ?? this.whoCanMessage,
whoCanComment: whoCanComment ?? this.whoCanComment,
showActivityStatus: showActivityStatus ?? this.showActivityStatus,
showInSearch: showInSearch ?? this.showInSearch,
showInSuggestions: showInSuggestions ?? this.showInSuggestions,
);
}
@ -67,8 +105,18 @@ class ProfilePrivacySettings {
postsVisibility: 'public',
savedVisibility: 'private',
followRequestPolicy: 'everyone',
defaultPostVisibility: 'public',
isPrivateProfile: false,
defaultVisibility: 'public',
isPrivate: false,
allowChains: true,
whoCanMessage: 'everyone',
whoCanComment: 'everyone',
showActivityStatus: true,
showInSearch: true,
showInSuggestions: true,
);
}
// Legacy getters for backwards compatibility
String get defaultPostVisibility => defaultVisibility;
bool get isPrivateProfile => isPrivate;
}

View file

@ -0,0 +1,417 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../theme/app_theme.dart';
import '../../widgets/app_scaffold.dart';
import '../../widgets/media/signed_media_image.dart';
import '../../services/api_service.dart';
import 'viewable_profile_screen.dart';
/// Screen to manage followers and following with tabbed interface
class FollowersFollowingScreen extends ConsumerStatefulWidget {
final String userId;
final int initialTabIndex; // 0 = Followers, 1 = Following
const FollowersFollowingScreen({
super.key,
required this.userId,
this.initialTabIndex = 0,
});
@override
ConsumerState<FollowersFollowingScreen> createState() =>
_FollowersFollowingScreenState();
}
class _FollowersFollowingScreenState
extends ConsumerState<FollowersFollowingScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<UserListItem> _followers = [];
List<UserListItem> _following = [];
bool _isLoadingFollowers = false;
bool _isLoadingFollowing = false;
String? _followersError;
String? _followingError;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 2,
vsync: this,
initialIndex: widget.initialTabIndex,
);
_loadFollowers();
_loadFollowing();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _setStateIfMounted(VoidCallback fn) {
if (!mounted) return;
setState(fn);
}
Future<void> _loadFollowers() async {
_setStateIfMounted(() {
_isLoadingFollowers = true;
_followersError = null;
});
try {
final api = ref.read(apiServiceProvider);
final data = await api.callGoApi(
'/users/${widget.userId}/followers',
queryParams: {'limit': '50', 'offset': '0'},
);
final List<UserListItem> users = ((data['followers'] ?? []) as List)
.map((json) => UserListItem.fromJson(json))
.toList();
_setStateIfMounted(() {
_followers = users;
});
} catch (e) {
_setStateIfMounted(() {
_followersError = e.toString();
});
} finally {
_setStateIfMounted(() {
_isLoadingFollowers = false;
});
}
}
Future<void> _loadFollowing() async {
_setStateIfMounted(() {
_isLoadingFollowing = true;
_followingError = null;
});
try {
final api = ref.read(apiServiceProvider);
final data = await api.callGoApi(
'/users/${widget.userId}/following',
queryParams: {'limit': '50', 'offset': '0'},
);
final List<UserListItem> users = ((data['following'] ?? []) as List)
.map((json) => UserListItem.fromJson(json))
.toList();
_setStateIfMounted(() {
_following = users;
});
} catch (e) {
_setStateIfMounted(() {
_followingError = e.toString();
});
} finally {
_setStateIfMounted(() {
_isLoadingFollowing = false;
});
}
}
Future<void> _unfollowUser(String userId) async {
try {
final api = ref.read(apiServiceProvider);
await api.callGoApi('/users/$userId/follow', method: 'DELETE');
_setStateIfMounted(() {
_following.removeWhere((u) => u.id == userId);
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Unfollowed successfully')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to unfollow: $e')),
);
}
}
}
Future<void> _removeFollower(String userId) async {
// This would require a backend endpoint to remove a follower
// For now, show a placeholder message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Remove follower feature coming soon')),
);
}
}
@override
Widget build(BuildContext context) {
return AppScaffold(
title: 'Connections',
showBackButton: true,
body: Column(
children: [
// Tab Bar
Container(
color: AppTheme.surfaceColor,
child: TabBar(
controller: _tabController,
indicatorColor: AppTheme.primaryColor,
labelColor: AppTheme.textPrimary,
unselectedLabelColor: AppTheme.textSecondary,
tabs: [
Tab(text: 'Followers (${_followers.length})'),
Tab(text: 'Following (${_following.length})'),
],
),
),
// Tab Views
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildUserList(
users: _followers,
isLoading: _isLoadingFollowers,
error: _followersError,
onRefresh: _loadFollowers,
isFollowersList: true,
),
_buildUserList(
users: _following,
isLoading: _isLoadingFollowing,
error: _followingError,
onRefresh: _loadFollowing,
isFollowersList: false,
),
],
),
),
],
),
);
}
Widget _buildUserList({
required List<UserListItem> users,
required bool isLoading,
required String? error,
required Future<void> Function() onRefresh,
required bool isFollowersList,
}) {
if (isLoading && users.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (error != null && users.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: AppTheme.textSecondary),
const SizedBox(height: 16),
Text('Failed to load', style: AppTheme.bodyLarge),
const SizedBox(height: 8),
TextButton(
onPressed: onRefresh,
child: const Text('Retry'),
),
],
),
);
}
if (users.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isFollowersList ? Icons.people_outline : Icons.person_add_outlined,
size: 64,
color: AppTheme.textSecondary.withOpacity(0.5),
),
const SizedBox(height: 16),
Text(
isFollowersList ? 'No followers yet' : 'Not following anyone yet',
style: AppTheme.bodyLarge.copyWith(color: AppTheme.textSecondary),
),
],
),
);
}
return RefreshIndicator(
onRefresh: onRefresh,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return _UserListTile(
user: user,
isFollowersList: isFollowersList,
onTap: () => _navigateToProfile(user),
onAction: isFollowersList
? () => _removeFollower(user.id)
: () => _unfollowUser(user.id),
);
},
),
);
}
void _navigateToProfile(UserListItem user) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ViewableProfileScreen(handle: user.handle),
),
);
}
}
// =============================================================================
// USER LIST ITEM MODEL
// =============================================================================
class UserListItem {
final String id;
final String handle;
final String? displayName;
final String? avatarUrl;
final int harmonyScore;
final String? harmonyTier;
final DateTime? followedAt;
UserListItem({
required this.id,
required this.handle,
this.displayName,
this.avatarUrl,
this.harmonyScore = 0,
this.harmonyTier,
this.followedAt,
});
factory UserListItem.fromJson(Map<String, dynamic> json) {
return UserListItem(
id: json['id'] ?? '',
handle: json['handle'] ?? '',
displayName: json['display_name'],
avatarUrl: json['avatar_url'],
harmonyScore: json['harmony_score'] ?? 0,
harmonyTier: json['harmony_tier'],
followedAt: json['followed_at'] != null
? DateTime.tryParse(json['followed_at'])
: null,
);
}
}
// =============================================================================
// USER LIST TILE WIDGET
// =============================================================================
class _UserListTile extends StatelessWidget {
final UserListItem user;
final bool isFollowersList;
final VoidCallback onTap;
final VoidCallback onAction;
const _UserListTile({
required this.user,
required this.isFollowersList,
required this.onTap,
required this.onAction,
});
Color _getTierColor(String? tier) {
switch (tier?.toLowerCase()) {
case 'gold':
return const Color(0xFFFFD700);
case 'silver':
return const Color(0xFFC0C0C0);
case 'bronze':
return const Color(0xFFCD7F32);
default:
return AppTheme.textSecondary;
}
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
leading: CircleAvatar(
radius: 24,
backgroundColor: AppTheme.surfaceColor,
child: user.avatarUrl != null
? ClipOval(
child: SignedMediaImage(
imageUrl: user.avatarUrl!,
width: 48,
height: 48,
fit: BoxFit.cover,
),
)
: Text(
(user.displayName ?? user.handle).substring(0, 1).toUpperCase(),
style: AppTheme.headlineSmall,
),
),
title: Row(
children: [
Flexible(
child: Text(
user.displayName ?? user.handle,
style: AppTheme.bodyLarge.copyWith(fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
),
if (user.harmonyTier != null) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getTierColor(user.harmonyTier).withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
user.harmonyTier!,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: _getTierColor(user.harmonyTier),
),
),
),
],
],
),
subtitle: Text(
'@${user.handle}',
style: AppTheme.bodySmall.copyWith(color: AppTheme.textSecondary),
),
trailing: isFollowersList
? null // Followers don't have action button for now
: TextButton(
onPressed: onAction,
style: TextButton.styleFrom(
foregroundColor: AppTheme.textSecondary,
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: const Text('Unfollow'),
),
);
}
}

View file

@ -15,6 +15,7 @@ import '../../widgets/media/signed_media_image.dart';
import '../compose/compose_screen.dart';
import '../post/post_detail_screen.dart';
import 'profile_settings_screen.dart';
import 'followers_following_screen.dart';
enum ProfileFeedType { posts, saved, chained }
@ -1086,15 +1087,35 @@ class _ProfileHeader extends StatelessWidget {
children: [
_StatItem(label: 'Posts', value: stats.posts.toString()),
const SizedBox(width: AppTheme.spacingMd),
_StatItem(label: 'Followers', value: stats.followers.toString()),
_StatItem(
label: 'Followers',
value: stats.followers.toString(),
onTap: () => _navigateToConnections(0),
),
const SizedBox(width: AppTheme.spacingMd),
_StatItem(label: 'Following', value: stats.following.toString()),
_StatItem(
label: 'Following',
value: stats.following.toString(),
onTap: () => _navigateToConnections(1),
),
],
),
),
);
}
void _navigateToConnections(int tabIndex) {
if (_profile == null) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FollowersFollowingScreen(
userId: _profile!.id,
initialTabIndex: tabIndex,
),
),
);
}
LinearGradient _generateGradient(String seed) {
final hash = seed.hashCode.abs();
final hue = (hash % 360).toDouble();
@ -1117,15 +1138,17 @@ class _ProfileHeader extends StatelessWidget {
class _StatItem extends StatelessWidget {
final String label;
final String value;
final VoidCallback? onTap;
const _StatItem({
required this.label,
required this.value,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Column(
final content = Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
@ -1156,6 +1179,14 @@ class _StatItem extends StatelessWidget {
),
],
);
if (onTap != null) {
return GestureDetector(
onTap: onTap,
child: content,
);
}
return content;
}
}

View file

@ -0,0 +1,560 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../theme/app_theme.dart';
import '../../widgets/app_scaffold.dart';
import '../../services/api_service.dart';
import '../../models/profile_privacy_settings.dart';
/// Comprehensive privacy settings screen for managing account privacy
class PrivacySettingsScreen extends ConsumerStatefulWidget {
final ProfilePrivacySettings? initialSettings;
const PrivacySettingsScreen({
super.key,
this.initialSettings,
});
@override
ConsumerState<PrivacySettingsScreen> createState() =>
_PrivacySettingsScreenState();
}
class _PrivacySettingsScreenState
extends ConsumerState<PrivacySettingsScreen> {
late ProfilePrivacySettings _settings;
bool _isLoading = false;
bool _isSaving = false;
bool _hasChanges = false;
@override
void initState() {
super.initState();
_settings = widget.initialSettings ?? ProfilePrivacySettings();
if (widget.initialSettings == null) {
_loadSettings();
}
}
void _setStateIfMounted(VoidCallback fn) {
if (!mounted) return;
setState(fn);
}
Future<void> _loadSettings() async {
_setStateIfMounted(() => _isLoading = true);
try {
final api = ref.read(apiServiceProvider);
final data = await api.callGoApi('/settings/privacy');
_setStateIfMounted(() {
_settings = ProfilePrivacySettings.fromJson(data);
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load settings: $e')),
);
}
} finally {
_setStateIfMounted(() => _isLoading = false);
}
}
Future<void> _saveSettings() async {
_setStateIfMounted(() => _isSaving = true);
try {
final api = ref.read(apiServiceProvider);
await api.callGoApi(
'/settings/privacy',
method: 'PATCH',
body: _settings.toJson(),
);
_setStateIfMounted(() => _hasChanges = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Privacy settings saved'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop(_settings);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to save: $e')),
);
}
} finally {
_setStateIfMounted(() => _isSaving = false);
}
}
void _updateSetting(void Function() update) {
setState(() {
update();
_hasChanges = true;
});
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_hasChanges) {
final shouldLeave = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Unsaved Changes'),
content: const Text('You have unsaved changes. Discard them?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Discard'),
),
],
),
);
return shouldLeave ?? false;
}
return true;
},
child: AppScaffold(
title: 'Privacy Settings',
showBackButton: true,
actions: [
if (_hasChanges)
TextButton(
onPressed: _isSaving ? null : _saveSettings,
child: _isSaving
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Save', style: TextStyle(color: Colors.white)),
),
],
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSection(
title: 'Account Privacy',
icon: Icons.lock_outline,
children: [
_buildPrivateAccountTile(),
],
),
const SizedBox(height: 24),
_buildSection(
title: 'Post Visibility',
icon: Icons.visibility_outlined,
children: [
_buildDefaultVisibilityTile(),
_buildAllowChainsTile(),
],
),
const SizedBox(height: 24),
_buildSection(
title: 'Interactions',
icon: Icons.chat_bubble_outline,
children: [
_buildWhoCanMessageTile(),
_buildWhoCanCommentTile(),
_buildShowActivityStatusTile(),
],
),
const SizedBox(height: 24),
_buildSection(
title: 'Discovery',
icon: Icons.search,
children: [
_buildShowInSearchTile(),
_buildShowSuggestedTile(),
],
),
const SizedBox(height: 24),
_buildSection(
title: 'Circle (Close Friends)',
icon: Icons.favorite_outline,
children: [
_buildCircleInfoTile(),
_buildManageCircleTile(),
],
),
const SizedBox(height: 24),
_buildSection(
title: 'Data & Privacy',
icon: Icons.shield_outlined,
children: [
_buildExportDataTile(),
_buildBlockedUsersTile(),
],
),
const SizedBox(height: 48),
],
),
),
),
);
}
Widget _buildSection({
required String title,
required IconData icon,
required List<Widget> children,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 20, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
title,
style: AppTheme.headlineSmall.copyWith(
color: AppTheme.primaryColor,
fontSize: 16,
),
),
],
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: children,
),
),
],
);
}
Widget _buildPrivateAccountTile() {
return SwitchListTile(
title: const Text('Private Account'),
subtitle: const Text('Only approved followers can see your posts'),
value: _settings.isPrivate,
onChanged: (value) => _updateSetting(() => _settings.isPrivate = value),
activeColor: AppTheme.primaryColor,
);
}
Widget _buildDefaultVisibilityTile() {
return ListTile(
title: const Text('Default Post Visibility'),
subtitle: Text(_getVisibilityLabel(_settings.defaultVisibility)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showVisibilityPicker(),
);
}
Widget _buildAllowChainsTile() {
return SwitchListTile(
title: const Text('Allow Chains'),
subtitle: const Text('Let others add to your posts'),
value: _settings.allowChains,
onChanged: (value) => _updateSetting(() => _settings.allowChains = value),
activeColor: AppTheme.primaryColor,
);
}
Widget _buildWhoCanMessageTile() {
return ListTile(
title: const Text('Who Can Message Me'),
subtitle: Text(_getAudienceLabel(_settings.whoCanMessage)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showAudiencePicker(
title: 'Who Can Message You',
currentValue: _settings.whoCanMessage,
onChanged: (value) => _updateSetting(() => _settings.whoCanMessage = value),
),
);
}
Widget _buildWhoCanCommentTile() {
return ListTile(
title: const Text('Who Can Comment'),
subtitle: Text(_getAudienceLabel(_settings.whoCanComment)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showAudiencePicker(
title: 'Who Can Comment',
currentValue: _settings.whoCanComment,
onChanged: (value) => _updateSetting(() => _settings.whoCanComment = value),
),
);
}
Widget _buildShowActivityStatusTile() {
return SwitchListTile(
title: const Text('Show Activity Status'),
subtitle: const Text('Let others see when you\'re online'),
value: _settings.showActivityStatus,
onChanged: (value) =>
_updateSetting(() => _settings.showActivityStatus = value),
activeColor: AppTheme.primaryColor,
);
}
Widget _buildShowInSearchTile() {
return SwitchListTile(
title: const Text('Show in Search'),
subtitle: const Text('Allow others to find you in search'),
value: _settings.showInSearch,
onChanged: (value) =>
_updateSetting(() => _settings.showInSearch = value),
activeColor: AppTheme.primaryColor,
);
}
Widget _buildShowSuggestedTile() {
return SwitchListTile(
title: const Text('Show in Suggestions'),
subtitle: const Text('Appear in "Suggested for You"'),
value: _settings.showInSuggestions,
onChanged: (value) =>
_updateSetting(() => _settings.showInSuggestions = value),
activeColor: AppTheme.primaryColor,
);
}
Widget _buildCircleInfoTile() {
return const ListTile(
leading: Icon(Icons.info_outline, color: AppTheme.textSecondary),
title: Text('About Circle'),
subtitle: Text(
'Share posts with only your closest friends. '
'Circle members see posts marked "Circle" visibility.',
),
);
}
Widget _buildManageCircleTile() {
return ListTile(
title: const Text('Manage Circle Members'),
subtitle: const Text('Add or remove close friends'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: Navigate to circle management screen
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Circle management coming soon')),
);
},
);
}
Widget _buildExportDataTile() {
return ListTile(
title: const Text('Export My Data'),
subtitle: const Text('Download your profile, posts, and connections'),
trailing: const Icon(Icons.download_outlined),
onTap: () => _exportData(),
);
}
Widget _buildBlockedUsersTile() {
return ListTile(
title: const Text('Blocked Users'),
subtitle: const Text('Manage users you\'ve blocked'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: Navigate to blocked users screen
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Blocked users screen coming soon')),
);
},
);
}
String _getVisibilityLabel(String visibility) {
switch (visibility) {
case 'public':
return 'Public - Anyone can see';
case 'followers':
return 'Followers Only';
case 'circle':
return 'Circle Only';
default:
return visibility;
}
}
String _getAudienceLabel(String audience) {
switch (audience) {
case 'everyone':
return 'Everyone';
case 'followers':
return 'Followers Only';
case 'mutuals':
return 'Mutual Follows Only';
case 'nobody':
return 'Nobody';
default:
return audience;
}
}
void _showVisibilityPicker() {
showModalBottomSheet(
context: context,
backgroundColor: AppTheme.surfaceColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
Text('Default Post Visibility', style: AppTheme.headlineSmall),
const SizedBox(height: 16),
_buildVisibilityOption('public', 'Public', Icons.public),
_buildVisibilityOption('followers', 'Followers Only', Icons.people),
_buildVisibilityOption('circle', 'Circle Only', Icons.favorite),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildVisibilityOption(String value, String label, IconData icon) {
final isSelected = _settings.defaultVisibility == value;
return ListTile(
leading: Icon(icon, color: isSelected ? AppTheme.primaryColor : null),
title: Text(label),
trailing: isSelected ? const Icon(Icons.check, color: AppTheme.primaryColor) : null,
onTap: () {
_updateSetting(() => _settings.defaultVisibility = value);
Navigator.pop(context);
},
);
}
void _showAudiencePicker({
required String title,
required String currentValue,
required void Function(String) onChanged,
}) {
showModalBottomSheet(
context: context,
backgroundColor: AppTheme.surfaceColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
Text(title, style: AppTheme.headlineSmall),
const SizedBox(height: 16),
_buildAudienceOption('everyone', 'Everyone', Icons.public, currentValue, onChanged),
_buildAudienceOption('followers', 'Followers Only', Icons.people, currentValue, onChanged),
_buildAudienceOption('mutuals', 'Mutual Follows', Icons.sync_alt, currentValue, onChanged),
_buildAudienceOption('nobody', 'Nobody', Icons.block, currentValue, onChanged),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildAudienceOption(
String value,
String label,
IconData icon,
String currentValue,
void Function(String) onChanged,
) {
final isSelected = currentValue == value;
return ListTile(
leading: Icon(icon, color: isSelected ? AppTheme.primaryColor : null),
title: Text(label),
trailing: isSelected ? const Icon(Icons.check, color: AppTheme.primaryColor) : null,
onTap: () {
onChanged(value);
Navigator.pop(context);
},
);
}
Future<void> _exportData() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 20),
Text('Preparing your data...'),
],
),
),
);
try {
final api = ref.read(apiServiceProvider);
final data = await api.callGoApi('/users/me/export');
Navigator.pop(context); // Close loading dialog
// Show success and maybe allow saving
if (mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Export Ready'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Your data export includes:'),
const SizedBox(height: 12),
Text('• Profile information', style: AppTheme.bodyMedium),
Text('${data['posts']?.length ?? 0} posts', style: AppTheme.bodyMedium),
Text('${data['following']?.length ?? 0} connections', style: AppTheme.bodyMedium),
const SizedBox(height: 12),
const Text(
'The data has been prepared. In a production app, '
'this would be saved to your device.',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Done'),
),
],
),
);
}
} catch (e) {
Navigator.pop(context); // Close loading dialog
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Export failed: $e')),
);
}
}
}
}