From 33fd8e65f15e2ede16bae2d9342ca7d024f391e2 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 26 May 2026 13:42:16 +0200 Subject: [PATCH] fix new tab assignments --- .../geckoview/domain/repositories/tab.dart | 43 +++++++++++- .../domain/services/sharing_intent.dart | 14 ++-- .../domain/repositories/tab_test.dart | 67 +++++++++++++++++++ 3 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 apps/weblibre/test/features/geckoview/domain/repositories/tab_test.dart diff --git a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart index 6ca04393..e7ba70f1 100644 --- a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart +++ b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart @@ -21,12 +21,14 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:nullability/nullability.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:synchronized/synchronized.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/extensions/uri.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart'; import 'package:weblibre/features/geckoview/domain/providers.dart'; @@ -50,6 +52,20 @@ import 'package:weblibre/utils/debouncer.dart'; part 'tab.g.dart'; +@visibleForTesting +ContainerData? resolveAssignedContainerForTabOpen({ + required TabContainerSelection containerSelection, + required ContainerData? requestedContainer, + required ContainerData? siteAssignedContainer, +}) { + return switch (containerSelection) { + UseSelectedContainerTabSelection() => + siteAssignedContainer ?? requestedContainer, + UnassignedContainerTabSelection() || + SpecificContainerTabSelection() => requestedContainer, + }; +} + sealed class TabBackPromptBehavior { const TabBackPromptBehavior(); } @@ -124,13 +140,32 @@ class TabRepository extends _$TabRepository { }) async { final tabDao = ref.read(tabDatabaseProvider).tabDao; - final assignedContainer = switch (containerSelection) { + var assignedContainer = switch (containerSelection) { UseSelectedContainerTabSelection() => await ref.read(selectedContainerProvider.notifier).fetchData(), UnassignedContainerTabSelection() => null, SpecificContainerTabSelection(:final container) => container, }; + if (tabMode is RegularTabMode && + url != null && + url.hasAuthority && + url.isHttpOrHttps) { + final siteAssignedContainerId = await ref + .read(containerRepositoryProvider.notifier) + .siteAssignedContainerId(url); + final siteAssignedContainer = await siteAssignedContainerId.mapNotNull( + (id) => + ref.read(containerRepositoryProvider.notifier).getContainerData(id), + ); + + assignedContainer = resolveAssignedContainerForTabOpen( + containerSelection: containerSelection, + requestedContainer: assignedContainer, + siteAssignedContainer: siteAssignedContainer, + ); + } + // For isolated tabs, skip parent context validation since // isolated tabs use their own immutable context ID. final validatedParentId = tabMode is IsolatedTabMode @@ -869,7 +904,7 @@ class TabRepository extends _$TabRepository { currentTabState.historyState.items.isEmpty; if (event.blocked || tabIsEmpty) { - await addTab( + final newTabId = await addTab( url: uri, tabMode: currentTabState.tabMode, containerSelection: TabContainerSelection.specific( @@ -885,6 +920,10 @@ class TabRepository extends _$TabRepository { if (currentTabState.historyState.items.isEmpty) { await closeTab(currentTabState.id); + if (!ref.mounted) { + return; + } + await selectTab(newTabId); } } else { final tabContainerId = await ref diff --git a/apps/weblibre/lib/features/share_intent/domain/services/sharing_intent.dart b/apps/weblibre/lib/features/share_intent/domain/services/sharing_intent.dart index 9d5ca6c5..c763bcc7 100644 --- a/apps/weblibre/lib/features/share_intent/domain/services/sharing_intent.dart +++ b/apps/weblibre/lib/features/share_intent/domain/services/sharing_intent.dart @@ -65,11 +65,17 @@ _buildSharingIntentTransformer( final shortcutContextId = intent.action == 'android.intent.action.VIEW' ? intent.extra['pwa_context_id'] as String? : null; + final shortcutContainerMode = + intent.extra['shortcut_container_mode'] as String?; + final hasShortcutContainerMetadata = + shortcutContextId != null || shortcutContainerMode != null; final containerMode = intent.action == 'android.intent.action.VIEW' - ? IntentContainerMode.fromWireValue( - intent.extra['shortcut_container_mode'] as String?, - contextId: shortcutContextId, - ) + ? hasShortcutContainerMetadata + ? IntentContainerMode.fromWireValue( + shortcutContainerMode, + contextId: shortcutContextId, + ) + : IntentContainerMode.unassigned : IntentContainerMode.useSelected; final allowed = await gatekeeper.shouldAllow( diff --git a/apps/weblibre/test/features/geckoview/domain/repositories/tab_test.dart b/apps/weblibre/test/features/geckoview/domain/repositories/tab_test.dart new file mode 100644 index 00000000..19881c7b --- /dev/null +++ b/apps/weblibre/test/features/geckoview/domain/repositories/tab_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart'; +import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; + +void main() { + group('resolveAssignedContainerForTabOpen', () { + final selectedContainer = _container('selected', 'selected-context'); + final siteAssignedContainer = _container('assigned', 'assigned-context'); + + test('prefers the site-assigned container for useSelected', () { + final resolved = resolveAssignedContainerForTabOpen( + containerSelection: const TabContainerSelection.useSelected(), + requestedContainer: selectedContainer, + siteAssignedContainer: siteAssignedContainer, + ); + + expect(resolved, siteAssignedContainer); + }); + + test( + 'falls back to the requested container when no site assignment exists', + () { + final resolved = resolveAssignedContainerForTabOpen( + containerSelection: const TabContainerSelection.useSelected(), + requestedContainer: selectedContainer, + siteAssignedContainer: null, + ); + + expect(resolved, selectedContainer); + }, + ); + + test('preserves an explicit specific container selection', () { + final explicitContainer = _container('explicit', 'explicit-context'); + + final resolved = resolveAssignedContainerForTabOpen( + containerSelection: TabContainerSelection.specific(explicitContainer), + requestedContainer: explicitContainer, + siteAssignedContainer: siteAssignedContainer, + ); + + expect(resolved, explicitContainer); + }); + + test('preserves an explicit unassigned selection', () { + final resolved = resolveAssignedContainerForTabOpen( + containerSelection: const TabContainerSelection.unassigned(), + requestedContainer: null, + siteAssignedContainer: siteAssignedContainer, + ); + + expect(resolved, isNull); + }); + }); +} + +ContainerData _container(String id, String contextId) { + return ContainerData( + id: id, + name: id, + color: Colors.blue, + orderKey: id, + metadata: ContainerMetadata.withDefaults(contextualIdentity: contextId), + ); +}