refactor: enhance ChatsDrawer with models mapping for improved conversation handling

- Updated _conversationsSliver and _buildTileFor methods to accept a modelsById parameter, allowing for efficient model retrieval during conversation tile building.
- Built a models map once per build for both conversation lists and search results, optimizing performance and reducing redundant data fetching.
- Refactored the archived section to improve layout and interaction, ensuring a more responsive user experience.
- Streamlined the handling of archived conversations with a dedicated header, enhancing visual clarity and usability.
This commit is contained in:
cogwheel0
2025-10-10 13:37:19 +05:30
parent 4e64b6f32a
commit fa32630868

View File

@@ -79,10 +79,18 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
} }
// Build a lazily-constructed sliver list of conversation tiles. // Build a lazily-constructed sliver list of conversation tiles.
Widget _conversationsSliver(List<dynamic> items, {bool inFolder = false}) { Widget _conversationsSliver(
List<dynamic> items, {
bool inFolder = false,
Map<String, Model> modelsById = const <String, Model>{},
}) {
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) => _buildTileFor(items[index], inFolder: inFolder), (context, index) => _buildTileFor(
items[index],
inFolder: inFolder,
modelsById: modelsById,
),
childCount: items.length, childCount: items.length,
), ),
); );
@@ -239,6 +247,15 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
return conversationsAsync.when( return conversationsAsync.when(
data: (items) { data: (items) {
final list = items; final list = items;
// Build a models map once for this build.
final modelsAsync = ref.watch(modelsProvider);
final Map<String, Model> modelsById = modelsAsync.maybeWhen(
data: (models) => {
for (final m in models)
if (m.id.isNotEmpty) m.id: m,
},
orElse: () => const <String, Model>{},
);
if (list.isEmpty) { if (list.isEmpty) {
return Center( return Center(
@@ -297,7 +314,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
), ),
), ),
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
_conversationsSliver(pinned), _conversationsSliver(pinned, modelsById: modelsById),
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
], ],
@@ -357,7 +374,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
child: SizedBox(height: Spacing.xs), child: SizedBox(height: Spacing.xs),
), ),
); );
out.add(_conversationsSliver(convs, inFolder: true)); out.add(
_conversationsSliver(
convs,
inFolder: true,
modelsById: modelsById,
),
);
out.add( out.add(
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: SizedBox(height: Spacing.xs), child: SizedBox(height: Spacing.xs),
@@ -396,7 +419,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
), ),
), ),
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
_conversationsSliver(regular), _conversationsSliver(regular, modelsById: modelsById),
], ],
if (archived.isNotEmpty) ...[ if (archived.isNotEmpty) ...[
@@ -404,9 +427,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
SliverPadding( SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: Spacing.md), padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: _buildArchivedSection(archived), child: _buildArchivedHeader(archived.length),
), ),
), ),
if (ref.watch(_showArchivedProvider)) ...[
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
_conversationsSliver(archived, modelsById: modelsById),
],
], ],
]; ];
return _buildRefreshableScrollableSlivers(slivers: slivers); return _buildRefreshableScrollableSlivers(slivers: slivers);
@@ -446,6 +473,15 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
} }
final pinned = list.where((c) => c.pinned == true).toList(); final pinned = list.where((c) => c.pinned == true).toList();
// Build a models map once for search builds too.
final modelsAsync = ref.watch(modelsProvider);
final Map<String, Model> modelsById = modelsAsync.maybeWhen(
data: (models) => {
for (final m in models)
if (m.id.isNotEmpty) m.id: m,
},
orElse: () => const <String, Model>{},
);
// For search results, apply the same folder safety logic // For search results, apply the same folder safety logic
final foldersState = ref.watch(foldersProvider); final foldersState = ref.watch(foldersProvider);
@@ -495,7 +531,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
), ),
), ),
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
_conversationsSliver(pinned), _conversationsSliver(pinned, modelsById: modelsById),
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
]); ]);
} }
@@ -559,7 +595,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
child: SizedBox(height: Spacing.xs), child: SizedBox(height: Spacing.xs),
), ),
); );
out.add(_conversationsSliver(convs, inFolder: true)); out.add(
_conversationsSliver(
convs,
inFolder: true,
modelsById: modelsById,
),
);
out.add( out.add(
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: SizedBox(height: Spacing.sm), child: SizedBox(height: Spacing.sm),
@@ -595,7 +637,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
), ),
), ),
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
_conversationsSliver(regular), _conversationsSliver(regular, modelsById: modelsById),
]); ]);
} }
@@ -605,10 +647,16 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
SliverPadding( SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: Spacing.md), padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: _buildArchivedSection(archived), child: _buildArchivedHeader(archived.length),
), ),
), ),
]); ]);
if (ref.watch(_showArchivedProvider)) {
slivers.add(
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
);
slivers.add(_conversationsSliver(archived, modelsById: modelsById));
}
} }
return _buildRefreshableScrollableSlivers(slivers: slivers); return _buildRefreshableScrollableSlivers(slivers: slivers);
@@ -1137,7 +1185,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
); );
} }
Widget _buildTileFor(dynamic conv, {bool inFolder = false}) { Widget _buildTileFor(
dynamic conv, {
bool inFolder = false,
Map<String, Model> modelsById = const <String, Model>{},
}) {
// Only rebuild this tile when its own selected state changes. // Only rebuild this tile when its own selected state changes.
final isActive = ref.watch( final isActive = ref.watch(
activeConversationProvider.select((c) => c?.id == conv.id), activeConversationProvider.select((c) => c?.id == conv.id),
@@ -1154,16 +1206,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
? conv.model as String ? conv.model as String
: null; : null;
if (modelId != null) { if (modelId != null) {
final modelsAsync = ref.watch(modelsProvider); model = modelsById[modelId];
model = modelsAsync.maybeWhen(
data: (models) {
for (final m in models) {
if (m.id == modelId) return m;
}
return null;
},
orElse: () => null,
);
} }
final api = ref.watch(apiServiceProvider); final api = ref.watch(apiServiceProvider);
@@ -1235,113 +1278,95 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
); );
} }
Widget _buildArchivedSection(List<dynamic> archived) { Widget _buildArchivedHeader(int count) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final show = ref.watch(_showArchivedProvider); final show = ref.watch(_showArchivedProvider);
return Column( return Material(
crossAxisAlignment: CrossAxisAlignment.stretch, color: show ? theme.navigationSelectedBackground : theme.surfaceContainer,
children: [ shape: RoundedRectangleBorder(
Material( borderRadius: BorderRadius.circular(AppBorderRadius.small),
side: BorderSide(
color: show color: show
? theme.navigationSelectedBackground ? theme.navigationSelected
: theme.surfaceContainer, : theme.surfaceContainerHighest.withValues(alpha: 0.40),
shape: RoundedRectangleBorder( width: BorderWidth.thin,
borderRadius: BorderRadius.circular(AppBorderRadius.small), ),
side: BorderSide( ),
color: show child: InkWell(
? theme.navigationSelected borderRadius: BorderRadius.circular(AppBorderRadius.small),
: theme.surfaceContainerHighest.withValues(alpha: 0.40), onTap: () => ref.read(_showArchivedProvider.notifier).set(!show),
width: BorderWidth.thin, overlayColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.pressed)) {
return theme.buttonPrimary.withValues(alpha: Alpha.buttonPressed);
}
if (states.contains(WidgetState.hovered) ||
states.contains(WidgetState.focused)) {
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
}
return Colors.transparent;
}),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: TouchTarget.listItem),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
), ),
), child: LayoutBuilder(
child: InkWell( builder: (context, constraints) {
borderRadius: BorderRadius.circular(AppBorderRadius.small), final hasFiniteWidth = constraints.maxWidth.isFinite;
onTap: () => ref.read(_showArchivedProvider.notifier).set(!show), final textFit = hasFiniteWidth ? FlexFit.tight : FlexFit.loose;
overlayColor: WidgetStateProperty.resolveWith((states) { return Row(
if (states.contains(WidgetState.pressed)) { mainAxisSize: hasFiniteWidth
return theme.buttonPrimary.withValues( ? MainAxisSize.max
alpha: Alpha.buttonPressed, : MainAxisSize.min,
children: [
Icon(
Platform.isIOS
? CupertinoIcons.archivebox
: Icons.archive_rounded,
color: theme.iconPrimary,
size: IconSize.listItem,
),
const SizedBox(width: Spacing.sm),
Flexible(
fit: textFit,
child: Text(
AppLocalizations.of(context)!.archived,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTypography.standard.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w400,
),
),
),
const SizedBox(width: Spacing.sm),
Text(
'$count',
style: AppTypography.standard.copyWith(
color: theme.textSecondary,
),
),
const SizedBox(width: Spacing.xs),
Icon(
show
? (Platform.isIOS
? CupertinoIcons.chevron_up
: Icons.expand_less)
: (Platform.isIOS
? CupertinoIcons.chevron_down
: Icons.expand_more),
color: theme.iconSecondary,
size: IconSize.listItem,
),
],
); );
} },
if (states.contains(WidgetState.hovered) ||
states.contains(WidgetState.focused)) {
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
}
return Colors.transparent;
}),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: TouchTarget.listItem,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
),
child: LayoutBuilder(
builder: (context, constraints) {
final hasFiniteWidth = constraints.maxWidth.isFinite;
final textFit = hasFiniteWidth
? FlexFit.tight
: FlexFit.loose;
return Row(
mainAxisSize: hasFiniteWidth
? MainAxisSize.max
: MainAxisSize.min,
children: [
Icon(
Platform.isIOS
? CupertinoIcons.archivebox
: Icons.archive_rounded,
color: theme.iconPrimary,
size: IconSize.listItem,
),
const SizedBox(width: Spacing.sm),
Flexible(
fit: textFit,
child: Text(
AppLocalizations.of(context)!.archived,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTypography.standard.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w400,
),
),
),
const SizedBox(width: Spacing.sm),
Text(
'${archived.length}',
style: AppTypography.standard.copyWith(
color: theme.textSecondary,
),
),
const SizedBox(width: Spacing.xs),
Icon(
show
? (Platform.isIOS
? CupertinoIcons.chevron_up
: Icons.expand_less)
: (Platform.isIOS
? CupertinoIcons.chevron_down
: Icons.expand_more),
color: theme.iconSecondary,
size: IconSize.listItem,
),
],
);
},
),
),
), ),
), ),
), ),
if (show) ...[ ),
const SizedBox(height: Spacing.xs),
...archived.map((c) => _buildTileFor(c)),
],
],
); );
} }