diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index bcbb60d..369a6ca 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4789,17 +4789,23 @@ class MeshCoreConnector extends ChangeNotifier { for (final secs in _coreScopePollSecs) { await Future.delayed(Duration(seconds: secs)); if (!isConnected) return; - final count = await _coreScopeService.fetchObserverCount(pkt.hashHex); - if (count == null) continue; - if (count > best) { - best = count; + final counts = await _coreScopeService.fetchCounts(pkt.hashHex); + if (counts == null) continue; + if (counts.observers > best) { + best = counts.observers; flat = 0; _updateChannelMessageById( channelIndex, messageId, - (m) => m.copyWith(coreScopeObserverCount: count), + (m) => m.copyWith( + coreScopeObserverCount: counts.observers, + coreScopeObservationCount: counts.observations, + ), + ); + appLogger.info( + 'observers=${counts.observers} observations=${counts.observations}', + tag: 'CoreScope', ); - appLogger.info('observer count=$count', tag: 'CoreScope'); notifyListeners(); } else if (++flat >= _coreScopeStableChecks) { return; // stabilised @@ -4814,21 +4820,26 @@ class MeshCoreConnector extends ChangeNotifier { } /// Tap-to-refresh: re-query CoreScope for a message's stored on-air hash and - /// bump the observer count if it grew (counts only ever climb). Returns the - /// fetched count (or null on failure / no stored hash) so the UI can confirm. - Future refreshCoreScopeObserverCount( + /// bump the counts if they grew (they only ever climb). Returns the fetched + /// counts (or null on failure / no stored hash) so the UI can confirm. + Future refreshCoreScopeCounts( int channelIndex, String messageId, String hashHex, ) async { - final count = await _coreScopeService.fetchObserverCount(hashHex); - if (count == null) return null; + final counts = await _coreScopeService.fetchCounts(hashHex); + if (counts == null) return null; _updateChannelMessageById(channelIndex, messageId, (m) { final current = m.coreScopeObserverCount ?? 0; - return count > current ? m.copyWith(coreScopeObserverCount: count) : m; + return counts.observers >= current + ? m.copyWith( + coreScopeObserverCount: counts.observers, + coreScopeObservationCount: counts.observations, + ) + : m; }); notifyListeners(); - return count; + return counts; } void _updateChannelMessageById( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 75beaf5..2fba431 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -886,6 +886,17 @@ }, "channel_coreScopeRefresh": "Refresh CoreScope observers", "channel_coreScopeRefreshFailed": "CoreScope unreachable", + "channel_coreScopeCounts": "{observers, plural, =1{1 observer} other{{observers} observers}} · {observations} observations", + "@channel_coreScopeCounts": { + "placeholders": { + "observers": { + "type": "int" + }, + "observations": { + "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 60ed869..0b35e7c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3100,6 +3100,12 @@ abstract class AppLocalizations { /// **'CoreScope unreachable'** String get channel_coreScopeRefreshFailed; + /// No description provided for @channel_coreScopeCounts. + /// + /// In en, this message translates to: + /// **'{observers, plural, =1{1 observer} other{{observers} observers}} · {observations} observations'** + String channel_coreScopeCounts(int observers, int observations); + /// 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 f224c2f..565c713 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1719,6 +1719,17 @@ class AppLocalizationsBg extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Успехи'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e8be34f..ac46c2e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1717,6 +1717,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Erfolgreich'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 879bfcd..e60307a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1687,6 +1687,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'successes'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 1fe615c..4fc5a2a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1714,6 +1714,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Éxitos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index b952743..48e1ade 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1721,6 +1721,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Succès'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 43b349f..6035ae3 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1726,6 +1726,17 @@ class AppLocalizationsHu extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'sikerek'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 2ec7193..ddc4eaf 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1716,6 +1716,17 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'successi'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 3fb763a..3464fd3 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1651,6 +1651,17 @@ class AppLocalizationsJa extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => '成功事例'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index b5c946f..979e4d4 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1647,6 +1647,17 @@ class AppLocalizationsKo extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => '성공 사례'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2bf67f4..d6a0cc8 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1704,6 +1704,17 @@ class AppLocalizationsNl extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Succesvol'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index fbe9376..c93f64d 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1729,6 +1729,17 @@ class AppLocalizationsPl extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Sukcesy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 95db95d..56e148a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1713,6 +1713,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Sucessos'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index d9964f4..408d120 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1717,6 +1717,17 @@ class AppLocalizationsRu extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'успешно'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 5771315..706eaed 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1706,6 +1706,17 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Úspechy'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 8f9230a..dbf321b 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1702,6 +1702,17 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Uspešni'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 8011941..533552d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1697,6 +1697,17 @@ class AppLocalizationsSv extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'framgångar'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index da55a4a..fad02d3 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1712,6 +1712,17 @@ class AppLocalizationsUk extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => 'Успішно'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8cbc2bc..cc9c1cc 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1626,6 +1626,17 @@ class AppLocalizationsZh extends AppLocalizations { @override String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; + @override + String channel_coreScopeCounts(int observers, int observations) { + String _temp0 = intl.Intl.pluralLogic( + observers, + locale: localeName, + other: '$observers observers', + one: '1 observer', + ); + return '$_temp0 · $observations observations'; + } + @override String get chat_successes => '成功'; diff --git a/lib/models/channel_message.dart b/lib/models/channel_message.dart index a7de3a3..b84a69b 100644 --- a/lib/models/channel_message.dart +++ b/lib/models/channel_message.dart @@ -56,6 +56,10 @@ class ChannelMessage { /// Unique observers CoreScope reports for this message's on-air packet /// (#524). Null until queried / when the feature is off. Transient. final int? coreScopeObserverCount; + + /// Total observations (sightings) CoreScope reports for the same packet, the + /// "Observations (N)" number in its UI. Transient. See [coreScopeObserverCount]. + final int? coreScopeObservationCount; final String? replyToMessageId; final String? replyToSenderName; final String? replyToText; @@ -96,6 +100,7 @@ class ChannelMessage { this.packetHash, this.onAirHash, this.coreScopeObserverCount, + this.coreScopeObservationCount, this.replyToMessageId, this.replyToSenderName, this.replyToText, @@ -144,6 +149,7 @@ class ChannelMessage { String? packetHash, String? onAirHash, int? coreScopeObserverCount, + int? coreScopeObservationCount, String? replyToMessageId, String? replyToSenderName, String? replyToText, @@ -187,6 +193,8 @@ class ChannelMessage { onAirHash: onAirHash ?? this.onAirHash, coreScopeObserverCount: coreScopeObserverCount ?? this.coreScopeObserverCount, + coreScopeObservationCount: + coreScopeObservationCount ?? this.coreScopeObservationCount, replyToMessageId: replyToMessageId ?? this.replyToMessageId, replyToSenderName: replyToSenderName ?? this.replyToSenderName, replyToText: replyToText ?? this.replyToText, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 347c498..5cc9a04 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1523,9 +1523,7 @@ class _ChannelChatScreenState extends State { if (isOutgoing && (message.coreScopeObserverCount ?? 0) > 0) ...[ dot, Tooltip( - message: context.l10n.channel_coreScopeTooltip( - message.coreScopeObserverCount!, - ), + message: _coreScopeLabel(message), child: InkWell( onTap: message.onAirHash == null ? null @@ -1642,25 +1640,52 @@ class _ChannelChatScreenState extends State { ); } + String _coreScopeLabel(ChannelMessage message) { + final observers = message.coreScopeObserverCount ?? 0; + final observations = message.coreScopeObservationCount; + return observations == null + ? context.l10n.channel_coreScopeTooltip(observers) + : context.l10n.channel_coreScopeCounts(observers, observations); + } + Future _refreshCoreScope(ChannelMessage message) async { final hash = message.onAirHash; if (hash == null) return; final connector = context.read(); final l10n = context.l10n; - final count = await connector.refreshCoreScopeObserverCount( + final counts = await connector.refreshCoreScopeCounts( message.channelIndex ?? _currentChannel.index, message.messageId, hash, ); if (!mounted) return; - showDismissibleSnackBar( - context, - content: Text( - count == null - ? l10n.channel_coreScopeRefreshFailed - : l10n.channel_coreScopeTooltip(count), + _showCoreScopeBanner( + counts == null + ? l10n.channel_coreScopeRefreshFailed + : l10n.channel_coreScopeCounts(counts.observers, counts.observations), + ); + } + + /// Top banner (out of the way of the composer), auto-dismissed after a few + /// seconds. Used for the CoreScope refresh result (#524). + void _showCoreScopeBanner(String message) { + final messenger = ScaffoldMessenger.of(context); + messenger.clearMaterialBanners(); + messenger.showMaterialBanner( + MaterialBanner( + content: Text(message), + leading: const Icon(Icons.cloud_outlined), + actions: [ + TextButton( + onPressed: messenger.hideCurrentMaterialBanner, + child: Text(context.l10n.common_ok), + ), + ], ), ); + Future.delayed(const Duration(seconds: 4), () { + if (mounted) messenger.hideCurrentMaterialBanner(); + }); } void _retryChannelMessage(ChannelMessage message) { @@ -1736,11 +1761,7 @@ class _ChannelChatScreenState extends State { leading: const Icon(Icons.cloud_outlined), title: Text(context.l10n.channel_coreScopeRefresh), subtitle: message.coreScopeObserverCount != null - ? Text( - context.l10n.channel_coreScopeTooltip( - message.coreScopeObserverCount!, - ), - ) + ? Text(_coreScopeLabel(message)) : null, onTap: () { Navigator.pop(sheetContext); diff --git a/lib/services/corescope_service.dart b/lib/services/corescope_service.dart index 32e876d..617b04b 100644 --- a/lib/services/corescope_service.dart +++ b/lib/services/corescope_service.dart @@ -7,6 +7,16 @@ import '../utils/app_logger.dart'; /// Default CoreScope instance (OKIMesh). Owner-run; its read API is public. const String kDefaultCoreScopeHost = 'map.okimesh.org'; +/// The two counts CoreScope reports for a packet. [observers] is the number of +/// distinct observers (the meaningful reach); [observations] is the total +/// sightings, which counts one observer hearing the packet via several paths +/// more than once (this is the "Observations (N)" number in CoreScope's UI). +class CoreScopeCounts { + const CoreScopeCounts({required this.observers, required this.observations}); + final int observers; + final int observations; +} + /// Queries a CoreScope instance for how many observers reported a given packet, /// keyed by the firmware/mesh packet hash (#524). Read-only and best-effort: /// any failure (offline, timeout, non-200, bad body, unknown packet) returns @@ -24,10 +34,10 @@ class CoreScopeService { final bool useTls; final Duration timeout; - /// Unique observers that reported the packet with [packetHash] - /// (16 lowercase hex chars). Returns null on any error, or when CoreScope - /// has no record of the hash yet. - Future fetchObserverCount(String packetHash) async { + /// Distinct observers and total observations for [packetHash] (16 lowercase + /// hex chars). Returns null on any error, or when CoreScope has no record of + /// the hash yet. + Future fetchCounts(String packetHash) async { if (packetHash.isEmpty) return null; final uri = Uri( scheme: useTls ? 'https' : 'http', @@ -58,13 +68,21 @@ class CoreScopeService { } final first = packets.first; if (first is! Map) return null; - final count = first['observer_count']; - final result = count is num ? count.toInt() : null; + final observers = first['observer_count']; + if (observers is! num) return null; + final observations = first['observation_count']; + final counts = CoreScopeCounts( + observers: observers.toInt(), + observations: observations is num + ? observations.toInt() + : observers.toInt(), + ); appLogger.info( - 'observer_count=$result for $packetHash', + 'observers=${counts.observers} observations=${counts.observations} ' + 'for $packetHash', tag: 'CoreScope', ); - return result; + return counts; } catch (e) { appLogger.warn('Query failed for $packetHash: $e', tag: 'CoreScope'); return null; diff --git a/test/services/corescope_service_test.dart b/test/services/corescope_service_test.dart index 2c9e0c7..fd141df 100644 --- a/test/services/corescope_service_test.dart +++ b/test/services/corescope_service_test.dart @@ -4,27 +4,43 @@ import 'package:http/testing.dart'; import 'package:meshcore_open/services/corescope_service.dart'; void main() { - group('CoreScopeService.fetchObserverCount', () { - test('parses observer_count from a grouped-by-hash response', () async { - late Uri captured; + group('CoreScopeService.fetchCounts', () { + test( + 'parses observer + observation counts from a grouped response', + () async { + late Uri captured; + final service = CoreScopeService( + host: 'map.okimesh.org', + client: MockClient((req) async { + captured = req.url; + return http.Response( + '{"packets":[{"hash":"abc123","observer_count":15,' + '"observation_count":34}],"total":1}', + 200, + ); + }), + ); + + final counts = await service.fetchCounts('abc123'); + expect(counts, isNotNull); + expect(counts!.observers, 15); + expect(counts.observations, 34); + expect(captured.host, 'map.okimesh.org'); + expect(captured.path, '/api/packets'); + expect(captured.queryParameters['hash'], 'abc123'); + expect(captured.queryParameters['groupByHash'], 'true'); + }, + ); + + test('falls back to observers when observation_count is absent', () async { final service = CoreScopeService( - host: 'map.okimesh.org', - client: MockClient((req) async { - captured = req.url; - return http.Response( - '{"packets":[{"hash":"abc123","observer_count":7}],"total":1}', - 200, - ); - }), + client: MockClient( + (_) async => http.Response('{"packets":[{"observer_count":9}]}', 200), + ), ); - - expect(await service.fetchObserverCount('abc123'), 7); - // Correct endpoint + query built from the hash. - expect(captured.scheme, 'https'); - expect(captured.host, 'map.okimesh.org'); - expect(captured.path, '/api/packets'); - expect(captured.queryParameters['hash'], 'abc123'); - expect(captured.queryParameters['groupByHash'], 'true'); + final counts = await service.fetchCounts('abc123'); + expect(counts!.observers, 9); + expect(counts.observations, 9); }); test('returns null when CoreScope has no record (empty packets)', () async { @@ -33,28 +49,28 @@ void main() { (_) async => http.Response('{"packets":[],"total":0}', 200), ), ); - expect(await service.fetchObserverCount('deadbeef'), isNull); + expect(await service.fetchCounts('deadbeef'), isNull); }); test('returns null on non-200', () async { final service = CoreScopeService( client: MockClient((_) async => http.Response('nope', 503)), ); - expect(await service.fetchObserverCount('abc123'), isNull); + expect(await service.fetchCounts('abc123'), isNull); }); test('returns null on malformed body', () async { final service = CoreScopeService( client: MockClient((_) async => http.Response('not json', 200)), ); - expect(await service.fetchObserverCount('abc123'), isNull); + expect(await service.fetchCounts('abc123'), isNull); }); test('returns null (never throws) on transport failure', () async { final service = CoreScopeService( client: MockClient((_) async => throw Exception('offline')), ); - expect(await service.fetchObserverCount('abc123'), isNull); + expect(await service.fetchCounts('abc123'), isNull); }); test( @@ -67,7 +83,7 @@ void main() { return http.Response('{}', 200); }), ); - expect(await service.fetchObserverCount(''), isNull); + expect(await service.fetchCounts(''), isNull); expect(called, isFalse); }, ); diff --git a/untranslated.json b/untranslated.json index f15eaec..9011080 100644 --- a/untranslated.json +++ b/untranslated.json @@ -54,6 +54,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -156,6 +157,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -258,6 +260,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -360,6 +363,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -462,6 +466,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -564,6 +569,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -666,6 +672,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -768,6 +775,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -870,6 +878,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -972,6 +981,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1074,6 +1084,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1176,6 +1187,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1278,6 +1290,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1380,6 +1393,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1482,6 +1496,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1584,6 +1599,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", @@ -1686,6 +1702,7 @@ "channel_coreScopeTooltip", "channel_coreScopeRefresh", "channel_coreScopeRefreshFailed", + "channel_coreScopeCounts", "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters",