diff --git a/migrations/v10-migration.md b/migrations/v10-migration.md index 895dec3e2..c9600a3b5 100644 --- a/migrations/v10-migration.md +++ b/migrations/v10-migration.md @@ -29,6 +29,7 @@ This guide covers all breaking changes in **Stream Chat Flutter SDK v10.0.0**. W - [MessageState](#messagestate) - [File Upload](#file-upload) - [AttachmentFileUploader](#attachmentfileuploader) +- [Unread Threads Banner](#unread-threads-banner) - [Appendix: Beta Release Timeline](#appendix-beta-release-timeline) - [Migration Checklist](#migration-checklist) @@ -60,6 +61,7 @@ Each breaking change section includes an **"Introduced in"** tag so you can quic | [**Message UI**](#message-ui) | New `onAttachmentTap` signature with fallback support, generic `StreamMessageAction` | | [**Message State**](#message-state--deletion) | `MessageDeleteScope` replaces `bool hard`, delete-for-me support | | [**File Upload**](#file-upload) | Four new abstract methods on `AttachmentFileUploader` | +| [**Unread Threads Banner**](#unread-threads-banner) | Wrapper pattern with `child`, `enabled`, `onRefresh`; removed `onTap`, `minHeight` | --- @@ -991,6 +993,58 @@ class CustomAttachmentFileUploader implements AttachmentFileUploader { --- +## Unread Threads Banner + +#### Key Changes: + +- `StreamUnreadThreadsBanner` is now a **wrapper widget** instead of a standalone banner placed in a `Column`. +- New `child` parameter — wrap your `StreamThreadListView` instead of placing the banner as a sibling. +- `onTap` (`VoidCallback?`) replaced by `onRefresh` (`Future Function()?`) — shows a loading spinner while the future is pending. +- `minHeight` parameter **removed**. +- `margin` default changed from `EdgeInsets.symmetric(horizontal: 8, vertical: 6)` to `EdgeInsets.zero`. +- `padding` default changed from `EdgeInsets.symmetric(horizontal: 16)` to `EdgeInsets.all(spacing.sm)`. + +#### Migration Steps: + +**Before:** +```dart +Column( + children: [ + ValueListenableBuilder( + valueListenable: controller.unseenThreadIds, + builder: (_, unreadThreads, __) => StreamUnreadThreadsBanner( + unreadThreads: unreadThreads, + onTap: () => controller + .refresh(resetValue: false) + .then((_) => controller.clearUnseenThreadIds()), + ), + ), + Expanded( + child: StreamThreadListView(controller: controller), + ), + ], +); +``` + +**After:** +```dart +ValueListenableBuilder>( + valueListenable: controller.unseenThreadIds, + builder: (context, unseenThreadIds, child) => StreamUnreadThreadsBanner( + enabled: unseenThreadIds.isNotEmpty, + unreadThreads: unseenThreadIds, + onRefresh: () async { + await controller.refresh(resetValue: false); + controller.clearUnseenThreadIds(); + }, + child: child!, + ), + child: StreamThreadListView(controller: controller), +); +``` + +--- + ## Appendix: Beta Release Timeline This appendix provides a chronological reference of breaking changes by beta version for users upgrading from specific pre-release versions. @@ -1036,6 +1090,12 @@ This appendix provides a chronological reference of breaking changes by beta ver ## Migration Checklist +### Unread Threads Banner +- [ ] Replace `Column` + `Expanded` layout with `StreamUnreadThreadsBanner(child: StreamThreadListView(...))` +- [ ] Replace `onTap` with `onRefresh` (returns `Future`) +- [ ] Add `enabled: true` to show the banner (defaults to hidden) +- [ ] Remove `minHeight` parameter if used + ### For v10.0.0-beta.12: - [ ] Replace `ArgumentError('The size of the attachment is...')` with `AttachmentTooLargeError` (provides `fileSize` and `maxSize` properties) - [ ] Replace `ArgumentError('The maximum number of attachments is...')` with `AttachmentLimitReachedError` (provides `maxCount` property) diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 45b756897..55f672cf1 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -544,6 +544,9 @@ abstract class Translations { /// The label for "$count new threads" String newThreadsLabel({required int count}); + /// The label for "Loading..." + String get loadingLabel; + /// The label for "Slide to cancel" String get slideToCancelLabel; @@ -1326,6 +1329,9 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments return '$count new threads'; } + @override + String get loadingLabel => 'Loading...'; + @override String get slideToCancelLabel => 'Slide to cancel'; diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart index 35090db82..e746fc927 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/misc/timestamp.dart'; import 'package:stream_chat_flutter/src/utils/date_formatter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamThreadListTile} /// A widget that displays a thread in a list. @@ -105,75 +106,77 @@ class _DefaultStreamThreadListTile extends StatelessWidget { channel?.formatName(currentUser: currentUser) ?? avatarUser?.name ?? context.translations.noTitleText; final participantUsers = thread.threadParticipants.map((it) => it.user).nonNulls.toList(growable: false); - return Material( - color: effectiveBackgroundColor, - child: InkWell( + return StreamListTileTheme( + data: StreamListTileThemeData( + contentPadding: effectivePadding, + backgroundColor: WidgetStatePropertyAll(effectiveBackgroundColor), + ), + child: StreamListTileContainer( + enabled: true, + selected: false, onTap: props.onTap, onLongPress: props.onLongPress, - child: Padding( - padding: effectivePadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (avatarUser case final user?) - Padding( - padding: const EdgeInsetsDirectional.only(end: 12), - child: StreamUserAvatar( - user: user, - size: StreamAvatarSize.xl, - ), - ) - else - const Padding( - padding: EdgeInsetsDirectional.only(end: 12), - child: SizedBox.square(dimension: 40), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (avatarUser case final user?) + Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: StreamUserAvatar( + user: user, + size: StreamAvatarSize.xl, ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ThreadTitle( - channelName: channelName, - style: effectiveChannelNameStyle, - ), + ) + else + const Padding( + padding: EdgeInsetsDirectional.only(end: 12), + child: SizedBox.square(dimension: 40), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ThreadTitle( + channelName: channelName, + style: effectiveChannelNameStyle, + ), + ), + if (unreadMessageCount case final count? when count > 0) ...[ + const SizedBox(width: 8), + ThreadUnreadCount( + unreadCount: count, + style: effectiveUnreadCountStyle, + backgroundColor: effectiveUnreadCountBackgroundColor, ), - if (unreadMessageCount case final count? when count > 0) ...[ - const SizedBox(width: 8), - ThreadUnreadCount( - unreadCount: count, - style: effectiveUnreadCountStyle, - backgroundColor: effectiveUnreadCountBackgroundColor, - ), - ], ], - ), - const SizedBox(height: 2), - ThreadRootMessagePreview( - parentMessage: parentMessage, - channel: channel, - language: language, - style: effectiveReplyToMessageStyle, - emptyStyle: effectiveLatestReplyMessageStyle, - ), - const SizedBox(height: 8), - ThreadFooter( - participantUsers: participantUsers, - replyCount: thread.replyCount, - latestActivityAt: latestActivityAt, - replyCountStyle: effectiveReplyCountStyle, - timestampStyle: effectiveTimestampStyle, - timestampFormatter: effectiveTimestampFormatter, - ), - ], - ), + ], + ), + const SizedBox(height: 2), + ThreadRootMessagePreview( + parentMessage: parentMessage, + channel: channel, + language: language, + style: effectiveReplyToMessageStyle, + emptyStyle: effectiveLatestReplyMessageStyle, + ), + const SizedBox(height: 8), + ThreadFooter( + participantUsers: participantUsers, + replyCount: thread.replyCount, + latestActivityAt: latestActivityAt, + replyCountStyle: effectiveReplyCountStyle, + timestampStyle: effectiveTimestampStyle, + timestampFormatter: effectiveTimestampFormatter, + ), + ], ), - ], - ), + ), + ], ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart index 5ccd0826d..20ab75076 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart @@ -25,6 +25,9 @@ typedef StreamThreadListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidget /// /// Uses a [StreamThreadListController] to load threads in paginated form. /// +/// Wrap with [StreamUnreadThreadsBanner] to show a banner above the list when +/// new unseen threads are available. +/// /// Each row is rendered using [StreamThreadListTile], which can be customized /// app-wide through [StreamComponentFactory]. /// @@ -40,6 +43,7 @@ typedef StreamThreadListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidget /// ``` /// /// See also: +/// * [StreamUnreadThreadsBanner], which wraps this view to show new threads. /// * [StreamMessageWidget], which renders each thread's parent message. /// * [StreamThreadListController] /// {@endtemplate} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart index 52d925aa0..1890b2240 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart @@ -1,86 +1,159 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template unreadThreadsBanner} -/// A widget that shows a banner with the number of unread threads. +/// A widget that displays an unread-threads banner. /// -/// This widget can be used to show a banner with the number of unread threads -/// on the top of the [ThreadListView]. +/// When a [child] is provided, the banner appears above it — similar to how +/// [RefreshIndicator] wraps a scrollable. When [child] is omitted, the widget +/// renders only the banner itself. +/// +/// When [enabled] is `false` (the default), the banner is hidden and only the +/// [child] (if any) is rendered. Set [enabled] to `true` and provide +/// [unreadThreads] to show the banner. +/// +/// Example: +/// +/// ```dart +/// StreamUnreadThreadsBanner( +/// enabled: true, +/// unreadThreads: unseenThreadIds, +/// onRefresh: () async { +/// await controller.refresh(resetValue: false); +/// controller.clearUnseenThreadIds(); +/// }, +/// child: StreamThreadListView(controller: controller), +/// ) +/// ``` /// {@endtemplate} -class StreamUnreadThreadsBanner extends StatelessWidget { +class StreamUnreadThreadsBanner extends StatefulWidget { /// {@macro unreadThreadsBanner} const StreamUnreadThreadsBanner({ super.key, - required this.unreadThreads, - this.onTap, - this.minHeight = 52, - this.margin = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.child, + this.enabled = false, + this.unreadThreads = const {}, + this.onRefresh, + this.margin = EdgeInsets.zero, + this.padding, }); - /// The set of all the unread threads. - final Set unreadThreads; + /// The widget below the banner in the tree. + /// + /// When `null`, only the banner is rendered without any wrapped content. + final Widget? child; + + /// Whether the banner is enabled. + /// + /// When `false`, the banner is hidden and only [child] is rendered. + /// + /// Defaults to `false`. + final bool enabled; - /// Optional callback to handle tap events. - final VoidCallback? onTap; + /// The set of all the unread thread IDs. + final Set unreadThreads; - /// The minimum height of the banner. + /// Called when the user taps the banner. /// - /// Defaults to 52. - final double minHeight; + /// While the returned [Future] is pending, the banner shows a loading + /// spinner instead of the refresh icon and label. + final Future Function()? onRefresh; /// The margin applied to the banner. /// - /// Defaults to `EdgeInsets.symmetric(horizontal: 8, vertical: 6)`. + /// Defaults to [EdgeInsets.zero]. final EdgeInsetsGeometry? margin; /// The padding applied to the banner. /// - /// Defaults to `EdgeInsets.symmetric(horizontal: 16)`. - final EdgeInsetsGeometry padding; + /// Defaults to `EdgeInsets.all(spacing.sm)`. + final EdgeInsetsGeometry? padding; @override - Widget build(BuildContext context) { - if (unreadThreads.isEmpty) { - return const Empty(); + State createState() => _StreamUnreadThreadsBannerState(); +} + +class _StreamUnreadThreadsBannerState extends State { + bool _isRefreshing = false; + + Future _handleTap() async { + if (_isRefreshing) return; + + setState(() => _isRefreshing = true); + try { + await widget.onRefresh?.call(); + } finally { + if (mounted) setState(() => _isRefreshing = false); } + } + + @override + Widget build(BuildContext context) { + final banner = widget.enabled ? _buildBanner(context) : null; + final child = widget.child; + + if (child == null) return banner ?? const Empty(); + + return Column( + children: [ + if (banner != null) banner, + Expanded(child: child), + ], + ); + } - final theme = StreamChatTheme.of(context); + Widget _buildBanner(BuildContext context) { + final isVisible = _isRefreshing || widget.unreadThreads.isNotEmpty; + if (!isVisible) return const Empty(); return GestureDetector( - onTap: onTap, + onTap: _isRefreshing ? null : _handleTap, child: Container( - margin: margin, - padding: padding, - constraints: BoxConstraints(minHeight: minHeight), - decoration: BoxDecoration( - color: theme.colorTheme.textHighEmphasis, - borderRadius: BorderRadius.circular(16), + margin: widget.margin, + padding: widget.padding ?? EdgeInsets.all(context.streamSpacing.sm), + color: context.streamColorScheme.backgroundSurface, + child: _isRefreshing ? _buildLoading(context) : _buildContent(context), + ), + ); + } + + Widget _buildLoading(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamLoadingSpinner( + color: context.streamColorScheme.textSecondary, ), - child: Row( - children: [ - Expanded( - child: Text( - context.translations.newThreadsLabel( - count: unreadThreads.length, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.headline.copyWith( - color: theme.colorTheme.barsBg, - ), - ), - ), - Icon( - context.streamIcons.refresh, - color: theme.colorTheme.barsBg, - ), - ], + SizedBox(width: context.streamSpacing.xs), + Text( + context.translations.loadingLabel, + style: context.streamTextTheme.metadataEmphasis, ), - ), + ], + ); + } + + Widget _buildContent(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + context.streamIcons.refresh, + size: 20, + color: context.streamColorScheme.textSecondary, + ), + SizedBox(width: context.streamSpacing.xs), + Text( + context.translations.newThreadsLabel( + count: widget.unreadThreads.length, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.streamTextTheme.metadataEmphasis, + ), + ], ); } } diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png index ffd86e82d..f3d87824a 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png and b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_dark.png b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_dark.png index 53c009e34..e8bd3164a 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_dark.png and b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png index c60497c86..3224ca610 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png and b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_view_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_view_test.dart index d387ecbc9..bccb89733 100644 --- a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_view_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -8,6 +9,11 @@ import '../../mocks.dart'; class MockStreamThreadListController extends Mock implements StreamThreadListController { @override PagedValue value = const PagedValue.loading(); + + final _unseenThreadIds = ValueNotifier>(const {}); + + @override + ValueListenable> get unseenThreadIds => _unseenThreadIds; } void main() { diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart index 4c1a8e7a8..120bb8c4d 100644 --- a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart +++ b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart @@ -10,7 +10,10 @@ void main() { constraints: const BoxConstraints.tightFor(width: 400, height: 100), builder: () => _wrapWithMaterialApp( brightness: brightness, - const StreamUnreadThreadsBanner(unreadThreads: {'id1', 'id2', 'id3'}), + const StreamUnreadThreadsBanner( + enabled: true, + unreadThreads: {'id1', 'id2', 'id3'}, + ), ), ); } diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index f11807d4b..7866984fd 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -644,6 +644,9 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { return '$count new threads'; } + @override + String get loadingLabel => 'Loading...'; + @override String get slideToCancelLabel => 'Slide to cancel'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart index 07fcd715d..4bc1df9fb 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart @@ -624,6 +624,9 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { return '$count fils nous'; } + @override + String get loadingLabel => 'Carregant...'; + @override String get slideToCancelLabel => 'Llisca per cancel·lar'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart index 0a5e28296..504151201 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart @@ -622,6 +622,9 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { return '$count neue Threads'; } + @override + String get loadingLabel => 'Wird geladen...'; + @override String get slideToCancelLabel => 'Zum Abbrechen schieben'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 4c4bd94f4..a082fb8c1 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -622,6 +622,9 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { return '$count new threads'; } + @override + String get loadingLabel => 'Loading...'; + @override String get slideToCancelLabel => 'Slide to cancel'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index f0df8c13a..c6e41b105 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -626,6 +626,9 @@ No es posible añadir más de $limit archivos adjuntos return '$count nuevos hilos'; } + @override + String get loadingLabel => 'Cargando...'; + @override String get slideToCancelLabel => 'Desliza para cancelar'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 6740353fe..ee6c5b526 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -628,6 +628,9 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ return '$count Nouveaux fils'; } + @override + String get loadingLabel => 'Chargement...'; + @override String get slideToCancelLabel => 'Glissez pour annuler'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 51586e045..bbb36712c 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -626,6 +626,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { return '$count नए थ्रेड्स'; } + @override + String get loadingLabel => 'लोड हो रहा है...'; + @override String get slideToCancelLabel => 'रद्द करने के लिए स्लाइड करें'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 042dca502..2a0e3d0b3 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -631,6 +631,9 @@ Attenzione: il limite massimo di $limit file è stato superato. return '$count nuovi thread'; } + @override + String get loadingLabel => 'Caricamento...'; + @override String get slideToCancelLabel => 'Scorri per annullare'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 8f99d1e0b..bd85fd957 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -609,6 +609,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { return '$count 件の新しいスレッド'; } + @override + String get loadingLabel => '読み込み中...'; + @override String get slideToCancelLabel => 'スライドでキャンセル'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 8b828a430..22da20982 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -612,6 +612,9 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { return '$count개의 새 스레드'; } + @override + String get loadingLabel => '로딩 중...'; + @override String get slideToCancelLabel => '슬라이드하여 취소'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart index 231203331..657c9d5d1 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart @@ -610,6 +610,9 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { return '$count nye tråder'; } + @override + String get loadingLabel => 'Laster...'; + @override String get slideToCancelLabel => 'Gli for å avbryte'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 9495771f6..a3336955e 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -625,6 +625,9 @@ Não é possível adicionar mais de $limit arquivos de uma vez return '$count novos tópicos'; } + @override + String get loadingLabel => 'Carregando...'; + @override String get slideToCancelLabel => 'Deslize para cancelar'; diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index c8ac6ae57..e72effb30 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -300,6 +300,7 @@ void main() { expect(localizations.voteCountLabel(count: 3), isNotNull); expect(localizations.repliedToLabel, isNotNull); expect(localizations.newThreadsLabel(count: 3), isNotNull); + expect(localizations.loadingLabel, isNotNull); expect(localizations.slideToCancelLabel, isNotNull); expect(localizations.holdToRecordLabel, isNotNull); expect(localizations.sendAnywayLabel, isNotNull); diff --git a/sample_app/lib/pages/thread_list_page.dart b/sample_app/lib/pages/thread_list_page.dart index 4201b30e2..ef0bd215a 100644 --- a/sample_app/lib/pages/thread_list_page.dart +++ b/sample_app/lib/pages/thread_list_page.dart @@ -25,63 +25,62 @@ class _ThreadListPageState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - ValueListenableBuilder( - valueListenable: controller.unseenThreadIds, - builder: (_, unreadThreads, __) => StreamUnreadThreadsBanner( - unreadThreads: unreadThreads, - onTap: () => controller.refresh(resetValue: false).then((_) => controller.clearUnseenThreadIds()), - ), - ), - Expanded( - child: StreamThreadListView( - controller: controller, - onThreadTap: (thread) async { - final channelCid = thread.channelCid; + return ValueListenableBuilder>( + valueListenable: controller.unseenThreadIds, + builder: (context, unseenThreadIds, child) => StreamUnreadThreadsBanner( + enabled: unseenThreadIds.isNotEmpty, + unreadThreads: unseenThreadIds, + onRefresh: () async { + await controller.refresh(resetValue: false); + controller.clearUnseenThreadIds(); + }, + child: child, + ), + child: StreamThreadListView( + controller: controller, + onThreadTap: (thread) async { + final channelCid = thread.channelCid; - final channel = StreamChat.of(context).client.channel( - channelCid.split(':')[0], - id: channelCid.split(':')[1], - ); + final channel = StreamChat.of(context).client.channel( + channelCid.split(':')[0], + id: channelCid.split(':')[1], + ); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return StreamChannel( - channel: channel, - initialMessageId: thread.draft?.parentId, - child: BetterStreamBuilder( - initialData: thread.parentMessage, - stream: channel.state?.messagesStream - .map( - (messages) => messages.firstWhereOrNull( - (m) => m.id == thread.parentMessage?.id, - ), - ) - .where((msg) => msg != null) - .cast(), - builder: (_, parentMessage) { - return ThreadPage( - parent: parentMessage, - onViewInChannelTap: (message) { - GoRouter.of(context).goNamed( - Routes.CHANNEL_PAGE.name, - pathParameters: Routes.CHANNEL_PAGE.params(channel), - queryParameters: {'mid': message.id}, - ); - }, + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return StreamChannel( + channel: channel, + initialMessageId: thread.draft?.parentId, + child: BetterStreamBuilder( + initialData: thread.parentMessage, + stream: channel.state?.messagesStream + .map( + (messages) => messages.firstWhereOrNull( + (m) => m.id == thread.parentMessage?.id, + ), + ) + .where((msg) => msg != null) + .cast(), + builder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + onViewInChannelTap: (message) { + GoRouter.of(context).goNamed( + Routes.CHANNEL_PAGE.name, + pathParameters: Routes.CHANNEL_PAGE.params(channel), + queryParameters: {'mid': message.id}, ); }, - ), - ); - }, - ), - ); - }, - ), - ), - ], + ); + }, + ), + ); + }, + ), + ); + }, + ), ); } }