Skip to content
Merged
5 changes: 5 additions & 0 deletions lib/providers/collab/remote_strategy_snapshot_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ class RemoteEditorSnapshotNotifier
} else {
await _refreshFromServer();
}
// Closed, or another strategy opened, while this one was being read:
// there is nothing left to watch for it.
if (_activeStrategyPublicId != strategyPublicId) return;
await _startShellSubscription(strategyPublicId);
final pageId = _activePagePublicId;
if (pageId != null) {
Expand Down Expand Up @@ -383,6 +386,8 @@ class RemoteEditorSnapshotNotifier
_pagesToOpen(shell).firstOrNull ?? shell.pages.firstOrNull?.publicId;

void clear() {
// A read still on its way must not bring the strategy back.
_readEpoch += 1;
_activeStrategyPublicId = null;
_activePagePublicId = null;
_shareToken = null;
Expand Down
61 changes: 49 additions & 12 deletions lib/providers/share_link_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ final pendingShareCodeStoreProvider = Provider<PendingShareCodeStore>(
(ref) => createPendingShareCodeStore(),
);

/// The page's own URL on web (where a share link arrives), null on native.
/// Overridden in tests.
final sharePageUrlProvider = Provider<Uri?>((ref) => kIsWeb ? Uri.base : null);
/// Reads the page's own URL on web (where a share link arrives), null on
/// native. Read each time it is needed: the router and this controller both
/// change it while the page stays open. Overridden in tests.
final sharePageUrlProvider = Provider<Uri? Function()>(
(ref) => () => kIsWeb ? Uri.base : null,
);

/// Replaces the address bar URL without navigating. Overridden in tests.
final replaceBrowserUrlProvider =
Expand Down Expand Up @@ -281,6 +284,29 @@ class ShareLinkController extends Notifier<String?> {
return redeemPendingIfPossible(source: 'add_to_library', explicit: true);
}

/// Whether a page reload reopens the held link. The editor says so as the
/// open strategy changes (see openCloudStrategyRecorderProvider): no while
/// the reader has a strategy of their own open, because a reload reopens
/// that one, and yes again once the strategy they read through the link is
/// back. Without this a reload restored both, and the two editors fought.
/// The link stays held in memory either way, so going back to its strategy
/// in this tab still reads through it.
void reopenOnReload(bool reopen) {
final token = state;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// Only a link that has opened. One still waiting to open (it arrived
// offline, say) keeps its code and its address: a reload is its retry.
if (token == null || ref.read(shareLinkViewProvider)?.token != token) {
return;
}
final store = ref.read(pendingShareCodeStoreProvider);
if (reopen) {
store.write(token);
} else {
store.clear();
}
_pointPageUrlAt(reopen ? token : null);
}

/// Whether [token]'s attempt is stale: a newer code arrived, or sign-in
/// redeemed this one while the attempt was waiting on the server.
bool _superseded(String token, int generation) =>
Expand Down Expand Up @@ -310,10 +336,14 @@ class ShareLinkController extends Notifier<String?> {
/// finishing late cannot clear a newer code that arrived meanwhile.
int _generation = 0;

/// The held code, the stored code and the address say the same link from
/// here on: a reload opens the link in the address, so an address left on
/// an older link would retry that one and drop this one.
void _hold(String token) {
_generation += 1;
state = token;
ref.read(pendingShareCodeStoreProvider).write(token);
_pointPageUrlAt(token);
}

void _release(int generation) {
Expand All @@ -322,24 +352,31 @@ class ShareLinkController extends Notifier<String?> {
}
state = null;
ref.read(pendingShareCodeStoreProvider).clear();
_dropSharePathFromPageUrl();
_pointPageUrlAt(null);
}

/// Once a code is done with, the page must not keep its /share/<code> URL,
/// Makes the page's URL the link for [code], or drops the link from it
/// when [code] is null. A reload opens whatever link the URL names, so
/// once a code is done with the page must not keep its /share/<code> URL,
/// or every reload would redeem it again (and, after the link is disabled,
/// report a failure to someone who still has access). The fragment stays:
/// Flutter's router keeps the open route there.
void _dropSharePathFromPageUrl() {
final page = ref.read(sharePageUrlProvider);
if (page == null ||
!isIcarusShareUri(page, currentOrigin: currentShareOrigin())) {
return;
}
void _pointPageUrlAt(String? code) {
final page = ref.read(sharePageUrlProvider)();
if (page == null) return;
final origin = currentShareOrigin();
final isLink = isIcarusShareUri(page, currentOrigin: origin);
final alreadyThere = code == null
? !isLink
: isLink &&
extractIcarusShareCode(page.toString(), currentOrigin: origin) ==
code;
if (alreadyThere) return;
ref.read(replaceBrowserUrlProvider)(Uri(
scheme: page.scheme,
host: page.host,
port: page.hasPort ? page.port : null,
path: '/',
path: code == null ? '/' : '/share/$code',
fragment: page.fragment.isEmpty ? null : page.fragment,
));
}
Expand Down
72 changes: 48 additions & 24 deletions lib/providers/strategy_page_session_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:math' show max;
import 'package:flutter/foundation.dart' show listEquals, setEquals;
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icarus/strategy/open_attempt.dart';
import 'package:hive_ce/hive.dart';
import 'package:icarus/collab/cloud_lineup_rows.dart';
import 'package:icarus/collab/collab_models.dart';
Expand Down Expand Up @@ -330,13 +331,17 @@ class StrategyPageSessionNotifier extends Notifier<StrategyPageSessionState> {
required StrategySource source,
required bool selectFirstPageIfNeeded,
String? preferredPageId,
OpenAttempt? attempt,
}) async {
// The open this is a step of, if any. It stops here too when the open
// gave way (see [OpenAttempt]).
final opening = attempt ?? OpenAttempt.unchallenged;
_pageSessionGeneration++;
// As in reset: a switch this supersedes leaves its transition here.
ref.read(transitionProvider.notifier).complete();
state = state.copyWith(transitionState: PageTransitionState.idle);
final pageSource = _resolvePageSource(strategyId, source);
final pageIds = await pageSource.listPageIds();
final pageIds = await opening.wait(pageSource.listPageIds());
// Drawn from this computer's copy, the page the copy could show is the
// only one there is to open (see RemoteEditorSnapshotNotifier).
final copiedPage = source == StrategySource.cloud
Expand Down Expand Up @@ -368,7 +373,9 @@ class StrategyPageSessionNotifier extends Notifier<StrategyPageSessionState> {
);

if (selected != null) {
await _rehydrateActivePageFromSource(selected);
await opening.wait(
_rehydrateActivePageFromSource(selected, attempt: opening),
);
}
// Snapshots that arrived while the strategy was opening went unheard:
// catch up on pages that came back while it was closed.
Expand Down Expand Up @@ -1185,11 +1192,15 @@ class StrategyPageSessionNotifier extends Notifier<StrategyPageSessionState> {
}
}

/// Reads the page an open starts on and puts it on the canvas. A step of
/// the open [attempt] belongs to, so it stops like one (see [OpenAttempt]).
Future<void> _rehydrateActivePageFromSource(
String pageId, {
required OpenAttempt attempt,
_RemotePageHydrationKey? hydrationKey,
bool preserveTextDrafts = false,
}) async {
final opening = attempt;
final strategyState = ref.read(strategyProvider);
final strategyId = strategyState.strategyId;
final source = strategyState.source;
Expand All @@ -1208,14 +1219,17 @@ class StrategyPageSessionNotifier extends Notifier<StrategyPageSessionState> {
);
}
final pageSource = _resolvePageSource(strategyId, source);
final pageData = await pageSource.loadPage(pageId);
await _applyLoadedPageData(
pageData,
strategyId: strategyId,
source: source,
hydrationKey: hydrationKey,
preserveTextDrafts: preserveTextDrafts,
loadedRemoteSnapshot: pageSource.loadedRemoteSnapshot,
final pageData = await opening.wait(pageSource.loadPage(pageId));
await opening.wait(
_applyLoadedPageData(
pageData,
strategyId: strategyId,
source: source,
hydrationKey: hydrationKey,
preserveTextDrafts: preserveTextDrafts,
loadedRemoteSnapshot: pageSource.loadedRemoteSnapshot,
attempt: opening,
),
);
}

Expand All @@ -1228,9 +1242,14 @@ class StrategyPageSessionNotifier extends Notifier<StrategyPageSessionState> {
RemoteEditorSnapshot? loadedRemoteSnapshot,
({MapValue map, StrategyThemeState theme})? preservedMetadata,
bool Function()? canApply,
OpenAttempt? attempt,
}) async {
final availablePageIds =
await _resolvePageSource(strategyId, source).listPageIds();
// The open this page is read for, if it is one: a page read for an open
// that gave way must not replace the canvas of what took its place.
final opening = attempt ?? OpenAttempt.unchallenged;
final availablePageIds = await opening.wait(
_resolvePageSource(strategyId, source).listPageIds(),
);
if (canApply != null && !canApply()) return;
final preserveHistory = source == StrategySource.cloud &&
_lastHydratedRemotePageKey?.strategyPublicId == strategyId &&
Expand All @@ -1252,13 +1271,15 @@ class StrategyPageSessionNotifier extends Notifier<StrategyPageSessionState> {
? Map<String, String>.from(ref.read(textDraftProvider))
: const <String, String>{};
try {
await applyStrategyEditorPageData(
ref,
pageData,
themeProfileId: themeProfileId,
themeOverridePalette: themeOverridePalette,
preserveHistory: preserveHistory,
mapOverride: preservedMetadata?.map,
await opening.wait(
applyStrategyEditorPageData(
ref,
pageData,
themeProfileId: themeProfileId,
themeOverridePalette: themeOverridePalette,
preserveHistory: preserveHistory,
mapOverride: preservedMetadata?.map,
),
);
for (final entry in retainedTextDrafts.entries) {
ref.read(textDraftProvider.notifier).setDraft(entry.key, entry.value);
Expand Down Expand Up @@ -1286,11 +1307,14 @@ class StrategyPageSessionNotifier extends Notifier<StrategyPageSessionState> {
: hydrationKey,
);
} finally {
state = state.copyWith(
activePageId: pageData.pageId,
isApplyingPage: false,
);
_resumePendingRemoteReapplyIfPossible();
// After an open gave way the session belongs to what replaced it.
if (opening.isCurrent) {
state = state.copyWith(
activePageId: pageData.pageId,
isApplyingPage: false,
);
_resumePendingRemoteReapplyIfPossible();
}
}
}

Expand Down
Loading
Loading