Replace blocked content popup with Instagram-style inline banner

This commit is contained in:
Patrick Britton 2026-02-06 11:53:32 -06:00
parent f6c4bb88e0
commit b5002c1ce4

View file

@ -39,6 +39,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
bool _isLoading = false; bool _isLoading = false;
bool _isUploadingImage = false; bool _isUploadingImage = false;
String? _errorMessage; String? _errorMessage;
String? _blockedMessage;
final int _maxCharacters = 500; final int _maxCharacters = 500;
bool _allowChain = true; bool _allowChain = true;
bool _isBold = false; bool _isBold = false;
@ -69,6 +70,10 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
_bodyController.addListener(() { _bodyController.addListener(() {
_charCountNotifier.value = _bodyController.text.length; _charCountNotifier.value = _bodyController.text.length;
_handleHashtagSuggestions(); _handleHashtagSuggestions();
// Clear blocked banner when user edits their text
if (_blockedMessage != null) {
setState(() => _blockedMessage = null);
}
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
@ -326,7 +331,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
// Layer 0: Client-side hard blocklist never even send to server // Layer 0: Client-side hard blocklist never even send to server
final blockMessage = ContentFilter.instance.check(_bodyController.text.trim()); final blockMessage = ContentFilter.instance.check(_bodyController.text.trim());
if (blockMessage != null) { if (blockMessage != null) {
await _showBlockedDialog(blockMessage); setState(() => _blockedMessage = blockMessage);
return; return;
} }
@ -411,7 +416,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
final msg = e.toString().replaceAll('Exception: ', ''); final msg = e.toString().replaceAll('Exception: ', '');
// Server-side blocklist catch (422 with blocked content message) // Server-side blocklist catch (422 with blocked content message)
if (msg.contains("isn't allowed on Sojorn") || msg.contains('not allowed')) { if (msg.contains("isn't allowed on Sojorn") || msg.contains('not allowed')) {
if (mounted) await _showBlockedDialog(msg); if (mounted) setState(() => _blockedMessage = msg);
} else { } else {
setState(() { setState(() {
_errorMessage = msg; _errorMessage = msg;
@ -454,35 +459,60 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
return result ?? false; return result ?? false;
} }
Future<void> _showBlockedDialog(String message) async { Widget _buildBlockedBanner() {
await showDialog<void>( return AnimatedSize(
context: context, duration: const Duration(milliseconds: 250),
barrierDismissible: false, curve: Curves.easeOut,
builder: (context) => AlertDialog( child: _blockedMessage != null
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ? Container(
title: Row( width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: AppTheme.spacingMd,
vertical: 12,
),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
border: Border(
bottom: BorderSide(
color: AppTheme.error.withValues(alpha: 0.2),
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(Icons.block, color: AppTheme.error, size: 24), Icon(Icons.info_outline, color: AppTheme.error, size: 20),
const SizedBox(width: 8), const SizedBox(width: 10),
const Text('Not Allowed'), Expanded(
], child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_blockedMessage!,
style: AppTheme.textTheme.bodySmall?.copyWith(
color: const Color(0xFF991B1B),
fontWeight: FontWeight.w500,
height: 1.4,
), ),
content: Text(
message,
style: AppTheme.textTheme.bodyMedium,
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.brightNavy,
foregroundColor: AppTheme.white,
shape: const StadiumBorder(),
),
child: const Text('Edit My Post'),
), ),
], ],
), ),
),
GestureDetector(
onTap: () => setState(() => _blockedMessage = null),
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: Icon(
Icons.close,
size: 16,
color: AppTheme.error.withValues(alpha: 0.6),
),
),
),
],
),
)
: const SizedBox.shrink(),
); );
} }
@ -515,6 +545,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
_buildBlockedBanner(),
if (_errorMessage != null) if (_errorMessage != null)
Container( Container(
width: double.infinity, width: double.infinity,