feat(#173): Settings -> Blocked management screen

New BlockedView pane (Settings category, after Privacy): lists blocked pubkeys
(contact name when resolvable, else short key) and name-only channel blocks, each
with Unblock; empty state when nothing blocked. App-local; firmware-offload
indicator deferred to Epic B. Adds block_* settings l10n.

Epic A #165 / A6 #173. Design: docs/architecture/block-contract-as-built.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/209/head
Strycher 1 week ago
parent b1757c6539
commit c3a47559f2

@ -2593,5 +2593,11 @@
"type": "String"
}
}
}
},
"block_settingsTitle": "Blocked",
"block_settingsSubtitle": "Users you've blocked",
"block_none": "No blocked users",
"block_keysSection": "Blocked contacts",
"block_namesSection": "Blocked names",
"block_namesHint": "Name-only blocks (channel senders) — each upgrades to a full block once we learn their key."
}

@ -7833,6 +7833,42 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Blocked \"{name}\" across all channels — name only until we learn their key'**
String block_blockedNameOnly(String name);
/// No description provided for @block_settingsTitle.
///
/// In en, this message translates to:
/// **'Blocked'**
String get block_settingsTitle;
/// No description provided for @block_settingsSubtitle.
///
/// In en, this message translates to:
/// **'Users you\'ve blocked'**
String get block_settingsSubtitle;
/// No description provided for @block_none.
///
/// In en, this message translates to:
/// **'No blocked users'**
String get block_none;
/// No description provided for @block_keysSection.
///
/// In en, this message translates to:
/// **'Blocked contacts'**
String get block_keysSection;
/// No description provided for @block_namesSection.
///
/// In en, this message translates to:
/// **'Blocked names'**
String get block_namesSection;
/// No description provided for @block_namesHint.
///
/// In en, this message translates to:
/// **'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'**
String get block_namesHint;
}
class _AppLocalizationsDelegate

@ -4588,4 +4588,23 @@ class AppLocalizationsBg extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4606,4 +4606,23 @@ class AppLocalizationsDe extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4510,4 +4510,23 @@ class AppLocalizationsEn extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4593,4 +4593,23 @@ class AppLocalizationsEs extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4622,4 +4622,23 @@ class AppLocalizationsFr extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4609,4 +4609,23 @@ class AppLocalizationsHu extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4598,4 +4598,23 @@ class AppLocalizationsIt extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4363,4 +4363,23 @@ class AppLocalizationsJa extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4364,4 +4364,23 @@ class AppLocalizationsKo extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4573,4 +4573,23 @@ class AppLocalizationsNl extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4610,4 +4610,23 @@ class AppLocalizationsPl extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4586,4 +4586,23 @@ class AppLocalizationsPt extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4604,4 +4604,23 @@ class AppLocalizationsRu extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4569,4 +4569,23 @@ class AppLocalizationsSk extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4567,4 +4567,23 @@ class AppLocalizationsSl extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4542,4 +4542,23 @@ class AppLocalizationsSv extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4605,4 +4605,23 @@ class AppLocalizationsUk extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -4237,4 +4237,23 @@ class AppLocalizationsZh extends AppLocalizations {
String block_blockedNameOnly(String name) {
return 'Blocked \"$name\" across all channels — name only until we learn their key';
}
@override
String get block_settingsTitle => 'Blocked';
@override
String get block_settingsSubtitle => 'Users you\'ve blocked';
@override
String get block_none => 'No blocked users';
@override
String get block_keysSection => 'Blocked contacts';
@override
String get block_namesSection => 'Blocked names';
@override
String get block_namesHint =>
'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.';
}

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../connector/meshcore_connector.dart';
import '../../l10n/l10n.dart';
import '../../services/block_service.dart';
/// Embeddable view (no Scaffold) for the Blocked settings pane.
///
/// Lists blocked public keys (with the contact name when resolvable) and
/// name-only channel blocks, each with an Unblock action. App-local; the
/// firmware-offload indicator is added by Epic B.
class BlockedView extends StatelessWidget {
const BlockedView({super.key});
@override
Widget build(BuildContext context) {
return Consumer2<BlockService, MeshCoreConnector>(
builder: (context, block, connector, child) {
final keys = block.blockedKeys.toList()..sort();
final names = block.blockedNames.keys.toList()..sort();
if (keys.isEmpty && names.isEmpty) {
return Center(child: Text(context.l10n.block_none));
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (keys.isNotEmpty) ...[
_sectionHeader(context.l10n.block_keysSection),
...keys.map((k) => _keyTile(context, block, connector, k)),
const SizedBox(height: 16),
],
if (names.isNotEmpty) ...[
_sectionHeader(context.l10n.block_namesSection),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
context.l10n.block_namesHint,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
...names.map((n) => _nameTile(context, block, n)),
],
],
);
},
);
}
Widget _sectionHeader(String text) => Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
child: Text(
text,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
String? _nameForKey(MeshCoreConnector connector, String keyHex) {
for (final c in connector.contacts) {
if (c.publicKeyHex == keyHex) return c.name;
}
for (final c in connector.discoveredContacts) {
if (c.publicKeyHex == keyHex) return c.name;
}
return null;
}
Widget _keyTile(
BuildContext context,
BlockService block,
MeshCoreConnector connector,
String keyHex,
) {
final name = _nameForKey(connector, keyHex);
final shortKey =
'${keyHex.substring(0, 8)}${keyHex.substring(keyHex.length - 8)}';
return ListTile(
leading: Icon(Icons.block, color: Colors.red.shade700),
title: Text(name ?? shortKey),
subtitle: name != null ? Text(shortKey) : null,
trailing: TextButton(
onPressed: () => block.unblock(keyHex),
child: Text(context.l10n.block_unblock),
),
);
}
Widget _nameTile(BuildContext context, BlockService block, String name) {
return ListTile(
leading: Icon(Icons.block, color: Colors.red.shade700),
title: Text(name),
trailing: TextButton(
onPressed: () => block.unblockName(name),
child: Text(context.l10n.block_unblock),
),
);
}
}

@ -17,6 +17,7 @@ import 'settings/settings_shell.dart';
import 'settings/app_settings_view.dart';
import 'settings/message_settings_view.dart';
import 'settings/observer_settings_view.dart';
import 'settings/blocked_view.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
import 'topology_debug_screen.dart';
@ -108,6 +109,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: l10n.settings_privacySubtitle,
builder: _privacyPane,
),
SettingsCategory(
icon: Icons.block,
title: l10n.block_settingsTitle,
subtitle: l10n.block_settingsSubtitle,
builder: _blockedPane,
),
SettingsCategory(
icon: Icons.contacts_outlined,
title: l10n.contacts_title,
@ -153,6 +160,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _observerPane(BuildContext context) => const ObserverSettingsView();
Widget _blockedPane(BuildContext context) => const BlockedView();
Widget _radioRangePane(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();

@ -26,7 +26,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"de": [
@ -56,7 +62,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"es": [
@ -86,7 +98,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"fr": [
@ -116,7 +134,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"hu": [
@ -146,7 +170,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"it": [
@ -176,7 +206,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"ja": [
@ -206,7 +242,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"ko": [
@ -236,7 +278,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"nl": [
@ -266,7 +314,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"pl": [
@ -296,7 +350,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"pt": [
@ -326,7 +386,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"ru": [
@ -356,7 +422,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"sk": [
@ -386,7 +458,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"sl": [
@ -416,7 +494,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"sv": [
@ -446,7 +530,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"uk": [
@ -476,7 +566,13 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
],
"zh": [
@ -506,6 +602,12 @@
"block_sender",
"block_blocked",
"block_unblocked",
"block_blockedNameOnly"
"block_blockedNameOnly",
"block_settingsTitle",
"block_settingsSubtitle",
"block_none",
"block_keysSection",
"block_namesSection",
"block_namesHint"
]
}

Loading…
Cancel
Save

Powered by TurnKey Linux.