diff --git a/docs/docs_screenshots/test/draft_list/draft_list_view_test.dart b/docs/docs_screenshots/test/draft_list/draft_list_view_test.dart deleted file mode 100644 index 025282ceaf..0000000000 --- a/docs/docs_screenshots/test/draft_list/draft_list_view_test.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:alchemist/alchemist.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../src/golden_client_stubs.dart'; -import '../src/golden_theme.dart'; -import '../src/mocks.dart'; - -Draft _makeDraft({ - required String channelId, - required String channelName, - required String text, - String? parentId, - Message? parentMessage, -}) { - return Draft( - channelCid: 'messaging:$channelId', - createdAt: DateTime(2024, 6, 1, 10, 0), - message: DraftMessage(text: text, parentId: parentId), - channel: ChannelModel( - id: channelId, - type: 'messaging', - extraData: {'name': channelName}, - ), - parentId: parentId, - parentMessage: parentMessage, - ); -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - goldenTest( - 'draft list view', - fileName: 'draft_list_view', - constraints: const BoxConstraints.tightFor(width: 375, height: 550), - builder: () { - final client = MockClient(); - stubMockClientCurrentUser(client, OwnUser(id: 'user-1', name: 'Alice')); - - final drafts = [ - _makeDraft( - channelId: 'general', - channelName: 'General', - text: 'Has anyone seen the latest release notes?', - ), - _makeDraft( - channelId: 'design', - channelName: 'Design', - text: 'I have some feedback on the new color scheme…', - ), - _makeDraft( - channelId: 'random', - channelName: 'Random', - text: 'Anyone up for lunch tomorrow?', - ), - ]; - - final controller = StreamDraftListController.fromValue( - PagedValue(items: drafts), - client: client, - ); - - stubQueryDraftsForGoldens(client, drafts); - - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - streamChatThemeData: docsStreamChatThemeData(), - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - appBar: AppBar( - title: const Text('Stream Chat'), - actions: const [ - IconButton(icon: Icon(Icons.edit_outlined), onPressed: null), - ], - ), - body: StreamDraftListView(controller: controller), - bottomNavigationBar: BottomNavigationBar( - currentIndex: 3, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.chat_bubble_outline), - label: 'Chats', - ), - BottomNavigationBarItem( - icon: Icon(Icons.alternate_email), - label: 'Mentions', - ), - BottomNavigationBarItem( - icon: Icon(Icons.comment_outlined), - label: 'Threads', - ), - BottomNavigationBarItem( - icon: Icon(Icons.edit_note), - label: 'Drafts', - ), - ], - ), - ), - ), - ); - }, - ); - - goldenTest( - 'channel draft message tile', - fileName: 'channel_draft_message', - constraints: const BoxConstraints.tightFor(width: 375, height: 80), - builder: () { - final client = MockClient(); - stubMockClientCurrentUser(client, OwnUser(id: 'user-1', name: 'Alice')); - - final draft = _makeDraft( - channelId: 'general', - channelName: 'General', - text: 'I was thinking about the new feature…', - ); - - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - streamChatThemeData: docsStreamChatThemeData(), - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: StreamDraftListTile( - draft: draft, - currentUser: User(id: 'user-1', name: 'Alice'), - ), - ), - ), - ); - }, - ); - - goldenTest( - 'thread draft message tile', - fileName: 'thread_draft_message', - constraints: const BoxConstraints.tightFor(width: 375, height: 80), - builder: () { - final client = MockClient(); - stubMockClientCurrentUser(client, OwnUser(id: 'user-1', name: 'Alice')); - - final parentMessage = Message( - id: 'parent-msg', - text: 'Has anyone seen the latest release?', - user: User(id: 'user-2', name: 'Bob'), - createdAt: DateTime(2024, 6, 1, 9, 0), - ); - - final draft = _makeDraft( - channelId: 'general', - channelName: 'General', - text: 'Yes, the new streaming API looks great!', - parentId: 'parent-msg', - parentMessage: parentMessage, - ); - - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - streamChatThemeData: docsStreamChatThemeData(), - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: StreamDraftListTile( - draft: draft, - currentUser: User(id: 'user-1', name: 'Alice'), - ), - ), - ), - ); - }, - ); -} diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 0c1dce0609..18f31dcf71 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -2,6 +2,7 @@ 🛑️ Breaking +- Removed `StreamDraftListView`, `StreamDraftListTile`, `StreamDraftListTileTheme`, and `StreamDraftListTileThemeData` from the SDK. Also removed `StreamChatThemeData.draftListTileTheme`. Refer to the sample app for a reference implementation using `StreamDraftListController` and `PagedValueListView`. - Replaced `StreamMessageInput.hintGetter` with `placeholderBuilder` over a sealed `MessageInputPlaceholder`. See [`migrations/redesign/message_composer.md`](../../migrations/redesign/message_composer.md). - Removed `StreamMessageListView.unreadIndicatorBuilder`; use `StreamComponentFactory.jumpToUnreadButton`. diff --git a/packages/stream_chat_flutter/lib/src/theme/draft_list_tile_theme.dart b/packages/stream_chat_flutter/lib/src/theme/draft_list_tile_theme.dart deleted file mode 100644 index dce3292e6e..0000000000 --- a/packages/stream_chat_flutter/lib/src/theme/draft_list_tile_theme.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/utils/date_formatter.dart'; - -/// {@template streamDraftListTileTheme} -/// Overrides the default style of [StreamDraftListTile] descendants. -/// -/// See also: -/// -/// * [StreamDraftListTileThemeData], which is used to configure this -/// theme. -/// {@endtemplate} -class StreamDraftListTileTheme extends InheritedTheme { - /// Creates a [StreamDraftListTileTheme]. - /// - /// The [data] parameter must not be null. - const StreamDraftListTileTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The configuration of this theme. - final StreamDraftListTileThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [StreamDraftListTileTheme] widget, then - /// [StreamChatThemeData.draftListTileTheme] is used. - static StreamDraftListTileThemeData of(BuildContext context) { - final draftListTileTheme = context.dependOnInheritedWidgetOfExactType(); - return draftListTileTheme?.data ?? StreamChatTheme.of(context).draftListTileTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => StreamDraftListTileTheme(data: data, child: child); - - @override - bool updateShouldNotify(StreamDraftListTileTheme oldWidget) => data != oldWidget.data; -} - -/// {@template streamDraftListTileThemeData} -/// A style that overrides the default appearance of -/// [StreamDraftListTile] widgets when used with -/// [StreamDraftListTileTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.draftListTileTheme]. -/// {@endtemplate} -class StreamDraftListTileThemeData with Diagnosticable { - /// {@macro streamDraftListTileThemeData} - const StreamDraftListTileThemeData({ - this.padding, - this.backgroundColor, - this.draftChannelNameStyle, - this.draftMessageStyle, - this.draftTimestampStyle, - this.draftTimestampFormatter, - }); - - /// The padding around the [StreamDraftListTile] widget. - final EdgeInsetsGeometry? padding; - - /// The background color of the [StreamDraftListTile] widget. - final Color? backgroundColor; - - /// The style of the channel name in the [StreamDraftListTile] widget. - final TextStyle? draftChannelNameStyle; - - /// The style of the draft message in the [StreamDraftListTile] widget. - final TextStyle? draftMessageStyle; - - /// The style of the draft timestamp in the [StreamDraftListTile] widget. - final TextStyle? draftTimestampStyle; - - /// Formatter for the draft timestamp. - /// - /// If null, uses the default date formatting. - /// - /// Example: - /// ```dart - /// StreamDraftListTileThemeData( - /// draftTimestampStyle: TextStyle(...), - /// draftTimestampFormatter: (context, date) { - /// return Jiffy.parseFromDateTime(date).fromNow(); // "2 hours ago" - /// }, - /// ) - /// ``` - final DateFormatter? draftTimestampFormatter; - - /// A copy of [StreamDraftListTileThemeData] with specified attributes - /// overridden. - StreamDraftListTileThemeData copyWith({ - EdgeInsetsGeometry? padding, - Color? backgroundColor, - TextStyle? draftChannelNameStyle, - TextStyle? draftMessageStyle, - TextStyle? draftTimestampStyle, - DateFormatter? draftTimestampFormatter, - Color? draftIconColor, - }) => StreamDraftListTileThemeData( - padding: padding ?? this.padding, - backgroundColor: backgroundColor ?? this.backgroundColor, - draftChannelNameStyle: draftChannelNameStyle ?? this.draftChannelNameStyle, - draftMessageStyle: draftMessageStyle ?? this.draftMessageStyle, - draftTimestampStyle: draftTimestampStyle ?? this.draftTimestampStyle, - draftTimestampFormatter: draftTimestampFormatter ?? this.draftTimestampFormatter, - ); - - /// Merges this [StreamDraftListTileThemeData] with the [other]. - StreamDraftListTileThemeData merge( - StreamDraftListTileThemeData? other, - ) { - if (other == null) return this; - return copyWith( - padding: other.padding, - backgroundColor: other.backgroundColor, - draftChannelNameStyle: other.draftChannelNameStyle, - draftMessageStyle: other.draftMessageStyle, - draftTimestampStyle: other.draftTimestampStyle, - draftTimestampFormatter: other.draftTimestampFormatter, - ); - } - - /// Linearly interpolate between two [StreamDraftListTileThemeData]. - StreamDraftListTileThemeData lerp( - StreamDraftListTileThemeData? a, - StreamDraftListTileThemeData? b, - double t, - ) => StreamDraftListTileThemeData( - padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), - backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), - draftChannelNameStyle: TextStyle.lerp( - a?.draftChannelNameStyle, - b?.draftChannelNameStyle, - t, - ), - draftMessageStyle: TextStyle.lerp( - a?.draftMessageStyle, - b?.draftMessageStyle, - t, - ), - draftTimestampStyle: TextStyle.lerp( - a?.draftTimestampStyle, - b?.draftTimestampStyle, - t, - ), - draftTimestampFormatter: t < 0.5 ? a?.draftTimestampFormatter : b?.draftTimestampFormatter, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamDraftListTileThemeData && - other.padding == padding && - other.backgroundColor == backgroundColor && - other.draftChannelNameStyle == draftChannelNameStyle && - other.draftMessageStyle == draftMessageStyle && - other.draftTimestampStyle == draftTimestampStyle && - other.draftTimestampFormatter == draftTimestampFormatter; - - @override - int get hashCode => - padding.hashCode ^ - backgroundColor.hashCode ^ - draftChannelNameStyle.hashCode ^ - draftMessageStyle.hashCode ^ - draftTimestampStyle.hashCode ^ - draftTimestampFormatter.hashCode; -} diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index 148f9a4ebb..8864b942dc 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -57,7 +57,6 @@ class StreamChatThemeData { StreamPollCommentsSheetThemeData? pollCommentsSheetTheme, StreamPollOptionVotesSheetThemeData? pollOptionVotesSheetTheme, StreamThreadListTileThemeData? threadListTileTheme, - StreamDraftListTileThemeData? draftListTileTheme, StreamVoiceRecordingAttachmentThemeData? voiceRecordingAttachmentTheme, StreamChannelListItemThemeData? channelListItemTheme, }) { @@ -86,7 +85,6 @@ class StreamChatThemeData { pollCommentsSheetTheme: pollCommentsSheetTheme, pollOptionVotesSheetTheme: pollOptionVotesSheetTheme, threadListTileTheme: threadListTileTheme, - draftListTileTheme: draftListTileTheme, voiceRecordingAttachmentTheme: voiceRecordingAttachmentTheme, channelListItemTheme: channelListItemTheme, ); @@ -117,7 +115,6 @@ class StreamChatThemeData { required this.pollCommentsSheetTheme, required this.pollOptionVotesSheetTheme, required this.threadListTileTheme, - required this.draftListTileTheme, required this.voiceRecordingAttachmentTheme, required this.channelListItemTheme, }); @@ -194,19 +191,6 @@ class StreamChatThemeData { pollCommentsSheetTheme: const StreamPollCommentsSheetThemeData(), pollOptionVotesSheetTheme: const StreamPollOptionVotesSheetThemeData(), threadListTileTheme: const StreamThreadListTileThemeData(), - draftListTileTheme: StreamDraftListTileThemeData( - backgroundColor: colorTheme.barsBg, - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), - draftChannelNameStyle: textTheme.bodyBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - draftMessageStyle: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - draftTimestampStyle: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - ), voiceRecordingAttachmentTheme: const StreamVoiceRecordingAttachmentThemeData(), channelListItemTheme: const StreamChannelListItemThemeData(), ); @@ -265,9 +249,6 @@ class StreamChatThemeData { /// Theme configuration for the [StreamChannelListItem] widget. final StreamChannelListItemThemeData channelListItemTheme; - /// Theme configuration for the [StreamDraftListTile] widget. - final StreamDraftListTileThemeData draftListTileTheme; - /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ @@ -288,7 +269,6 @@ class StreamChatThemeData { StreamPollCommentsSheetThemeData? pollCommentsSheetTheme, StreamPollOptionVotesSheetThemeData? pollOptionVotesSheetTheme, StreamThreadListTileThemeData? threadListTileTheme, - StreamDraftListTileThemeData? draftListTileTheme, StreamVoiceRecordingAttachmentThemeData? voiceRecordingAttachmentTheme, StreamChannelListItemThemeData? channelListItemTheme, }) => StreamChatThemeData.raw( @@ -307,7 +287,6 @@ class StreamChatThemeData { pollCommentsSheetTheme: pollCommentsSheetTheme ?? this.pollCommentsSheetTheme, pollOptionVotesSheetTheme: pollOptionVotesSheetTheme ?? this.pollOptionVotesSheetTheme, threadListTileTheme: threadListTileTheme ?? this.threadListTileTheme, - draftListTileTheme: draftListTileTheme ?? this.draftListTileTheme, voiceRecordingAttachmentTheme: voiceRecordingAttachmentTheme ?? this.voiceRecordingAttachmentTheme, channelListItemTheme: channelListItemTheme ?? this.channelListItemTheme, ); @@ -331,7 +310,6 @@ class StreamChatThemeData { pollCommentsSheetTheme: pollCommentsSheetTheme.merge(other.pollCommentsSheetTheme), pollOptionVotesSheetTheme: pollOptionVotesSheetTheme.merge(other.pollOptionVotesSheetTheme), threadListTileTheme: threadListTileTheme.merge(other.threadListTileTheme), - draftListTileTheme: draftListTileTheme.merge(other.draftListTileTheme), voiceRecordingAttachmentTheme: voiceRecordingAttachmentTheme.merge(other.voiceRecordingAttachmentTheme), channelListItemTheme: channelListItemTheme.merge(other.channelListItemTheme), ); diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart index 74c9dad44c..ed9cc011b0 100644 --- a/packages/stream_chat_flutter/lib/src/theme/themes.dart +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -2,7 +2,6 @@ export 'avatar_theme.dart'; export 'channel_header_theme.dart'; export 'channel_list_header_theme.dart'; export 'color_theme.dart'; -export 'draft_list_tile_theme.dart'; export 'gallery_footer_theme.dart'; export 'gallery_header_theme.dart'; export 'message_list_view_theme.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 8548ad4e0f..c50a2a1f72 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -196,8 +196,6 @@ export 'src/reactions/detail/reaction_detail_sheet.dart'; export 'src/reactions/picker/reaction_picker.dart'; export 'src/scroll_view/channel_scroll_view/stream_channel_list_item.dart'; export 'src/scroll_view/channel_scroll_view/stream_channel_list_view.dart'; -export 'src/scroll_view/draft_scroll_view/stream_draft_list_tile.dart'; -export 'src/scroll_view/draft_scroll_view/stream_draft_list_view.dart'; export 'src/scroll_view/member_scroll_view/stream_member_grid_view.dart'; export 'src/scroll_view/member_scroll_view/stream_member_list_view.dart'; export 'src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart'; @@ -219,7 +217,6 @@ export 'src/scroll_view/user_scroll_view/stream_user_list_tile.dart'; export 'src/scroll_view/user_scroll_view/stream_user_list_view.dart'; export 'src/stream_chat.dart'; export 'src/stream_chat_configuration.dart'; -export 'src/theme/draft_list_tile_theme.dart'; export 'src/theme/stream_chat_theme.dart'; export 'src/theme/themes.dart'; export 'src/user/user_mention_tile.dart'; diff --git a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_dark.png b/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_dark.png deleted file mode 100644 index fc10fd6bc3..0000000000 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_dark.png and /dev/null differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_light.png b/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_light.png deleted file mode 100644 index 0cf5dce112..0000000000 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_light.png and /dev/null differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/stream_draft_list_tile_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/stream_draft_list_tile_test.dart deleted file mode 100644 index 9d77767bbb..0000000000 --- a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/stream_draft_list_tile_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:alchemist/alchemist.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../../mocks.dart'; - -void main() { - final user = User(id: 'uid1', name: 'User 1'); - final createdAt = DateTime.parse('2022-07-20T16:00:00.000Z'); - final draft = Draft( - channelCid: 'messaging:123', - channel: ChannelModel( - cid: 'messaging:123', - extraData: const {'name': 'Group chat'}, - ), - createdAt: createdAt, - message: DraftMessage( - text: 'This is a draft message that I want to save for later', - ), - ); - - for (final brightness in Brightness.values) { - goldenTest( - '[${brightness.name}] -> StreamDraftListTile looks fine', - fileName: 'stream_draft_list_tile_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 600, height: 120), - builder: () => _wrapWithMaterialApp( - brightness: brightness, - StreamDraftListTile(draft: draft, currentUser: user), - ), - ); - } - - group('Formatter Tests', () { - testWidgets( - 'StreamDraftListTile displays custom formatted timestamp', - (tester) async { - await tester.pumpWidget( - _wrapWithMaterialApp( - StreamDraftListTileTheme( - data: StreamDraftListTileThemeData( - draftTimestampFormatter: (context, timestamp) { - return 'CUSTOM_FORMAT_20_07_2022'; - }, - ), - child: StreamDraftListTile(draft: draft, currentUser: user), - ), - ), - ); - - await tester.pumpAndSettle(); - - // Verify the custom formatted text is visible - expect(find.text('CUSTOM_FORMAT_20_07_2022'), findsOneWidget); - }, - ); - - testWidgets( - 'StreamDraftListTile inner theme overrides outer theme', - (tester) async { - await tester.pumpWidget( - _wrapWithMaterialApp( - StreamDraftListTileTheme( - data: StreamDraftListTileThemeData( - draftTimestampFormatter: (context, timestamp) { - return 'OUTER_FORMATTER'; - }, - ), - child: StreamDraftListTileTheme( - data: StreamDraftListTileThemeData( - draftTimestampFormatter: (context, timestamp) { - return 'INNER_FORMATTER'; - }, - ), - child: StreamDraftListTile(draft: draft, currentUser: user), - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - // Inner formatter should be used - expect(find.text('INNER_FORMATTER'), findsOneWidget); - expect(find.text('OUTER_FORMATTER'), findsNothing); - }, - ); - }); -} - -Widget _wrapWithMaterialApp( - Widget widget, { - Brightness? brightness, -}) { - final client = MockClient(); - final clientState = MockClientState(); - final currentUser = OwnUser(id: 'current-user-id', name: 'Current User'); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(currentUser); - - return MaterialApp( - theme: ThemeData( - useMaterial3: true, - brightness: brightness ?? Brightness.light, - ), - home: StreamChat( - client: client, - streamChatConfigData: StreamChatConfigurationData(), - connectivityStream: Stream.value([ConnectivityResult.wifi]), - streamChatThemeData: StreamChatThemeData(brightness: brightness), - child: Builder( - builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }, - ), - ), - ); -} diff --git a/packages/stream_chat_flutter/test/src/theme/draft_list_tile_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/draft_list_tile_theme_test.dart deleted file mode 100644 index 2f9e4c939b..0000000000 --- a/packages/stream_chat_flutter/test/src/theme/draft_list_tile_theme_test.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -String _dummyFormatter(BuildContext context, DateTime date) => 'formatted'; - -void main() { - testWidgets('StreamDraftListTileTheme merges with ancestor theme', (tester) async { - const backgroundColor = Colors.blue; - const childBackgroundColor = Colors.red; - - late BuildContext capturedContext; - - await tester.pumpWidget( - MaterialApp( - home: StreamChatTheme( - data: StreamChatThemeData( - draftListTileTheme: const StreamDraftListTileThemeData( - backgroundColor: backgroundColor, - ), - ), - child: Builder( - builder: (context) { - return StreamDraftListTileTheme( - data: const StreamDraftListTileThemeData( - backgroundColor: childBackgroundColor, - ), - child: Builder( - builder: (context) { - capturedContext = context; - return const SizedBox(); - }, - ), - ); - }, - ), - ), - ), - ); - - // Verify that the theme data is correctly merged - final theme = StreamDraftListTileTheme.of(capturedContext); - expect(theme.backgroundColor, childBackgroundColor); - }); - - test('StreamDraftListTileThemeData equality', () { - const themeData1 = StreamDraftListTileThemeData( - backgroundColor: Colors.red, - padding: EdgeInsets.all(8), - draftChannelNameStyle: TextStyle(fontSize: 16), - draftMessageStyle: TextStyle(fontSize: 14), - draftTimestampStyle: TextStyle(fontSize: 12), - draftTimestampFormatter: _dummyFormatter, - ); - - const themeData2 = StreamDraftListTileThemeData( - backgroundColor: Colors.red, - padding: EdgeInsets.all(8), - draftChannelNameStyle: TextStyle(fontSize: 16), - draftMessageStyle: TextStyle(fontSize: 14), - draftTimestampStyle: TextStyle(fontSize: 12), - draftTimestampFormatter: _dummyFormatter, - ); - - const themeData3 = StreamDraftListTileThemeData( - backgroundColor: Colors.blue, // Different color - padding: EdgeInsets.all(8), - draftChannelNameStyle: TextStyle(fontSize: 16), - draftMessageStyle: TextStyle(fontSize: 14), - draftTimestampStyle: TextStyle(fontSize: 12), - draftTimestampFormatter: _dummyFormatter, - ); - - // Same properties should be equal - expect(themeData1, themeData2); - // Different properties should not be equal - expect(themeData1, isNot(themeData3)); - - // Hash codes should match for equal objects - expect(themeData1.hashCode, themeData2.hashCode); - }); - - test('StreamDraftListTileThemeData copyWith', () { - const original = StreamDraftListTileThemeData( - backgroundColor: Colors.red, - padding: EdgeInsets.all(8), - draftChannelNameStyle: TextStyle(fontSize: 16), - draftMessageStyle: TextStyle(fontSize: 14), - draftTimestampStyle: TextStyle(fontSize: 12), - draftTimestampFormatter: _dummyFormatter, - ); - - const newBackgroundColor = Colors.blue; - const newPadding = EdgeInsets.all(16); - - final copied = original.copyWith( - backgroundColor: newBackgroundColor, - padding: newPadding, - ); - - // Verify copied properties - expect(copied.backgroundColor, newBackgroundColor); - expect(copied.padding, newPadding); - // Unchanged properties should remain the same - expect(copied.draftChannelNameStyle, original.draftChannelNameStyle); - expect(copied.draftMessageStyle, original.draftMessageStyle); - expect(copied.draftTimestampStyle, original.draftTimestampStyle); - expect(copied.draftTimestampFormatter, original.draftTimestampFormatter); - }); - - test('StreamDraftListTileThemeData merge', () { - const original = StreamDraftListTileThemeData( - backgroundColor: Colors.red, - padding: EdgeInsets.all(8), - draftChannelNameStyle: TextStyle(fontSize: 16), - draftMessageStyle: TextStyle(fontSize: 14), - draftTimestampStyle: TextStyle(fontSize: 12), - draftTimestampFormatter: _dummyFormatter, - ); - - const other = StreamDraftListTileThemeData( - backgroundColor: Colors.blue, - padding: EdgeInsets.all(16), - // Other properties are null - ); - - final merged = original.merge(other); - - // Properties from 'other' should override 'original' - expect(merged.backgroundColor, other.backgroundColor); - expect(merged.padding, other.padding); - // Null properties in 'other' should not override 'original' - expect(merged.draftChannelNameStyle, original.draftChannelNameStyle); - expect(merged.draftMessageStyle, original.draftMessageStyle); - expect(merged.draftTimestampStyle, original.draftTimestampStyle); - expect(merged.draftTimestampFormatter, original.draftTimestampFormatter); - - // Merging with null should return original - final mergedWithNull = original.merge(null); - expect(mergedWithNull, original); - }); - - test('StreamDraftListTileThemeData lerp', () { - const data1 = StreamDraftListTileThemeData( - backgroundColor: Colors.black, - padding: EdgeInsets.all(8), - ); - - const data2 = StreamDraftListTileThemeData( - backgroundColor: Colors.white, - padding: EdgeInsets.all(16), - ); - - // t = 0 should return data1 - final lerpedAt0 = data1.lerp(data1, data2, 0); - expect(lerpedAt0.backgroundColor, data1.backgroundColor); - expect(lerpedAt0.padding, data1.padding); - - // t = 1 should return data2 - final lerpedAt1 = data1.lerp(data1, data2, 1); - expect(lerpedAt1.backgroundColor, data2.backgroundColor); - expect(lerpedAt1.padding, data2.padding); - - // t = 0.5 should return something in between - final lerpedAt05 = data1.lerp(data1, data2, 0.5); - expect(lerpedAt05.backgroundColor, Color.lerp(Colors.black, Colors.white, 0.5)); - expect( - lerpedAt05.padding, - EdgeInsetsGeometry.lerp( - const EdgeInsets.all(8), - const EdgeInsets.all(16), - 0.5, - ), - ); - }); -} diff --git a/sample_app/lib/pages/draft_list_page.dart b/sample_app/lib/pages/draft_list_page.dart index 99b86ef70a..e486ac9d05 100644 --- a/sample_app/lib/pages/draft_list_page.dart +++ b/sample_app/lib/pages/draft_list_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:sample_app/pages/channel_page.dart'; import 'package:sample_app/pages/thread_page.dart'; +import 'package:sample_app/widgets/stream_draft_list_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class DraftListPage extends StatefulWidget { diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_tile.dart b/sample_app/lib/widgets/stream_draft_list_tile.dart similarity index 69% rename from packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_tile.dart rename to sample_app/lib/widgets/stream_draft_list_tile.dart index d2311f4a30..bd0fdf8e44 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_tile.dart +++ b/sample_app/lib/widgets/stream_draft_list_tile.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/misc/timestamp.dart'; +import 'package:sample_app/widgets/stream_draft_list_tile_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@template streamDraftListTile} /// A widget that displays a draft in a list. /// -/// This widget is used in the [StreamDraftListView] to display a draft. -/// /// The widget displays the channel name, the draft message preview, and the /// timestamp. -/// {@endtemplate} class StreamDraftListTile extends StatelessWidget { - /// {@macro streamDraftListTile} + /// Creates a new [StreamDraftListTile]. const StreamDraftListTile({ super.key, required this.draft, @@ -49,12 +45,14 @@ class StreamDraftListTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (draft.channel case final channel?) - DraftTitle( + _DraftTitle( channelName: channel.formatName(currentUser: currentUser), + theme: theme, ), - DraftMessageContent( + _DraftMessageContent( draft: draft, currentUser: currentUser, + theme: theme, ), ], ), @@ -64,29 +62,23 @@ class StreamDraftListTile extends StatelessWidget { } } -/// {@template draftTitle} -/// A widget that displays the channel name. -/// {@endtemplate} -class DraftTitle extends StatelessWidget { - /// {@macro draftTitle} - const DraftTitle({ - super.key, +class _DraftTitle extends StatelessWidget { + const _DraftTitle({ this.channelName, + required this.theme, }); - /// The channel name to display. final String? channelName; + final StreamDraftListTileThemeData theme; @override Widget build(BuildContext context) { - final theme = StreamDraftListTileTheme.of(context); - return Row( mainAxisSize: MainAxisSize.min, children: [ Icon( - size: 16, Icons.edit_note_rounded, + size: 16, color: theme.draftChannelNameStyle?.color, ), const SizedBox(width: 4), @@ -103,26 +95,22 @@ class DraftTitle extends StatelessWidget { } } -/// {@template draftMessageContent} -/// A widget that displays the draft message content. -/// {@endtemplate} -class DraftMessageContent extends StatelessWidget { - /// {@macro draftMessageContent} - const DraftMessageContent({ - super.key, +class _DraftMessageContent extends StatelessWidget { + const _DraftMessageContent({ required this.draft, this.currentUser, + required this.theme, }); - /// The draft to display. final Draft draft; - - /// The current user. final User? currentUser; + final StreamDraftListTileThemeData theme; @override Widget build(BuildContext context) { - final theme = StreamDraftListTileTheme.of(context); + final date = draft.createdAt.toLocal(); + final formatter = theme.draftTimestampFormatter; + final timestamp = formatter != null ? formatter(context, date) : _formatDate(date); return Row( children: [ @@ -132,12 +120,23 @@ class DraftMessageContent extends StatelessWidget { textStyle: theme.draftMessageStyle, ), ), - StreamTimestamp( - date: draft.createdAt.toLocal(), + Text( + timestamp, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: theme.draftTimestampStyle, - formatter: theme.draftTimestampFormatter, ), ], ); } + + String _formatDate(DateTime date) { + final jiffy = Jiffy.parseFromDateTime(date); + final now = DateTime.now(); + if (now.difference(date).inDays == 0 && now.day == date.day) { + return jiffy.jm; + } + if (now.difference(date).inDays < 7) return jiffy.EEEE; + return jiffy.yMd; + } } diff --git a/sample_app/lib/widgets/stream_draft_list_tile_theme.dart b/sample_app/lib/widgets/stream_draft_list_tile_theme.dart new file mode 100644 index 0000000000..c28b61b1b6 --- /dev/null +++ b/sample_app/lib/widgets/stream_draft_list_tile_theme.dart @@ -0,0 +1,116 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Overrides the default style of [StreamDraftListTile] descendants. +class StreamDraftListTileTheme extends InheritedTheme { + /// Creates a [StreamDraftListTileTheme]. + const StreamDraftListTileTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The configuration of this theme. + final StreamDraftListTileThemeData data; + + /// Returns the closest [StreamDraftListTileThemeData] from the widget tree, + /// falling back to a default built from the ambient [StreamChatTheme]. + static StreamDraftListTileThemeData of(BuildContext context) { + final tileTheme = context.dependOnInheritedWidgetOfExactType(); + if (tileTheme != null) return tileTheme.data; + + final chatTheme = StreamChatTheme.of(context); + final colorTheme = chatTheme.colorTheme; + final textTheme = chatTheme.textTheme; + + return StreamDraftListTileThemeData( + backgroundColor: colorTheme.barsBg, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), + draftChannelNameStyle: textTheme.bodyBold.copyWith( + color: colorTheme.textHighEmphasis, + ), + draftMessageStyle: textTheme.footnote.copyWith( + color: colorTheme.textLowEmphasis, + ), + draftTimestampStyle: textTheme.footnote.copyWith( + color: colorTheme.textLowEmphasis, + ), + ); + } + + @override + Widget wrap(BuildContext context, Widget child) => StreamDraftListTileTheme(data: data, child: child); + + @override + bool updateShouldNotify(StreamDraftListTileTheme oldWidget) => data != oldWidget.data; +} + +/// A style that overrides the default appearance of [StreamDraftListTile] +/// widgets when used with [StreamDraftListTileTheme]. +class StreamDraftListTileThemeData with Diagnosticable { + /// Creates a new [StreamDraftListTileThemeData]. + const StreamDraftListTileThemeData({ + this.padding, + this.backgroundColor, + this.draftChannelNameStyle, + this.draftMessageStyle, + this.draftTimestampStyle, + this.draftTimestampFormatter, + }); + + /// The padding around the [StreamDraftListTile] widget. + final EdgeInsetsGeometry? padding; + + /// The background color of the [StreamDraftListTile] widget. + final Color? backgroundColor; + + /// The style of the channel name in the [StreamDraftListTile] widget. + final TextStyle? draftChannelNameStyle; + + /// The style of the draft message in the [StreamDraftListTile] widget. + final TextStyle? draftMessageStyle; + + /// The style of the draft timestamp in the [StreamDraftListTile] widget. + final TextStyle? draftTimestampStyle; + + /// Formatter for the draft timestamp. Defaults to a Jiffy-based format. + final String Function(BuildContext, DateTime)? draftTimestampFormatter; + + /// Returns a copy of this theme with the given fields replaced. + StreamDraftListTileThemeData copyWith({ + EdgeInsetsGeometry? padding, + Color? backgroundColor, + TextStyle? draftChannelNameStyle, + TextStyle? draftMessageStyle, + TextStyle? draftTimestampStyle, + String Function(BuildContext, DateTime)? draftTimestampFormatter, + }) => StreamDraftListTileThemeData( + padding: padding ?? this.padding, + backgroundColor: backgroundColor ?? this.backgroundColor, + draftChannelNameStyle: draftChannelNameStyle ?? this.draftChannelNameStyle, + draftMessageStyle: draftMessageStyle ?? this.draftMessageStyle, + draftTimestampStyle: draftTimestampStyle ?? this.draftTimestampStyle, + draftTimestampFormatter: draftTimestampFormatter ?? this.draftTimestampFormatter, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is StreamDraftListTileThemeData && + other.padding == padding && + other.backgroundColor == backgroundColor && + other.draftChannelNameStyle == draftChannelNameStyle && + other.draftMessageStyle == draftMessageStyle && + other.draftTimestampStyle == draftTimestampStyle && + other.draftTimestampFormatter == draftTimestampFormatter; + + @override + int get hashCode => + padding.hashCode ^ + backgroundColor.hashCode ^ + draftChannelNameStyle.hashCode ^ + draftMessageStyle.hashCode ^ + draftTimestampStyle.hashCode ^ + draftTimestampFormatter.hashCode; +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_view.dart b/sample_app/lib/widgets/stream_draft_list_view.dart similarity index 51% rename from packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_view.dart rename to sample_app/lib/widgets/stream_draft_list_view.dart index 1400679a7e..5a7e04705d 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_view.dart +++ b/sample_app/lib/widgets/stream_draft_list_view.dart @@ -1,10 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:sample_app/widgets/stream_draft_list_tile.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamDraftListView]. Widget defaultDraftListViewSeparatorBuilder( @@ -17,7 +14,6 @@ Widget defaultDraftListViewSeparatorBuilder( /// [StreamDraftListView]. typedef StreamDraftListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetBuilder; -/// {@template streamDraftListView} /// A [ListView] that shows a list of [Draft]'s. It uses a /// [StreamDraftListController] to load the drafts in paginated form. /// @@ -38,9 +34,8 @@ typedef StreamDraftListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetB /// See also: /// * [StreamDraftListTile] /// * [StreamDraftListController] -/// {@endtemplate} class StreamDraftListView extends StatelessWidget { - /// {@macro streamDraftListView} + /// Creates a new [StreamDraftListView]. const StreamDraftListView({ super.key, required this.controller, @@ -96,11 +91,9 @@ class StreamDraftListView extends StatelessWidget { /// The index to take into account when triggering [controller.loadMore]. final int loadMoreTriggerIndex; - /// {@template flutter.widgets.scroll_view.scrollDirection} /// The axis along which the scroll view scrolls. /// /// Defaults to [Axis.vertical]. - /// {@endtemplate} final Axis scrollDirection; /// The amount of space by which to inset the children. @@ -108,149 +101,41 @@ class StreamDraftListView extends StatelessWidget { /// Whether to wrap each child in an [AutomaticKeepAlive]. /// - /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] - /// widgets so that children can use [KeepAliveNotification]s to preserve - /// their state when they would otherwise be garbage collected off-screen. - /// - /// This feature (and [addRepaintBoundaries]) must be disabled if the children - /// are going to manually maintain their [KeepAlive] state. It may also be - /// more efficient to disable this feature if it is known ahead of time that - /// none of the children will ever try to keep themselves alive. - /// /// Defaults to true. final bool addAutomaticKeepAlives; /// Whether to wrap each child in a [RepaintBoundary]. /// - /// Typically, children in a scrolling container are wrapped in repaint - /// boundaries so that they do not need to be repainted as the list scrolls. - /// If the children are easy to repaint (e.g., solid color blocks or a short - /// snippet of text), it might be more efficient to not add a repaint boundary - /// and simply repaint the children during scrolling. - /// /// Defaults to true. final bool addRepaintBoundaries; /// Whether to wrap each child in an [IndexedSemantics]. /// - /// Typically, children in a scrolling container must be annotated with a - /// semantic index in order to generate the correct accessibility - /// announcements. This should only be set to false if the indexes have - /// already been provided by an [IndexedSemantics] widget. - /// /// Defaults to true. - /// - /// See also: - /// - /// * [IndexedSemantics], for an explanation of how to manually - /// provide semantic indexes. final bool addSemanticIndexes; - /// {@template flutter.widgets.scroll_view.reverse} /// Whether the scroll view scrolls in the reading direction. /// - /// For example, if [scrollDirection] is [Axis.vertical], then the scroll view - /// scrolls from top to bottom when [reverse] is false and from bottom to top - /// when [reverse] is true. - /// /// Defaults to false. - /// {@endtemplate} final bool reverse; - /// {@template flutter.widgets.scroll_view.controller} /// An object that can be used to control the position to which this scroll /// view is scrolled. - /// - /// Must be null if [primary] is true. - /// - /// A [ScrollController] serves several purposes. It can be used to control - /// the initial scroll position (see [ScrollController.initialScrollOffset]). - /// It can be used to control whether the scroll view should automatically - /// save and restore its scroll position in the [PageStorage] (see - /// [ScrollController.keepScrollOffset]). It can be used to read the current - /// scroll position (see [ScrollController.offset]), or change it (see - /// [ScrollController.animateTo]). - /// {@endtemplate} final ScrollController? scrollController; - /// {@template flutter.widgets.scroll_view.primary} /// Whether this is the primary scroll view associated with the parent /// [PrimaryScrollController]. /// - /// When this is true, the scroll view is scrollable even if it does not have - /// sufficient content to actually scroll. Otherwise, by default the user can - /// only scroll the view if it has sufficient content. See [physics]. - /// - /// Also when true, the scroll view is used for default [ScrollAction]s. If a - /// ScrollAction is not handled by an otherwise focused part of the - /// application, the ScrollAction will be evaluated using this scroll view, - /// for example, when executing [Shortcuts] key events like page up and down. - /// - /// On iOS, this also identifies the scroll view that will scroll to top in - /// response to a tap in the status bar. - /// {@endtemplate} - /// /// Defaults to true when [scrollController] is null. final bool? primary; - /// {@template flutter.widgets.scroll_view.shrinkWrap} /// Whether the extent of the scroll view in the [scrollDirection] should be /// determined by the contents being viewed. /// - /// If the scroll view does not shrink wrap, then the scroll view will expand - /// to the maximum allowed size in the [scrollDirection]. If the scroll view - /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must - /// be true. - /// - /// Shrink wrapping the content of the scroll view is significantly more - /// expensive than expanding to the maximum allowed size because the content - /// can expand and contract during scrolling, which means the size of the - /// scroll view needs to be recomputed whenever the scroll position changes. - /// /// Defaults to false. - /// {@endtemplate} final bool shrinkWrap; - /// {@template flutter.widgets.scroll_view.physics} /// How the scroll view should respond to user input. - /// - /// For example, determines how the scroll view continues to animate after the - /// user stops dragging the scroll view. - /// - /// Defaults to matching platform conventions. Furthermore, if [primary] is - /// false, then the user cannot scroll if there is insufficient content to - /// scroll, while if [primary] is true, they can always attempt to scroll. - /// - /// To force the scroll view to always be scrollable even if there is - /// insufficient content, as if [primary] was true but without necessarily - /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics - /// object, as in: - /// - /// ```dart - /// physics: const AlwaysScrollableScrollPhysics(), - /// ``` - /// - /// To force the scroll view to use the default platform conventions and not - /// be scrollable if there is insufficient content, regardless of the value of - /// [primary], provide an explicit [ScrollPhysics] object, as in: - /// - /// ```dart - /// physics: const ScrollPhysics(), - /// ``` - /// - /// The physics can be changed dynamically (by providing a new object in a - /// subsequent build), but new physics will only take effect if the _class_ of - /// the provided object changes. Merely constructing a new instance with a - /// different configuration is insufficient to cause the physics to be - /// reapplied. (This is because the final object used is generated - /// dynamically, which can be relatively expensive, and it would be - /// inefficient to speculatively create this object each frame to see if the - /// physics should be updated.) - /// {@endtemplate} - /// - /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the - /// [ScrollPhysics] provided by that behavior will take precedence after - /// [physics]. final ScrollPhysics? physics; /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} @@ -259,10 +144,7 @@ class StreamDraftListView extends StatelessWidget { /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; - /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} - /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will - /// dismiss the keyboard automatically. - /// {@endtemplate} + /// Defines how this [ScrollView] will dismiss the keyboard automatically. final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; /// {@macro flutter.widgets.scrollable.restorationId} @@ -317,33 +199,93 @@ class StreamDraftListView extends StatelessWidget { emptyTitle: Text(context.translations.emptyMessagesText), ), ), - loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( + loadMoreErrorBuilder: (context, error) => _LoadMoreError( onTap: controller.retry, - error: Text(context.translations.loadingMessagesError), + error: context.translations.loadingMessagesError, ), - loadMoreIndicatorBuilder: (context) => Center( + loadMoreIndicatorBuilder: (context) => const Center( child: Padding( - padding: const EdgeInsets.all(16), - child: StreamLoadingSpinner(), + padding: EdgeInsets.all(16), + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive(), + ), ), ), loadingBuilder: (context) => loadingBuilder?.call(context) ?? const Center( - child: StreamScrollViewLoadingWidget(), + child: SizedBox.square( + dimension: 42, + child: CircularProgressIndicator.adaptive(), + ), ), errorBuilder: (context, error) => errorBuilder?.call(context, error) ?? Center( - child: StreamScrollViewErrorWidget( - errorTitle: Text(context.translations.loadingMessagesError), - onRetryPressed: controller.refresh, + child: _ErrorWidget( + errorTitle: context.translations.loadingMessagesError, + onRetry: controller.refresh, ), ), ); } } +/// A simple error widget shown when the draft list fails to load. +class _ErrorWidget extends StatelessWidget { + const _ErrorWidget({ + required this.errorTitle, + required this.onRetry, + }); + + final String errorTitle; + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(errorTitle), + const SizedBox(height: 8), + TextButton( + onPressed: onRetry, + child: const Text('Retry'), + ), + ], + ); + } +} + +/// A simple load-more error widget shown inline in the list. +class _LoadMoreError extends StatelessWidget { + const _LoadMoreError({ + required this.onTap, + required this.error, + }); + + final VoidCallback onTap; + final String error; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(error, style: const TextStyle(color: Colors.white)), + const Icon(Icons.refresh, color: Colors.white), + ], + ), + ), + ); + } +} + /// A widget that is used to display a separator between /// [StreamDraftListTile] items. class StreamDraftListSeparator extends StatelessWidget {