···11+import 'package:riverpod_annotation/riverpod_annotation.dart';
22+33+part 'thread_collapse_state.g.dart';
44+55+/// Manages collapse/expand state for thread posts.
66+///
77+/// State is session-only and keyed by post URI. Posts are expanded by default.
88+@riverpod
99+class ThreadCollapseState extends _$ThreadCollapseState {
1010+ @override
1111+ Map<String, bool> build() => {};
1212+1313+ /// Check if a post is collapsed.
1414+ ///
1515+ /// Returns false (expanded) if post URI is not in the map.
1616+ bool isCollapsed(String postUri) => state[postUri] ?? false;
1717+1818+ /// Toggle collapse state for a post.
1919+ void toggle(String postUri) {
2020+ state = {...state, postUri: !(state[postUri] ?? false)};
2121+ }
2222+2323+ /// Collapse all posts in the provided list.
2424+ void collapseAll(List<String> uris) {
2525+ final newState = {...state};
2626+ for (final uri in uris) {
2727+ newState[uri] = true;
2828+ }
2929+ state = newState;
3030+ }
3131+3232+ /// Expand all posts (clear all collapse state).
3333+ void expandAll() {
3434+ state = {};
3535+ }
3636+3737+ /// Collapse a specific post.
3838+ void collapse(String postUri) {
3939+ state = {...state, postUri: true};
4040+ }
4141+4242+ /// Expand a specific post.
4343+ void expand(String postUri) {
4444+ state = {...state, postUri: false};
4545+ }
4646+}
···11+import 'package:lazurite/src/features/thread/domain/thread.dart';
22+33+/// Utility class for thread layout calculations.
44+///
55+/// Provides constants and methods for computing indentation, connector
66+/// positioning, and depth management for nested thread rendering.
77+class ThreadLayoutCalculator {
88+ ThreadLayoutCalculator._();
99+1010+ /// Base indentation for depth 0 posts
1111+ static const double baseIndent = 0.0;
1212+1313+ /// Additional indentation per nesting level
1414+ static const double indentPerLevel = 32.0;
1515+1616+ /// Maximum depth before flattening
1717+ static const int maxDepth = 5;
1818+1919+ /// Avatar radius in pixels
2020+ static const double avatarRadius = 20.0;
2121+2222+ /// Offset from left edge to avatar center
2323+ static const double avatarCenterOffset = 20.0;
2424+2525+ /// Calculate left padding for a post at given depth.
2626+ ///
2727+ /// Depth is clamped to [maxDepth] to prevent infinite horizontal scrolling.
2828+ static double calculateIndent(int depth) {
2929+ final effectiveDepth = depth.clamp(0, maxDepth);
3030+ return baseIndent + (effectiveDepth * indentPerLevel);
3131+ }
3232+3333+ /// Calculate connector line position for a given depth.
3434+ ///
3535+ /// Positions the connector at the avatar center, accounting for indentation.
3636+ static double calculateConnectorLeft(int depth) {
3737+ return calculateIndent(depth) + avatarCenterOffset;
3838+ }
3939+4040+ /// Determine if depth should be flattened.
4141+ ///
4242+ /// Returns true if depth exceeds [maxDepth], indicating that further
4343+ /// nesting should be prevented to maintain readability.
4444+ static bool shouldFlattenDepth(int depth) {
4545+ return depth > maxDepth;
4646+ }
4747+4848+ /// Calculate effective depth (accounts for max depth flattening).
4949+ ///
5050+ /// Returns the actual depth value that should be used for rendering,
5151+ /// clamped to [maxDepth].
5252+ static int calculateEffectiveDepth(int actualDepth) {
5353+ return actualDepth.clamp(0, maxDepth);
5454+ }
5555+5656+ /// Get parent context for deep threads.
5757+ ///
5858+ /// Returns a string like "@username" to show which post this is replying to
5959+ /// when depth is flattened.
6060+ static String getParentContext(ThreadViewPost post) {
6161+ if (post.parent == null) return '';
6262+ final handle = post.parent!.post.author.handle;
6363+ return '@$handle';
6464+ }
6565+6666+ /// Count total number of replies (including nested).
6767+ ///
6868+ /// Recursively counts all descendants of a post.
6969+ static int countAllReplies(ThreadViewPost post) {
7070+ var count = post.replies.length;
7171+ for (final reply in post.replies) {
7272+ count += countAllReplies(reply);
7373+ }
7474+ return count;
7575+ }
7676+}