mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 257 lines 8.4 kB view raw
1import 'package:atproto/com_atproto_moderation_defs.dart'; 2import 'package:atproto_core/atproto_core.dart'; 3import 'package:flutter/material.dart'; 4import 'package:flutter_bloc/flutter_bloc.dart'; 5import 'package:lazurite/features/profile/cubit/profile_action_cubit.dart'; 6import 'package:lazurite/shared/presentation/helpers/haptic_helper.dart'; 7import 'package:lazurite/core/theme/theme_extensions.dart'; 8 9enum _ReportType { post, actor } 10 11class ReportDialog extends StatefulWidget { 12 const ReportDialog.post({super.key, required this.postUri, required this.cid, required this.authorHandle}) 13 : actorDid = null, 14 _type = _ReportType.post; 15 16 const ReportDialog.actor({super.key, required String did, required this.authorHandle}) 17 : postUri = null, 18 cid = null, 19 actorDid = did, 20 _type = _ReportType.actor; 21 22 final AtUri? postUri; 23 final String? cid; 24 final String? actorDid; 25 final String authorHandle; 26 final _ReportType _type; 27 28 @override 29 State<ReportDialog> createState() => _ReportDialogState(); 30} 31 32class _ReportDialogState extends State<ReportDialog> { 33 final _reasonController = TextEditingController(); 34 ReasonType? _selectedReason; 35 bool _isSubmitting = false; 36 37 static const _reasonOptions = [ 38 ( 39 type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonSpam), 40 label: 'Spam', 41 description: 'Spam or unsolicited content', 42 ), 43 ( 44 type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonViolation), 45 label: 'Violation', 46 description: 'Violates community guidelines', 47 ), 48 ( 49 type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonMisleading), 50 label: 'Misleading', 51 description: 'Misleading or deceptive content', 52 ), 53 ( 54 type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonSexual), 55 label: 'Sexual Content', 56 description: 'Unwanted sexual content', 57 ), 58 ( 59 type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonRude), 60 label: 'Harassment', 61 description: 'Harassment or rude behaviour', 62 ), 63 ( 64 type: ReasonType.knownValue(data: KnownReasonType.comAtprotoModerationDefsReasonOther), 65 label: 'Other', 66 description: 'Other reason (requires explanation)', 67 ), 68 ]; 69 70 @override 71 void dispose() { 72 _reasonController.dispose(); 73 super.dispose(); 74 } 75 76 @override 77 Widget build(BuildContext context) { 78 final title = widget._type == _ReportType.post ? 'Report Post' : 'Report Account'; 79 final target = widget.authorHandle; 80 81 return AlertDialog( 82 title: Text('$title by @$target'), 83 content: SizedBox( 84 width: double.maxFinite, 85 child: Column( 86 mainAxisSize: MainAxisSize.min, 87 crossAxisAlignment: CrossAxisAlignment.start, 88 children: [ 89 Text('Reason', style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), 90 const SizedBox(height: 8), 91 ..._reasonOptions.map((option) => _buildReasonOption(option)), 92 if (_requiresExplanation) ...[ 93 const SizedBox(height: 16), 94 Text( 95 'Explanation (required)', 96 style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), 97 ), 98 const SizedBox(height: 8), 99 TextField( 100 controller: _reasonController, 101 maxLines: 3, 102 maxLength: 2000, 103 decoration: const InputDecoration( 104 hintText: 'Please explain why you are reporting this...', 105 border: OutlineInputBorder(), 106 ), 107 ), 108 ], 109 ], 110 ), 111 ), 112 actions: [ 113 TextButton(onPressed: _isSubmitting ? null : () => Navigator.pop(context), child: const Text('Cancel')), 114 FilledButton( 115 onPressed: _canSubmit ? _submit : null, 116 child: _isSubmitting 117 ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) 118 : const Text('Submit Report'), 119 ), 120 ], 121 ); 122 } 123 124 Widget _buildReasonOption(({ReasonType type, String label, String description}) option) { 125 final isSelected = _selectedReason == option.type; 126 final theme = Theme.of(context); 127 128 return InkWell( 129 onTap: _isSubmitting 130 ? null 131 : () { 132 HapticHelper.selectionClick(); 133 setState(() { 134 _selectedReason = option.type; 135 }); 136 }, 137 child: Padding( 138 padding: const EdgeInsets.symmetric(vertical: 8), 139 child: Row( 140 crossAxisAlignment: CrossAxisAlignment.start, 141 children: [ 142 Container( 143 width: 20, 144 height: 20, 145 decoration: BoxDecoration( 146 shape: BoxShape.circle, 147 border: Border.all(color: isSelected ? theme.colorScheme.primary : theme.colorScheme.outline, width: 2), 148 ), 149 child: isSelected 150 ? Center( 151 child: Container( 152 width: 10, 153 height: 10, 154 decoration: BoxDecoration(shape: BoxShape.circle, color: theme.colorScheme.primary), 155 ), 156 ) 157 : null, 158 ), 159 const SizedBox(width: 12), 160 Expanded( 161 child: Column( 162 crossAxisAlignment: CrossAxisAlignment.start, 163 children: [ 164 Text(option.label, style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500)), 165 Text( 166 option.description, 167 style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), 168 ), 169 ], 170 ), 171 ), 172 ], 173 ), 174 ), 175 ); 176 } 177 178 bool get _requiresExplanation { 179 if (_selectedReason == null) return false; 180 if (_selectedReason!.isNotKnownValue) return false; 181 return _selectedReason!.knownValue == KnownReasonType.comAtprotoModerationDefsReasonOther; 182 } 183 184 bool get _canSubmit { 185 if (_selectedReason == null) return false; 186 if (_requiresExplanation && _reasonController.text.trim().isEmpty) return false; 187 return !_isSubmitting; 188 } 189 190 Future<void> _submit() async { 191 if (!_canSubmit) return; 192 193 setState(() { 194 _isSubmitting = true; 195 }); 196 197 final cubit = context.read<ProfileActionCubit>(); 198 final reason = _reasonController.text.trim().isNotEmpty ? _reasonController.text.trim() : null; 199 200 String? reportId; 201 202 if (widget._type == _ReportType.post && widget.postUri != null && widget.cid != null) { 203 reportId = await cubit.reportPost( 204 postUri: widget.postUri!, 205 cid: widget.cid!, 206 reasonType: _selectedReason!, 207 reason: reason, 208 ); 209 } else if (widget._type == _ReportType.actor) { 210 reportId = await cubit.reportActor(reasonType: _selectedReason!, reason: reason); 211 } 212 213 if (mounted) { 214 Navigator.pop(context); 215 216 if (reportId != null) { 217 _showSuccessDialog(reportId); 218 } else { 219 _showErrorDialog(); 220 } 221 } 222 } 223 224 void _showSuccessDialog(String reportId) { 225 showDialog<void>( 226 context: context, 227 builder: (context) => AlertDialog( 228 title: const Row( 229 children: [ 230 Icon(Icons.check_circle, color: Colors.green), 231 SizedBox(width: 8), 232 Text('Report Submitted'), 233 ], 234 ), 235 content: Text('Thank you. Your report (ID: $reportId) has been submitted.'), 236 actions: [FilledButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], 237 ), 238 ); 239 } 240 241 void _showErrorDialog() { 242 showDialog<void>( 243 context: context, 244 builder: (context) => AlertDialog( 245 title: const Row( 246 children: [ 247 Icon(Icons.error_outline, color: Colors.red), 248 SizedBox(width: 8), 249 Text('Report Failed'), 250 ], 251 ), 252 content: const Text('Unable to submit your report. Please try again later.'), 253 actions: [FilledButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))], 254 ), 255 ); 256 } 257}