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>
fix/252-block-outgoing-dm
Strycher 7 days ago
parent de5169331f
commit 163ffe35f4

@ -66,6 +66,7 @@ API: `isBlocked(keyHex)`, `isNameBlocked(name)`, `block(keyHex)`, `unblock(keyHe
| Surface | Rule |
|---|---|
| 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) |
| 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 |

@ -2602,5 +2602,6 @@
"block_namesSection": "Blocked names",
"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_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:
/// **'Radio block list full (32) — extra blocks stay app-only'**
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

@ -4618,4 +4618,7 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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
String get block_offloadStoreFull =>
'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) {
if (context.watch<BlockService>().isBlocked(widget.contact.publicKeyHex)) {
return _buildBlockedBar();
}
final maxBytes = maxContactMessageBytes();
final colorScheme = Theme.of(context).colorScheme;
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 {
final text = _textController.text.trim();
if (text.isEmpty) return;

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

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.