fix(#252): gate outgoing DM to a blocked contact

Replace the DM composer with an "Unblock to message" notice bar when
the contact is blocked, so a block also stops outgoing DMs (previously
only incoming DMs/adverts were suppressed). Inline unblock restores the
composer. Adds block_composerNotice l10n string and documents the
outgoing-DM rule in block-contract-as-built.md §4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/264/head
Strycher 7 days ago
parent de5169331f
commit 42a00baa82

@ -66,6 +66,7 @@ API: `isBlocked(keyHex)`, `isNameBlocked(name)`, `block(keyHex)`, `unblock(keyHe
| Surface | Rule | | Surface | Rule |
|---|---| |---|---|
| Incoming DM | drop + suppress notification when `senderKey ∈ blockedKeys` (on Offband, firmware already dropped it at receive — §7) | | Incoming DM | drop + suppress notification when `senderKey ∈ blockedKeys` (on Offband, firmware already dropped it at receive — §7) |
| Outgoing DM | **prevent sending** to a blocked contact — the DM composer is replaced by a notice bar with an inline **Unblock** action (you cannot message someone you've blocked without first unblocking). Chat history stays visible. |
| Advert | suppress the new-advert notification for `blockedKeys`. **Do NOT discard the advert** — the contact-name update must keep flowing (self-heal, §5) | | Advert | suppress the new-advert notification for `blockedKeys`. **Do NOT discard the advert** — the contact-name update must keep flowing (self-heal, §5) |
| Contacts / Discovery lists | **keep blocked entries VISIBLE** with a clear blocked indicator — a red block icon (`Icons.block`, circle-with-slash) + muted/greyed styling. **Do NOT hide them.** Inline unblock available. *(A "hide blocked entirely" toggle is a deliberate post-implementation follow-on, not v1.)* | | Contacts / Discovery lists | **keep blocked entries VISIBLE** with a clear blocked indicator — a red block icon (`Icons.block`, circle-with-slash) + muted/greyed styling. **Do NOT hide them.** Inline unblock available. *(A "hide blocked entirely" toggle is a deliberate post-implementation follow-on, not v1.)* |
| Channel message | hide per the resolution rule in §5 | | Channel message | hide per the resolution rule in §5 |

@ -2602,5 +2602,6 @@
"block_namesSection": "Blocked names", "block_namesSection": "Blocked names",
"block_namesHint": "Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.", "block_namesHint": "Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.",
"block_offloadActive": "Firmware offload active — blocks sync to this radio", "block_offloadActive": "Firmware offload active — blocks sync to this radio",
"block_offloadStoreFull": "Radio block list full (32) — extra blocks stay app-only" "block_offloadStoreFull": "Radio block list full (32) — extra blocks stay app-only",
"block_composerNotice": "You've blocked this contact"
} }

@ -7887,6 +7887,12 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Radio block list full (32) — extra blocks stay app-only'** /// **'Radio block list full (32) — extra blocks stay app-only'**
String get block_offloadStoreFull; String get block_offloadStoreFull;
/// No description provided for @block_composerNotice.
///
/// In en, this message translates to:
/// **'You\'ve blocked this contact'**
String get block_composerNotice;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

@ -4618,4 +4618,7 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4636,4 +4636,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4540,4 +4540,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4623,4 +4623,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4652,4 +4652,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4639,4 +4639,7 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4628,4 +4628,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4393,4 +4393,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4394,4 +4394,7 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4603,4 +4603,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4640,4 +4640,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4616,4 +4616,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4634,4 +4634,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4599,4 +4599,7 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4597,4 +4597,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4572,4 +4572,7 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4635,4 +4635,7 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -4267,4 +4267,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get block_offloadStoreFull => String get block_offloadStoreFull =>
'Radio block list full (32) — extra blocks stay app-only'; 'Radio block list full (32) — extra blocks stay app-only';
@override
String get block_composerNotice => 'You\'ve blocked this contact';
} }

@ -591,6 +591,9 @@ class _ChatScreenState extends State<ChatScreen> {
} }
Widget _buildInputBar(MeshCoreConnector connector) { Widget _buildInputBar(MeshCoreConnector connector) {
if (context.watch<BlockService>().isBlocked(widget.contact.publicKeyHex)) {
return _buildBlockedBar();
}
final maxBytes = maxContactMessageBytes(); final maxBytes = maxContactMessageBytes();
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final settings = context.watch<AppSettingsService>().settings; final settings = context.watch<AppSettingsService>().settings;
@ -736,6 +739,38 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
Widget _buildBlockedBar() {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: colorScheme.surface,
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
),
child: SafeArea(
child: Row(
children: [
const SizedBox(width: 4),
Icon(Icons.block, color: colorScheme.error),
const SizedBox(width: 12),
Expanded(
child: Text(
context.l10n.block_composerNotice,
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
),
TextButton(
onPressed: () => context.read<BlockService>().unblock(
widget.contact.publicKeyHex,
),
child: Text(context.l10n.block_unblock),
),
],
),
),
);
}
Future<void> _sendMessage(MeshCoreConnector connector) async { Future<void> _sendMessage(MeshCoreConnector connector) async {
final text = _textController.text.trim(); final text = _textController.text.trim();
if (text.isEmpty) return; if (text.isEmpty) return;

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial flserial
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

@ -49,7 +49,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"de": [ "de": [
@ -102,7 +103,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"es": [ "es": [
@ -155,7 +157,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"fr": [ "fr": [
@ -208,7 +211,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"hu": [ "hu": [
@ -261,7 +265,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"it": [ "it": [
@ -314,7 +319,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"ja": [ "ja": [
@ -367,7 +373,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"ko": [ "ko": [
@ -420,7 +427,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"nl": [ "nl": [
@ -473,7 +481,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"pl": [ "pl": [
@ -526,7 +535,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"pt": [ "pt": [
@ -579,7 +589,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"ru": [ "ru": [
@ -632,7 +643,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"sk": [ "sk": [
@ -685,7 +697,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"sl": [ "sl": [
@ -738,7 +751,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"sv": [ "sv": [
@ -791,7 +805,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"uk": [ "uk": [
@ -844,7 +859,8 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
], ],
"zh": [ "zh": [
@ -897,6 +913,7 @@
"block_namesSection", "block_namesSection",
"block_namesHint", "block_namesHint",
"block_offloadActive", "block_offloadActive",
"block_offloadStoreFull" "block_offloadStoreFull",
"block_composerNotice"
] ]
} }

@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial flserial
flutter_local_notifications_windows flutter_local_notifications_windows
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

Loading…
Cancel
Save

Powered by TurnKey Linux.