mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
## Summary - Align mobile home avatars and share the same sticky-date and Latest controls across channels and threads. - Stabilize thread tail navigation and emoji category scrolling. - Remove iOS glass compositing artifacts and match long-press blur across action surfaces. ## Why Threads and emoji selection used competing scroll and compositing paths, causing rebounds, flicker, and inconsistent controls. ## Testing - `just mobile-check` - `flutter test` (1,529 tests) - Signed Release installed, launched, and reviewed on a connected iPhone --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
98 lines
3.7 KiB
Dart
98 lines
3.7 KiB
Dart
import 'package:flutter/widgets.dart';
|
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
|
|
|
/// Settles an ordinary thread open on the latest hydrated reply after layout.
|
|
///
|
|
/// Scheduling again before completion invalidates callbacks aimed at an older
|
|
/// tail, allowing a rebuild with newly arrived replies to choose the target.
|
|
class InitialThreadTailSettle {
|
|
var _generation = 0;
|
|
var _isComplete = false;
|
|
|
|
/// Whether no more settling is needed.
|
|
///
|
|
/// Completion occurs when there is no tail target, the target is already
|
|
/// visible after hydration has settled, or the scheduled scroll finishes.
|
|
bool get isComplete => _isComplete;
|
|
|
|
/// Permanently abandons initial settling and invalidates queued callbacks.
|
|
///
|
|
/// This is terminal: later scheduling remains disabled even if the user
|
|
/// returns to the tail and resumes ordinary follow behavior.
|
|
void abandon() {
|
|
_generation++;
|
|
_isComplete = true;
|
|
}
|
|
|
|
/// Schedules a settle after each hydrated thread layout until [isComplete].
|
|
///
|
|
/// A later schedule replaces an earlier target while replies are still
|
|
/// arriving. The final target is left in place when already visible; otherwise
|
|
/// it scrolls into the viewport between the measured top and bottom overlays.
|
|
void schedule({
|
|
required BuildContext context,
|
|
required ItemScrollController controller,
|
|
required ItemPositionsListener positionsListener,
|
|
required int? targetIndex,
|
|
required double hiddenTopFraction,
|
|
required double hiddenBottomFraction,
|
|
required VoidCallback onSettled,
|
|
}) {
|
|
if (_isComplete) return;
|
|
|
|
final generation = ++_generation;
|
|
if (targetIndex == null) {
|
|
_isComplete = true;
|
|
onSettled();
|
|
return;
|
|
}
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!context.mounted || generation != _generation) return;
|
|
|
|
// Let events received during hydration rebuild the list before committing
|
|
// the target. That rebuild schedules a new generation at the current tail.
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!context.mounted ||
|
|
!controller.isAttached ||
|
|
generation != _generation) {
|
|
return;
|
|
}
|
|
final targetIsFullyVisible = positionsListener.itemPositions.value.any(
|
|
(position) =>
|
|
position.index == targetIndex &&
|
|
position.itemLeadingEdge >= hiddenTopFraction &&
|
|
position.itemTrailingEdge <= 1 - hiddenBottomFraction,
|
|
);
|
|
// Short threads already expose their tail from the top anchor. Moving
|
|
// that fully visible target down would only add empty space above the
|
|
// head. A clipped tail still takes the measured correction path.
|
|
if (targetIsFullyVisible) {
|
|
_isComplete = true;
|
|
onSettled();
|
|
return;
|
|
}
|
|
// This package uses a temporary second list for distant targets. The
|
|
// caller keeps the hydrated viewport unpainted until this one-frame
|
|
// placement completes, so that implementation detail cannot appear as
|
|
// an entry bounce.
|
|
controller
|
|
.scrollTo(
|
|
index: targetIndex,
|
|
alignment: hiddenTopFraction,
|
|
duration: const Duration(milliseconds: 1),
|
|
)
|
|
.whenComplete(() {
|
|
if (generation != _generation) return;
|
|
_isComplete = true;
|
|
onSettled();
|
|
});
|
|
});
|
|
// A post-frame callback does not itself request the frame in which it
|
|
// runs. Slow hydration can otherwise leave this settle parked until an
|
|
// unrelated redraw.
|
|
WidgetsBinding.instance.scheduleFrame();
|
|
});
|
|
}
|
|
}
|