Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -2094,3 +2094,27 @@ msgstr "%s people"
# Shown instead of data figures while sharing via Share My Connection.
msgid "advanced_stats_smc_note"
msgstr "Data stats are available only when sharing via Unbounded."

# Sharing-mode names. "Direct connection" is Share My Connection, where people
# connect straight to this device through a port on the user's router.
# "WebRTC" is the Unbounded relay used when that port can't be opened; keep
# it as the technical name.
msgid "share_mode_smc"
msgstr "Direct connection"

msgid "share_mode_unbounded"
msgstr "WebRTC"

# Status line on the Action Mode card: the first %s is the status (e.g.
# "Enabled"), the second is the sharing mode (e.g. "WebRTC").
msgid "smc_status_with_mode"
msgstr "%s · %s"

msgid "advanced_stats_mode"
msgstr "Sharing mode"

# Under the mode when Lantern switched to Unbounded because a direct
# connection failed. %s is the reason, e.g. "Your router accepted the port
# mapping but isn't forwarding traffic to this computer."
msgid "advanced_stats_mode_fallback"
msgstr "Direct connection unavailable: %s"
5 changes: 5 additions & 0 deletions lib/core/models/share_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class ShareState {
/// SmC only; Unbounded reports readiness via [unboundedRunning].
final SharePhase phase;
final String? errorMessage;
/// Why SmC could not start, when this Unbounded session is its fallback.
/// Set only by the fallback itself; copyWith carries it unchanged.
final String? fallbackReason;
final RelayStats relay;

const ShareState({
Expand All @@ -54,6 +57,7 @@ class ShareState {
this.totalCount = 0,
this.phase = SharePhase.idle,
this.errorMessage,
this.fallbackReason,
this.relay = const RelayStats(),
});

Expand All @@ -80,6 +84,7 @@ class ShareState {
errorMessage: identical(errorMessage, _unsetErrorMessage)
? this.errorMessage
: errorMessage as String?,
fallbackReason: fallbackReason,
Comment thread
myleshorton marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
relay: relay ?? this.relay,
);
}
Expand Down
8 changes: 4 additions & 4 deletions lib/features/action_mode/action_mode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import 'package:lantern/features/home/provider/app_setting_notifier.dart';
import 'package:lantern/features/action_mode/action_mode_widgets.dart';
import 'package:lantern/features/action_mode/advanced_stats_sheet.dart';
import 'package:lantern/features/action_mode/peer_status_pill.dart';
import 'package:lantern/features/action_mode/auto_enable_mode.dart';
import 'package:lantern/features/action_mode/provider/share_notifier.dart';
import 'package:lantern/features/action_mode/provider/action_mode_tab_visible_notifier.dart';
import 'package:lantern/features/action_mode/action_mode_globe.dart';
Expand Down Expand Up @@ -71,8 +70,6 @@ class ActionModeTab extends HookConsumerWidget {
onToggle: () =>
ref.read(shareProvider.notifier).toggle(context, ref),
),
const SizedBox(height: 8),
const ActionModeAutoEnable(),
],
),
),
Expand Down Expand Up @@ -118,9 +115,12 @@ class _StatusCard extends StatelessWidget {
_ => 'smc_status_configuring'.i18n,
},
};
final mode = shareModeLabel(state.mode);

return ActionModeStatusCard(
status: status,
status: hasError || mode == null
? status
: 'smc_status_with_mode'.i18n.fill([status, mode]),
enabled: state.active || state.probing,
ready:
(state.mode == ShareMode.unbounded && state.unboundedRunning) ||
Expand Down
27 changes: 26 additions & 1 deletion lib/features/action_mode/advanced_stats_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import 'package:lantern/core/widgets/info_row.dart';
import 'package:lantern/core/widgets/section_label.dart';
import 'package:lantern/features/action_mode/provider/share_notifier.dart';

/// The user-facing name of a sharing mode, or null when not sharing.
String? shareModeLabel(ShareMode mode) => switch (mode) {
ShareMode.smc => 'share_mode_smc'.i18n,
ShareMode.unbounded => 'share_mode_unbounded'.i18n,
ShareMode.off => null,
};

void showAdvancedStatsSheet(BuildContext context) {
showAppBottomSheet(
context: context,
Expand Down Expand Up @@ -49,6 +56,19 @@ class AdvancedStatsView extends ConsumerWidget {
AppCard(
child: Column(
children: [
_StatRow(
key: const Key('advanced-stats.mode'),
label: 'advanced_stats_mode'.i18n,
value: shareModeLabel(state.mode) ?? 'smc_status_off'.i18n,
detail:
state.mode == ShareMode.unbounded &&
state.fallbackReason != null
? 'advanced_stats_mode_fallback'.i18n.fill([
state.fallbackReason!,
])
: null,
),
divider,
if (!smc) ...[
_StatRow(
label: 'advanced_stats_current_speed'.i18n,
Expand Down Expand Up @@ -125,7 +145,12 @@ class AdvancedStatsView extends ConsumerWidget {
String _directions(String down, String up) => '↓ $down ↑ $up';

class _StatRow extends StatelessWidget {
const _StatRow({required this.label, required this.value, this.detail});
const _StatRow({
super.key,
required this.label,
required this.value,
this.detail,
});
final String label, value;
final String? detail;

Expand Down
9 changes: 6 additions & 3 deletions lib/features/action_mode/provider/share_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ class ShareNotifier extends _$ShareNotifier {
appLogger.error(
'SmC setPeerProxy failed, falling back to Unbounded: ${err.error}',
);
unawaited(_fallbackToUnbounded(widgetRef));
unawaited(_fallbackToUnbounded(widgetRef, err.error));
}, (_) => null);
case ShareMode.unbounded:
final res = await widgetRef
Expand Down Expand Up @@ -332,7 +332,7 @@ class ShareNotifier extends _$ShareNotifier {

/// Switches a failed SmC session to Unbounded. radiance has already rolled
/// its peer-share setting back, so only the local mode and broflake remain.
Future<void> _fallbackToUnbounded(WidgetRef widgetRef) async {
Future<void> _fallbackToUnbounded(WidgetRef widgetRef, String? reason) async {
if (state.mode == ShareMode.unbounded) return;
_changes++;
try {
Expand All @@ -341,6 +341,7 @@ class ShareNotifier extends _$ShareNotifier {
active: true,
mode: ShareMode.unbounded,
totalCount: state.totalCount,
fallbackReason: (reason?.isNotEmpty ?? false) ? reason : null,
relay: state.relay.newSession(),
);
final result = await widgetRef
Expand Down Expand Up @@ -430,7 +431,7 @@ class ShareNotifier extends _$ShareNotifier {
appLogger.info(
'SmC start failed, falling back to Unbounded: ${errMsg ?? ""}',
);
unawaited(_fallbackToUnbounded(widgetRef));
unawaited(_fallbackToUnbounded(widgetRef, errMsg));
return;
}
if (phase == SharePhase.idle) {
Expand Down Expand Up @@ -470,6 +471,8 @@ class ShareNotifier extends _$ShareNotifier {
);
ref.read(appSettingProvider.notifier).setUnboundedTotalHelped(newTotal);
unawaited(_resolveAndEmit(ip, arc));
// SmC has no byte counters, so the country row gains a person only.
unawaited(_attributeToCountry(ip, (down: 0, up: 0), arrived: true));
Comment thread
myleshorton marked this conversation as resolved.
} else if (eventState == -1) {
final entry = _peerArcs[ip];
if (entry == null) return;
Expand Down
69 changes: 64 additions & 5 deletions test/features/action_mode/action_mode_widgets_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,15 @@ void main() {
await tester.tapAt(tester.getCenter(toggle) - const Offset(20, 0));
await tester.pump(const Duration(milliseconds: 300));
expect(_Share.toggles, 1);
final checkbox = find.byKey(const Key('action-mode.auto-enable'));
await tester.ensureVisible(checkbox);
final link = find.byKey(const Key('action-mode.advanced-stats'));
await tester.ensureVisible(link);
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(checkbox);
await tester.pump(const Duration(milliseconds: 300));
expect(tester.widget<Checkbox>(checkbox).value, isTrue);
await tester.tap(link);
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(AdvancedStatsView), findsOneWidget);
// Auto-enable lives in Action Mode settings, not on the tab.
expect(find.byType(ActionModeAutoEnable), findsNothing);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox());
},
Expand Down Expand Up @@ -621,6 +624,62 @@ void main() {
expect(find.text('Current speed'), findsNothing);
expect(find.text('Data relayed this session'), findsNothing);
expect(find.text('Data relayed all time'), findsOneWidget);
expect(find.text('Direct connection'), findsOneWidget);
expect(tester.takeException(), isNull);
});

for (final (mode, label) in [
(ShareMode.unbounded, 'Enabled · WebRTC'),
(ShareMode.smc, 'Enabled · Direct connection'),
]) {
testWidgets('status line names the $mode mode', (tester) async {
await mount(
tester,
const ActionModeTab(),
shareState: ShareState(
active: true,
mode: mode,
phase: mode == ShareMode.smc ? SharePhase.serving : SharePhase.idle,
unboundedRunning: mode == ShareMode.unbounded,
),
animated: true,
);
final card = tester.widget<ActionModeStatusCard>(
find.byType(ActionModeStatusCard),
);
expect(card.status, label);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox());
});
}

testWidgets('advanced stats explains a fallback to WebRTC', (tester) async {
await mount(
tester,
const AdvancedStatsView(),
shareState: const ShareState(
active: true,
mode: ShareMode.unbounded,
unboundedRunning: true,
fallbackReason: "Your router isn't forwarding traffic.",
relay: relay,
),
);
final mode = find.byKey(const Key('advanced-stats.mode'));
expect(
find.descendant(of: mode, matching: find.text('WebRTC')),
findsOneWidget,
);
expect(
find.descendant(
of: mode,
matching: find.text(
"Direct connection unavailable: Your router isn't forwarding "
'traffic.',
),
),
findsOneWidget,
);
expect(tester.takeException(), isNull);
});
}
5 changes: 5 additions & 0 deletions test/features/action_mode/snapshot_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,10 @@ void main() {
);
await tester.pump();
expect(container.read(shareProvider).activeCount, 1);
// SmC peers count toward their country too, with no bytes.
final us = container.read(shareProvider).relay.countries['US']!;
expect(us.people, 3);
expect(us.down, 900);
service.events.add(
AppEvent(
eventType: 'peer-status',
Expand All @@ -558,6 +562,7 @@ void main() {
await tester.pump();
expect(container.read(shareProvider).mode, ShareMode.unbounded);
expect(container.read(shareProvider).activeCount, 0);
expect(container.read(shareProvider).fallbackReason, 'port unreachable');
service.events.add(
AppEvent(
eventType: 'unbounded-snapshot',
Expand Down
Loading