From 291932926526a1fc53f5500be956c24b6b44d62e Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 10 Aug 2026 03:12:33 -0400 Subject: [PATCH] feat(#550): CoreScope orchestration + Experimental toggle + observer badge After a channel send, when firmware advertises 0xC6 (cap2 0x08 + ver>=22) AND the owner enabled the feature, the connector queries the packet hash, then CoreScope for observer_count, and stamps it on the message (background, best-effort). Adds the coreScopeObserverCountEnabled AppSettings flag, a gated switch in the #509 Experimental section (disabled until the radio supports the capability), and a distinct cloud badge next to the radio-heard count. All inert until the firmware PR (#611) lands. Completes the client side of #524. Agent: QuietSnow (session 31eaba02) --- lib/connector/meshcore_connector.dart | 114 +++++++++++++++++++++++++ lib/l10n/app_en.arb | 11 +++ lib/l10n/app_localizations.dart | 24 ++++++ lib/l10n/app_localizations_bg.dart | 22 +++++ lib/l10n/app_localizations_de.dart | 22 +++++ lib/l10n/app_localizations_en.dart | 22 +++++ lib/l10n/app_localizations_es.dart | 22 +++++ lib/l10n/app_localizations_fr.dart | 22 +++++ lib/l10n/app_localizations_hu.dart | 22 +++++ lib/l10n/app_localizations_it.dart | 22 +++++ lib/l10n/app_localizations_ja.dart | 22 +++++ lib/l10n/app_localizations_ko.dart | 22 +++++ lib/l10n/app_localizations_nl.dart | 22 +++++ lib/l10n/app_localizations_pl.dart | 22 +++++ lib/l10n/app_localizations_pt.dart | 22 +++++ lib/l10n/app_localizations_ru.dart | 22 +++++ lib/l10n/app_localizations_sk.dart | 22 +++++ lib/l10n/app_localizations_sl.dart | 22 +++++ lib/l10n/app_localizations_sv.dart | 22 +++++ lib/l10n/app_localizations_uk.dart | 22 +++++ lib/l10n/app_localizations_zh.dart | 22 +++++ lib/models/app_settings.dart | 12 +++ lib/screens/channel_chat_screen.dart | 18 ++++ lib/screens/settings_screen.dart | 26 +++++- lib/services/app_settings_service.dart | 6 ++ untranslated.json | 68 +++++++++++++++ 26 files changed, 672 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d4ec468..eb02c16 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -19,6 +19,7 @@ import '../models/path_selection.dart'; import '../models/translation_support.dart'; import '../helpers/channel_send_timestamp.dart'; import '../helpers/pending_reactions.dart'; +import '../services/corescope_service.dart'; import '../helpers/pocketmesh_reaction.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/time_anomaly.dart'; @@ -296,6 +297,11 @@ class MeshCoreConnector extends ChangeNotifier { bool _caplogAwaitingStart = false; Completer? _caplogAckCompleter; Completer? _caplogStatusCompleter; + + /// Outstanding 0xC6 packet-hash queries, keyed "ts_chan" so a reply matches + /// its request without relying on ordering (#524). + final Map> _pendingPktHashCompleters = {}; + final CoreScopeService _coreScopeService = CoreScopeService(); int _caplogChunks = 0; // CHUNK frames in the active download (diagnostic) String? _firmwareVersion; String? _deviceModel; @@ -631,6 +637,16 @@ class MeshCoreConnector extends ChangeNotifier { /// Whether this radio exposes a settable device notification scope (#475). bool get supportsNotifyScope => firmwareSupportsNotifyScope(_offbandCaps2); + /// Whether this radio answers the 0xC6 packet-hash query (#524/#611). + bool get supportsPktHash => + firmwareSupportsPktHash(_offbandCaps2, _firmwareVerCode); + + /// Whether to fetch CoreScope observer counts for outgoing channel messages: + /// firmware supports the hash query AND the owner enabled the feature. + bool get _coreScopeQueryActive => + supportsPktHash && + (_appSettingsService?.settings.coreScopeObserverCountEnabled ?? false); + /// Whether the radio can actually be QUERIED, as opposed to merely reporting /// that it has the feature. False until firmware lands the get/set command, /// so no frame is emitted that nothing will answer. The distinction is real @@ -3883,6 +3899,17 @@ class MeshCoreConnector extends ChangeNotifier { channelSendQueueId: message.messageId, expectsGenericAck: true, ); + // Owner-only CoreScope observer count (#524): background, gated, and + // best-effort. Never blocks or fails the send. + if (_coreScopeQueryActive) { + unawaited( + _fetchAndStoreCoreScopeCount( + channel.index, + sendTsSecs, + message.messageId, + ), + ); + } } catch (e) { // Clear the stuck queue id and surface the failure in the chat bubble // instead of leaving the message pending and blocking the queue (#395). @@ -4679,6 +4706,90 @@ class MeshCoreConnector extends ChangeNotifier { } } + /// Ask firmware for the on-air packet hash of a channel message we sent, + /// keyed by (ts, chan) (0xC6, #524). Returns null on timeout / error frame. + Future _queryPacketHash( + int ts, + int chan, { + Duration timeout = const Duration(seconds: 5), + }) async { + final key = '${ts}_$chan'; + final completer = Completer(); + _pendingPktHashCompleters[key] = completer; + final timer = Timer(timeout, () { + if (!completer.isCompleted) completer.complete(null); + }); + try { + await sendFrame(buildOffbandPktHashGetFrame(ts, chan)); + return await completer.future; + } catch (e) { + appLogger.warn('0xC6 query failed for $key: $e', tag: 'CoreScope'); + return null; + } finally { + timer.cancel(); + _pendingPktHashCompleters.remove(key); + } + } + + /// Routes a 0xC6 reply to its waiting query. Success replies echo the key; + /// error/malformed frames are logged and left for the query to time out (the + /// error frame carries no key, so it cannot be correlated). + void _handleOffbandPktHashFrame(Uint8List frame) { + final parsed = parseOffbandPktHashReply(frame); + if (parsed == null) { + appLogger.warn('0xC6 error or malformed reply', tag: 'CoreScope'); + return; + } + final key = '${parsed.timestamp}_${parsed.channelIdx}'; + final completer = _pendingPktHashCompleters[key]; + if (completer != null && !completer.isCompleted) { + completer.complete(parsed); + } + } + + /// After sending a channel message, fetch its firmware hash then CoreScope's + /// observer count and stamp both on the message. Best-effort and gated; any + /// failure leaves the message showing radio-only. + Future _fetchAndStoreCoreScopeCount( + int channelIndex, + int ts, + String messageId, + ) async { + try { + final pkt = await _queryPacketHash(ts, channelIndex); + if (pkt == null) return; + _updateChannelMessageById( + channelIndex, + messageId, + (m) => m.copyWith(onAirHash: pkt.hashHex), + ); + final count = await _coreScopeService.fetchObserverCount(pkt.hashHex); + if (count == null) return; + _updateChannelMessageById( + channelIndex, + messageId, + (m) => m.copyWith(coreScopeObserverCount: count), + ); + notifyListeners(); + } catch (e) { + appLogger.warn( + 'CoreScope observer-count fetch failed: $e', + tag: 'CoreScope', + ); + } + } + + void _updateChannelMessageById( + int channelIndex, + String messageId, + ChannelMessage Function(ChannelMessage) transform, + ) { + final messages = _channelMessages[channelIndex]; + if (messages == null) return; + final i = messages.indexWhere((m) => m.messageId == messageId); + if (i >= 0) messages[i] = transform(messages[i]); + } + /// Called when a BLOCK_LIST dump ends. A truncated dump (early-END) is /// re-requested once the link settles; never derive removals from a partial /// pull. The union reconcile against the local list is wired in B4. @@ -4871,6 +4982,9 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeOffbandCaplog: _handleOffbandCaplogFrame(frame); break; + case cmdOffbandPktHash: + _handleOffbandPktHashFrame(frame); + break; case respCodeOffbandDeviceUi: _handleDeviceUiReply(frame); break; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bba2a88..cbdb224 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -218,6 +218,9 @@ "settings_experimental": "Experimental", "settings_experimentalSubtitle": "Advanced and in-development features", "settings_experimentalDescription": "These features are experimental and may change or be removed at any time. They default to off.", + "settings_coreScopeObserverCount": "CoreScope observer counts", + "settings_coreScopeObserverCountSubtitle": "On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.", + "settings_coreScopeObserverCountUnsupported": "Requires a firmware build with packet-hash support (v22+).", "settings_experimentalHide": "Hide Experimental section", "settings_experimentalHideSubtitle": "Unlock again anytime with 7 taps on the version row.", "settings_experimentalUnlocked": "Experimental features unlocked", @@ -873,6 +876,14 @@ } } }, + "channel_coreScopeTooltip": "Seen by {count, plural, =1{1 CoreScope observer} other{{count} CoreScope observers}}", + "@channel_coreScopeTooltip": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "chat_successes": "successes", "chat_score": "Score", "chat_removePath": "Remove path", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f242495..1a50515 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1108,6 +1108,24 @@ abstract class AppLocalizations { /// **'These features are experimental and may change or be removed at any time. They default to off.'** String get settings_experimentalDescription; + /// No description provided for @settings_coreScopeObserverCount. + /// + /// In en, this message translates to: + /// **'CoreScope observer counts'** + String get settings_coreScopeObserverCount; + + /// No description provided for @settings_coreScopeObserverCountSubtitle. + /// + /// In en, this message translates to: + /// **'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'** + String get settings_coreScopeObserverCountSubtitle; + + /// No description provided for @settings_coreScopeObserverCountUnsupported. + /// + /// In en, this message translates to: + /// **'Requires a firmware build with packet-hash support (v22+).'** + String get settings_coreScopeObserverCountUnsupported; + /// No description provided for @settings_experimentalHide. /// /// In en, this message translates to: @@ -3064,6 +3082,12 @@ abstract class AppLocalizations { /// **'Heard {count, plural, =1{1 repeat} other{{count} repeats}}'** String channel_heardTooltip(int count); + /// No description provided for @channel_coreScopeTooltip. + /// + /// In en, this message translates to: + /// **'Seen by {count, plural, =1{1 CoreScope observer} other{{count} CoreScope observers}}'** + String channel_coreScopeTooltip(int count); + /// No description provided for @chat_successes. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index b3d09ac..ae3110b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -551,6 +551,17 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1691,6 +1702,17 @@ class AppLocalizationsBg extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Успехи'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 595d95f..6e5a280 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -548,6 +548,17 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1689,6 +1700,17 @@ class AppLocalizationsDe extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Erfolgreich'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b481aaf..8e0a6f5 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -539,6 +539,17 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1659,6 +1670,17 @@ class AppLocalizationsEn extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'successes'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 4148713..8dab2d2 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -549,6 +549,17 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1686,6 +1697,17 @@ class AppLocalizationsEs extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Éxitos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3d79d4d..1c0a46a 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -553,6 +553,17 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1693,6 +1704,17 @@ class AppLocalizationsFr extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Succès'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 9baf3c9..50232fe 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -551,6 +551,17 @@ class AppLocalizationsHu extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1698,6 +1709,17 @@ class AppLocalizationsHu extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'sikerek'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 5ad20ca..fc416a4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -551,6 +551,17 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1688,6 +1699,17 @@ class AppLocalizationsIt extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'successi'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 812850e..4dfbae5 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -525,6 +525,17 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1623,6 +1634,17 @@ class AppLocalizationsJa extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => '成功事例'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 147a064..72e02fc 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -525,6 +525,17 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1619,6 +1630,17 @@ class AppLocalizationsKo extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => '성공 사례'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index d21f770..f77dbd5 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -545,6 +545,17 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1676,6 +1687,17 @@ class AppLocalizationsNl extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Succesvol'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 8cecaec..9149e2e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -553,6 +553,17 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1701,6 +1712,17 @@ class AppLocalizationsPl extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Sukcesy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 621dfef..4486ed5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -551,6 +551,17 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1685,6 +1696,17 @@ class AppLocalizationsPt extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Sucessos'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 33f21ef..15c2402 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -550,6 +550,17 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1689,6 +1700,17 @@ class AppLocalizationsRu extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'успешно'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 38d39a9..1cf8063 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -545,6 +545,17 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1678,6 +1689,17 @@ class AppLocalizationsSk extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Úspechy'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 8e10eeb..bb78aa1 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -543,6 +543,17 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1674,6 +1685,17 @@ class AppLocalizationsSl extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Uspešni'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index e9bb905..ac202cc 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -540,6 +540,17 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1669,6 +1680,17 @@ class AppLocalizationsSv extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'framgångar'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 53eb85f..4b0914a 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -547,6 +547,17 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1684,6 +1695,17 @@ class AppLocalizationsUk extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => 'Успішно'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 91aa849..e39bf18 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -518,6 +518,17 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_experimentalDescription => 'These features are experimental and may change or be removed at any time. They default to off.'; + @override + String get settings_coreScopeObserverCount => 'CoreScope observer counts'; + + @override + String get settings_coreScopeObserverCountSubtitle => + 'On outgoing channel messages, show how many CoreScope observers heard the packet, alongside radio-heard repeats. Needs internet.'; + + @override + String get settings_coreScopeObserverCountUnsupported => + 'Requires a firmware build with packet-hash support (v22+).'; + @override String get settings_experimentalHide => 'Hide Experimental section'; @@ -1598,6 +1609,17 @@ class AppLocalizationsZh extends AppLocalizations { return 'Heard $_temp0'; } + @override + String channel_coreScopeTooltip(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count CoreScope observers', + one: '1 CoreScope observer', + ); + return 'Seen by $_temp0'; + } + @override String get chat_successes => '成功'; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index c012887..7e95baa 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -144,6 +144,11 @@ class AppSettings { /// the version row (#509). Not a security boundary, just keeps advanced/ /// me-centric toggles out of the default UI. Persists across restarts. final bool experimentalUnlocked; + + /// Owner-only: show CoreScope-observed repeat counts on outgoing channel + /// messages (#524). Lives under the Experimental section, default off, and + /// only takes effect when the firmware supports the 0xC6 packet-hash query. + final bool coreScopeObserverCountEnabled; final Map batteryChemistryByDeviceId; final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; @@ -223,6 +228,7 @@ class AppSettings { this.languageOverride, this.appDebugLogEnabled = false, this.experimentalUnlocked = false, + this.coreScopeObserverCountEnabled = false, Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, @@ -290,6 +296,7 @@ class AppSettings { 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, 'experimental_unlocked': experimentalUnlocked, + 'corescope_observer_count_enabled': coreScopeObserverCountEnabled, 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, @@ -388,6 +395,8 @@ class AppSettings { languageOverride: json['language_override'] as String?, appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false, experimentalUnlocked: json['experimental_unlocked'] as bool? ?? false, + coreScopeObserverCountEnabled: + json['corescope_observer_count_enabled'] as bool? ?? false, batteryChemistryByDeviceId: (json['battery_chemistry_by_device_id'] as Map?)?.map( (key, value) => MapEntry(key.toString(), value.toString()), @@ -506,6 +515,7 @@ class AppSettings { Object? languageOverride = _unset, bool? appDebugLogEnabled, bool? experimentalUnlocked, + bool? coreScopeObserverCountEnabled, Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, @@ -566,6 +576,8 @@ class AppSettings { : languageOverride as String?, appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, experimentalUnlocked: experimentalUnlocked ?? this.experimentalUnlocked, + coreScopeObserverCountEnabled: + coreScopeObserverCountEnabled ?? this.coreScopeObserverCountEnabled, batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, batteryChemistryByRepeaterId: diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 87799a3..73b9545 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1517,6 +1517,24 @@ class _ChannelChatScreenState extends State { ), ), ], + // CoreScope observer count (#524), shown parallel to the radio-heard + // count with a distinct icon, never merged into one number. + if (isOutgoing && (message.coreScopeObserverCount ?? 0) > 0) ...[ + dot, + Tooltip( + message: context.l10n.channel_coreScopeTooltip( + message.coreScopeObserverCount!, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.cloud_outlined, size: 12, color: metaColor), + const SizedBox(width: 4), + Text('${message.coreScopeObserverCount}', style: metaStyle), + ], + ), + ), + ], if (isOutgoing) ...[ const SizedBox(width: 6), Icon( diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 553e17a..3f34969 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -233,7 +233,10 @@ class _SettingsScreenState extends State { Widget _experimentalPane(BuildContext context) { final l10n = context.l10n; - final settingsService = context.read(); + final settingsService = context.watch(); + final supportsPktHash = context.select( + (c) => c.supportsPktHash, + ); return ListView( padding: const EdgeInsets.all(16), children: [ @@ -244,8 +247,25 @@ class _SettingsScreenState extends State { ), ), const SizedBox(height: 16), - // Future me-centric/experimental toggles land here (e.g. Fast Sync - // #118, CoreScope repeats #524). Empty of features until they arrive. + // CoreScope observer counts (#524). Disabled until the connected radio + // advertises the 0xC6 packet-hash capability. + Card( + child: SwitchListTile( + secondary: const Icon(Icons.cloud_outlined), + title: Text(l10n.settings_coreScopeObserverCount), + subtitle: Text( + supportsPktHash + ? l10n.settings_coreScopeObserverCountSubtitle + : l10n.settings_coreScopeObserverCountUnsupported, + ), + value: settingsService.settings.coreScopeObserverCountEnabled, + onChanged: supportsPktHash + ? (value) => + settingsService.setCoreScopeObserverCountEnabled(value) + : null, + ), + ), + const SizedBox(height: 16), // A plain action (not a switch): "hide" is a one-shot, and a switch // whose ON state means "hidden" read backwards. Card( diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index f666cfe..ed85a61 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -186,6 +186,12 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(experimentalUnlocked: value)); } + Future setCoreScopeObserverCountEnabled(bool value) async { + await updateSettings( + _settings.copyWith(coreScopeObserverCountEnabled: value), + ); + } + Future setMapShowDiscoveryContacts(bool value) async { await updateSettings(_settings.copyWith(mapShowDiscoveryContacts: value)); } diff --git a/untranslated.json b/untranslated.json index 7e7793a..8141241 100644 --- a/untranslated.json +++ b/untranslated.json @@ -15,6 +15,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -48,6 +51,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -111,6 +115,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -144,6 +151,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -207,6 +215,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -240,6 +251,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -303,6 +315,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -336,6 +351,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -399,6 +415,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -432,6 +451,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -495,6 +515,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -528,6 +551,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -591,6 +615,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -624,6 +651,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -687,6 +715,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -720,6 +751,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -783,6 +815,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -816,6 +851,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -879,6 +915,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -912,6 +951,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -975,6 +1015,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -1008,6 +1051,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1071,6 +1115,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -1104,6 +1151,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1167,6 +1215,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -1200,6 +1251,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1263,6 +1315,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -1296,6 +1351,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1359,6 +1415,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -1392,6 +1451,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1455,6 +1515,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -1488,6 +1551,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1551,6 +1615,9 @@ "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", + "settings_coreScopeObserverCount", + "settings_coreScopeObserverCountSubtitle", + "settings_coreScopeObserverCountUnsupported", "settings_experimentalHide", "settings_experimentalHideSubtitle", "settings_experimentalUnlocked", @@ -1584,6 +1651,7 @@ "debugLog_logSaved", "debugLog_logUnavailable", "channel_heardTooltip", + "channel_coreScopeTooltip", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters",