diff --git a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart index 9e889700..be6dc5fe 100644 --- a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart +++ b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.dart @@ -940,6 +940,23 @@ class TabRepository extends _$TabRepository { return; } + // The tab is already in the target container, so there is nothing + // to reconcile. This notably fires when reassigning a tab into a + // container that shares the default Gecko context: assignContainer + // recreates the tab in the target container, and that new tab's + // load re-triggers this event. Without this guard the transiently + // empty new tab would be treated as an empty tab and churn yet + // another tab (re-prompting app-links). + final currentTabContainerId = await ref + .read(tabDataRepositoryProvider.notifier) + .getTabContainerId(currentTabState.id); + if (!ref.mounted) { + return; + } + if (currentTabContainerId == targetContainerId) { + return; + } + final tabIsEmpty = currentTabState.url == TabState.defaultUrl && currentTabState.historyState.items.isEmpty; @@ -985,7 +1002,11 @@ class TabRepository extends _$TabRepository { if (originUri == null) { await ref .read(tabDataRepositoryProvider.notifier) - .assignContainer(latestTabState.id, containerData); + .assignContainer( + latestTabState.id, + containerData, + replacementUrl: uri, + ); } else if (latestTabState.url == originUri) { await ref .read(tabDataRepositoryProvider.notifier) @@ -993,6 +1014,7 @@ class TabRepository extends _$TabRepository { latestTabState.id, containerData, closeOldTab: false, + replacementUrl: uri, ); } else { logger.w( diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.dart b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.dart index 55cc28d2..99432979 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/domain/repositories/tab.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; @@ -37,6 +38,14 @@ class TabDataRepository extends _$TabDataRepository { String tabId, ContainerData targetContainer, { bool closeOldTab = true, + // When the reassignment is driven by a navigation to a site assigned to + // [targetContainer] (see the site-assignment listener), the recreated tab + // must load that requested URL rather than the tab's current URL. The + // current URL can be a stale origin page (e.g. the search result the link + // was opened from) when the assignment fires before the navigation + // commits. Left null for manual moves (drag/drop, menu), which keep the + // tab on its current page. + Uri? replacementUrl, }) async { final selectedTabId = ref.read(selectedTabProvider); final tabState = ref.read(tabStatesProvider)[tabId]; @@ -52,8 +61,20 @@ class TabDataRepository extends _$TabDataRepository { final currentContainerData = await getTabContainerData(tabId); - if (targetContainer.metadata.contextualIdentity == - currentContainerData?.metadata.contextualIdentity) { + final sameContext = + targetContainer.metadata.contextualIdentity == + currentContainerData?.metadata.contextualIdentity; + + // A pure regrouping (no navigation) can stay in place when the Gecko + // context is unchanged — this is the manual-move case (drag/drop, menu), + // which passes no [replacementUrl] and keeps the tab on its current page. + // + // An assignment-driven move ([replacementUrl] set) must send the tab to the + // requested site. Reloading in place on the origin tab is unreliable after + // an app-link "open in app" cancel (the engine session is left in a state + // where the load never commits), so we recreate the tab in that case even + // when the context is unchanged. The fresh session loads the URL reliably. + if (sameContext && replacementUrl == null) { await ref .read(tabDatabaseProvider) .tabDao @@ -67,13 +88,19 @@ class TabDataRepository extends _$TabDataRepository { await ref .read(tabRepositoryProvider.notifier) .addTab( - url: tabState.url, + url: replacementUrl ?? tabState.url, tabMode: tabState.tabMode, containerSelection: TabContainerSelection.specific( targetContainer, ), // parentId defaults to null - breaks parent chain when changing contextual identity selectTab: selectedTabId == tabState.id, + // Assignment-driven navigation to an assigned site: bypass the + // app-links delegate so cancelling an "open in app" prompt does + // not re-trigger it on the recreated tab's load. + flags: replacementUrl != null + ? LoadUrlFlags.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE + : LoadUrlFlags.NONE, ); } }