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 _isUploadingImage = false;
String? _errorMessage;
String? _blockedMessage;
final int _maxCharacters = 500;
bool _allowChain = true;
bool _isBold = false;
@ -69,6 +70,10 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
_bodyController.addListener(() {
_charCountNotifier.value = _bodyController.text.length;
_handleHashtagSuggestions();
// Clear blocked banner when user edits their text
if (_blockedMessage != null) {
setState(() => _blockedMessage = null);
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
@ -326,7 +331,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
// Layer 0: Client-side hard blocklist never even send to server
final blockMessage = ContentFilter.instance.check(_bodyController.text.trim());
if (blockMessage != null) {
await _showBlockedDialog(blockMessage);
setState(() => _blockedMessage = blockMessage);
return;
}
@ -411,7 +416,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
final msg = e.toString().replaceAll('Exception: ', '');
// Server-side blocklist catch (422 with blocked content message)
if (msg.contains("isn't allowed on Sojorn") || msg.contains('not allowed')) {
if (mounted) await _showBlockedDialog(msg);
if (mounted) setState(() => _blockedMessage = msg);
} else {
setState(() {
_errorMessage = msg;
@ -454,35 +459,60 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
return result ?? false;
}
Future<void> _showBlockedDialog(String message) async {
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
Icon(Icons.block, color: AppTheme.error, size: 24),
const SizedBox(width: 8),
const Text('Not Allowed'),
],
),
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'),
),
],
),
Widget _buildBlockedBanner() {
return AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
child: _blockedMessage != null
? Container(
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: [
Icon(Icons.info_outline, color: AppTheme.error, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_blockedMessage!,
style: AppTheme.textTheme.bodySmall?.copyWith(
color: const Color(0xFF991B1B),
fontWeight: FontWeight.w500,
height: 1.4,
),
),
],
),
),
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(
child: Column(
children: [
_buildBlockedBanner(),
if (_errorMessage != null)
Container(
width: double.infinity,