diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart index faf08fe8..acb3d1ec 100644 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/back_button.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/unread_indicator.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import '../stream_chat_flutter.dart'; - +/// Back button implementation class StreamBackButton extends StatelessWidget { + + /// Constructor for creating back button const StreamBackButton({ Key? key, this.onPressed, @@ -12,15 +14,17 @@ class StreamBackButton extends StatelessWidget { this.cid, }) : super(key: key); + /// Callback for when button is pressed final VoidCallback? onPressed; + + /// Show unread count final bool showUnreads; /// Channel cid used to retrieve unread count final String? cid; @override - Widget build(BuildContext context) { - return Stack( + Widget build(BuildContext context) => Stack( alignment: Alignment.center, children: [ RawMaterialButton( @@ -54,5 +58,4 @@ class StreamBackButton extends StatelessWidget { ), ], ); - } } diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 911b306b..89f19466 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import '../stream_chat_flutter.dart'; -import 'channel_info.dart'; -import 'option_list_tile.dart'; - +/// Bottom Sheet with options class ChannelBottomSheet extends StatefulWidget { - final VoidCallback? onViewInfoTap; + /// Constructor for creating bottom sheet + const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key); - const ChannelBottomSheet({this.onViewInfoTap}); + /// Callback when 'View Info' is tapped + final VoidCallback? onViewInfoTap; @override _ChannelBottomSheetState createState() => _ChannelBottomSheetState(); @@ -29,7 +30,7 @@ class _ChannelBottomSheetState extends State { return Material( color: StreamChatTheme.of(context).colorTheme.white, clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -120,7 +121,7 @@ class _ChannelBottomSheetState extends State { ), borderRadius: BorderRadius.circular(32), onlineIndicatorConstraints: - BoxConstraints.tight(Size(12, 12)), + BoxConstraints.tight(const Size(12, 12)), ), const SizedBox( height: 6, diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 1882c5b0..8b0eb8fb 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -4,13 +4,9 @@ import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_name.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import './channel_name.dart'; -import '../stream_chat_flutter.dart'; -import 'channel_image.dart'; -import 'connection_status_builder.dart'; - /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png) /// @@ -43,16 +39,36 @@ import 'connection_status_builder.dart'; /// Usually you would use this widget as an [AppBar] inside a [Scaffold]. /// However you can also use it as a normal widget. /// -/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel. -/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates. +/// Make sure to have a [StreamChannel] ancestor in order to provide the +/// information about the channel. +/// Every part of the widget uses a [StreamBuilder] to render the channel +/// information as soon as it updates. /// /// By default the widget shows a backButton that calls [Navigator.pop]. -/// You can disable this button using the [showBackButton] property of just override the behaviour +/// You can disable this button using the [showBackButton] property of just +/// override the behaviour /// with [onBackPressed]. /// -/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. +/// The widget components render the ui based on the first ancestor of type +/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// Modify it to change the widget appearance. class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { + /// Creates a channel header + const ChannelHeader({ + Key? key, + this.showBackButton = true, + this.onBackPressed, + this.onTitleTap, + this.showTypingIndicator = true, + this.onImageTap, + this.showConnectionStateTile = false, + this.title, + this.subtitle, + this.leading, + this.actions, + }) : preferredSize = const Size.fromHeight(kToolbarHeight), + super(key: key); + /// True if this header shows the leading back button final bool showBackButton; @@ -69,6 +85,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// If true the typing indicator will be rendered if a user is typing final bool showTypingIndicator; + /// Show connection tile on header final bool showConnectionStateTile; /// Title widget @@ -84,22 +101,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the [ChannelImage] final List? actions; - /// Creates a channel header - ChannelHeader({ - Key? key, - this.showBackButton = true, - this.onBackPressed, - this.onTitleTap, - this.showTypingIndicator = true, - this.onImageTap, - this.showConnectionStateTile = false, - this.title, - this.subtitle, - this.leading, - this.actions, - }) : preferredSize = Size.fromHeight(kToolbarHeight), - super(key: key); - @override Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; @@ -110,7 +111,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { onPressed: onBackPressed, showUnreads: true, ) - : SizedBox()); + : const SizedBox()); return ConnectionStatusBuilder( statusBuilder: (context, status) { @@ -131,6 +132,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { } return InfoTile( + // ignore: avoid_bool_literals_in_conditional_expressions showMessage: showConnectionStateTile ? showStatus : false, message: statusString, child: AppBar( @@ -165,7 +167,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { centerTitle: true, title: InkWell( onTap: onTitleTap, - child: Container( + child: SizedBox( height: preferredSize.height, width: preferredSize.width, child: Column( @@ -178,7 +180,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { .channelHeaderTheme .title, ), - SizedBox(height: 2), + const SizedBox(height: 2), subtitle ?? ChannelInfo( showTypingIndicator: showTypingIndicator, diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart index 74983505..3f3a38be 100644 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ b/packages/stream_chat_flutter/lib/src/channel_image.dart @@ -36,12 +36,14 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// } /// ``` /// -/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates. +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. /// /// By default the widget radius size is 40x40 pixels. /// Set the property [constraints] to set a custom dimension. /// -/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. +/// The widget renders the ui based on the first ancestor of type +/// [StreamChatTheme]. /// Modify it to change the widget appearance. class ChannelImage extends StatelessWidget { /// Instantiate a new ChannelImage @@ -56,6 +58,7 @@ class ChannelImage extends StatelessWidget { this.selectionThickness = 4, }) : super(key: key); + /// [BorderRadius] to display the widget final BorderRadius? borderRadius; /// The channel to show the image of @@ -67,10 +70,13 @@ class ChannelImage extends StatelessWidget { /// The function called when the image is tapped final VoidCallback? onTap; + /// If image is selected final bool selected; + /// Selection color for image final Color? selectionColor; + /// Thickness of selection image final double selectionThickness; @override @@ -91,8 +97,7 @@ class ChannelImage extends StatelessWidget { stream: streamChat.client.state.usersStream.map( (users) => users[otherMember?.userId] ?? otherMember!.user!), initialData: otherMember!.user, - builder: (context, snapshot) { - return UserAvatar( + builder: (context, snapshot) => UserAvatar( borderRadius: borderRadius ?? StreamChatTheme.of(context) .channelPreviewTheme @@ -109,14 +114,14 @@ class ChannelImage extends StatelessWidget { selectionColor: selectionColor ?? StreamChatTheme.of(context).colorTheme.accentBlue, selectionThickness: selectionThickness, - ); - }); + )); } else { final images = channel.state?.members .where((member) => member.user?.id != streamChat.user?.id && member.user?.extraData['image'] != null) .take(4) + // ignore: cast_nullable_to_non_nullable .map((e) => e.user?.extraData['image'] as String) .toList(); return GroupImage( @@ -161,8 +166,7 @@ class ChannelImage extends StatelessWidget { if (image != null) CachedNetworkImage( imageUrl: image, - errorWidget: (_, __, ___) { - return Center( + errorWidget: (_, __, ___) => Center( child: Text( snapshot.data?.containsKey('name') ?? false ? snapshot.data!['name'][0] @@ -172,8 +176,7 @@ class ChannelImage extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - ); - }, + ), fit: BoxFit.cover, ) else diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index abd1680c..72f6cf18 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -6,6 +6,14 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'connection_status_builder.dart'; class ChannelInfo extends StatelessWidget { + const ChannelInfo({ + Key? key, + required this.channel, + this.textStyle, + this.showTypingIndicator = true, + }) : super(key: key); + + /// The channel about which the info is to be displayed final Channel channel; /// The style of the text displayed @@ -14,13 +22,6 @@ class ChannelInfo extends StatelessWidget { /// If true the typing indicator will be rendered if a user is typing final bool showTypingIndicator; - const ChannelInfo({ - Key? key, - required this.channel, - this.textStyle, - this.showTypingIndicator = true, - }) : super(key: key); - @override Widget build(BuildContext context) { final client = StreamChat.of(context).client; @@ -93,10 +94,10 @@ class ChannelInfo extends StatelessWidget { Widget _buildConnectingTitleState(BuildContext context) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( + const SizedBox( height: 16, width: 16, - child: const Center( + child: Center( child: CircularProgressIndicator(), ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index bb103f7b..c8d2aa69 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -6,10 +6,7 @@ import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'connection_status_builder.dart'; -import 'info_tile.dart'; -import 'stream_chat.dart'; - +/// Widget builder for title typedef TitleBuilder = Widget Function( BuildContext context, ConnectionStatus status, @@ -42,10 +39,13 @@ typedef TitleBuilder = Widget Function( /// Usually you would use this widget as an [AppBar] inside a [Scaffold]. /// However you can also use it as a normal widget. /// -/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status. -/// However you can also pass your own [StreamChatClient] if you don't have it in the widget tree. +/// The widget by default uses the inherited [StreamChatClient] +/// to fetch information about the status. +/// However you can also pass your own [StreamChatClient] +/// if you don't have it in the widget tree. /// -/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelListHeaderTheme] property. +/// The widget components render the ui based on the first ancestor of type +/// [StreamChatTheme] and on its [ChannelListHeaderTheme] property. /// Modify it to change the widget appearance. class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// Instantiates a ChannelListHeader @@ -75,8 +75,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// Callback to call when pressing the new chat button. final VoidCallback? onNewChatButtonTap; + /// Show connection state tile final bool showConnectionStateTile; + /// Callback before navigation is performed final VoidCallback? preNavigationCallback; /// Subtitle widget @@ -113,6 +115,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } return InfoTile( + // ignore: avoid_bool_literals_in_conditional_expressions showMessage: showConnectionStateTile ? showStatus : false, message: statusString, child: AppBar( @@ -143,7 +146,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { .avatarTheme ?.constraints, ) - : Offstage(), + : const Offstage(), ), actions: actions ?? [ @@ -151,7 +154,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { child: IconButton( icon: ConnectionStatusBuilder( statusBuilder: (context, status) { - var color; + Color? color; switch (status) { case ConnectionStatus.connected: color = StreamChatTheme.of(context) @@ -193,11 +196,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { case ConnectionStatus.disconnected: return _buildDisconnectedTitleState(context, _client); default: - return Offstage(); + return const Offstage(); } }, ), - subtitle ?? Offstage(), + subtitle ?? const Offstage(), ], ), ), @@ -213,18 +216,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ), ); - Widget _buildConnectingTitleState(BuildContext context) { - return Row( + Widget _buildConnectingTitleState(BuildContext context) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( + const SizedBox( height: 16, width: 16, child: Center( child: CircularProgressIndicator(), ), ), - SizedBox(width: 10), + const SizedBox(width: 10), Text( 'Searching for Network', style: StreamChatTheme.of(context) @@ -237,13 +239,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ), ], ); - } Widget _buildDisconnectedTitleState( BuildContext context, StreamChatClient client, - ) { - return Row( + ) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( @@ -275,8 +275,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ), ], ); - } @override - Size get preferredSize => Size.fromHeight(kToolbarHeight); + Size get preferredSize => const Size.fromHeight(kToolbarHeight); } diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 473007db..c58ed42f 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -3,13 +3,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import '../stream_chat_flutter.dart'; -import 'channel_bottom_sheet.dart'; -import 'channel_preview.dart'; /// Callback called when tapping on a channel typedef ChannelTapCallback = void Function(Channel, Widget?); @@ -17,6 +16,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?); /// Builder used to create a custom [ChannelPreview] from a [Channel] typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); +/// Callback for when 'View Info' is tapped typedef ViewInfoCallback = void Function(Channel); /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png) @@ -47,10 +47,12 @@ typedef ViewInfoCallback = void Function(Channel); /// ``` /// /// -/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels. +/// Make sure to have a [StreamChat] ancestor in order to provide the +/// information about the channels. /// The widget uses a [ListView.custom] to render the list of channels. /// -/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. +/// The widget components render the ui based on the first ancestor of +/// type [StreamChatTheme]. /// Modify it to change the widget appearance. class ChannelListView extends StatefulWidget { /// Instantiate a new ChannelListView @@ -94,8 +96,10 @@ class ChannelListView extends StatefulWidget { final Map? options; /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// You can sort based on last_updated, last_message_at, updated_at, + /// created_at or member_count. /// Direction can be ascending or descending. final List>? sort; @@ -137,8 +141,10 @@ class ChannelListView extends StatefulWidget { /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; + /// List of selected channels which are displayed differently final List selectedChannels; + /// Callback for when 'View Info' is tapped final ViewInfoCallback? onViewInfoTap; /// The builder that will be used in case of error @@ -203,15 +209,14 @@ class _ChannelListViewState extends State { crossAxisCount: widget.crossAxisCount, ), itemCount: channels.length, - physics: AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) { - return _gridItemBuilder(context, index, channels); - }, + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) => + _gridItemBuilder(context, index, channels), ); } else { child = ListView.separated( padding: widget.padding, - physics: AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), itemCount: channels.isNotEmpty ? channels.length + 1 : channels.length, separatorBuilder: (_, index) { @@ -220,9 +225,8 @@ class _ChannelListViewState extends State { } return _separatorBuilder(context, index); }, - itemBuilder: (context, index) { - return _listItemBuilder(context, index, channels); - }, + itemBuilder: (context, index) => + _listItemBuilder(context, index, channels), ); } } @@ -233,11 +237,9 @@ class _ChannelListViewState extends State { ); } - Widget _buildEmptyWidget(BuildContext context) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), + Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder( + builder: (context, viewportConstraints) => SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), child: Stack( children: [ ConstrainedBox( @@ -307,31 +309,27 @@ class _ChannelListViewState extends State { ), ], ), - ); - }, - ); - } + ), + ); - Widget _buildLoadingWidget(BuildContext context) { - return ListView( - padding: widget.padding, - physics: AlwaysScrollableScrollPhysics(), - children: List.generate( - 25, - (i) { - if (widget.crossAxisCount == 1) { - if (i % 2 != 0) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder!(context, i); + Widget _buildLoadingWidget(BuildContext context) => ListView( + padding: widget.padding, + physics: const AlwaysScrollableScrollPhysics(), + children: List.generate( + 25, + (i) { + if (widget.crossAxisCount == 1) { + if (i % 2 != 0) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder!(context, i); + } + return _separatorBuilder(context, i); } - return _separatorBuilder(context, i); } - } - return _buildLoadingItem(context); - }, - ), - ); - } + return _buildLoadingItem(context); + }, + ), + ); Shimmer _buildLoadingItem(BuildContext context) { if (widget.crossAxisCount > 1) { @@ -340,24 +338,24 @@ class _ChannelListViewState extends State { highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke, child: Column( children: [ - SizedBox(height: 4), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ for (int i = 0; i < widget.crossAxisCount; i++) Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), - constraints: BoxConstraints.tightFor( + constraints: const BoxConstraints.tightFor( height: 70, width: 70, ), ), ], ), - SizedBox( + const SizedBox( height: 16, ), ], @@ -373,7 +371,7 @@ class _ChannelListViewState extends State { color: StreamChatTheme.of(context).colorTheme.white, shape: BoxShape.circle, ), - constraints: BoxConstraints.tightFor( + constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), @@ -389,7 +387,7 @@ class _ChannelListViewState extends State { color: StreamChatTheme.of(context).colorTheme.white, borderRadius: BorderRadius.circular(11), ), - constraints: BoxConstraints.tightFor( + constraints: const BoxConstraints.tightFor( height: 16, width: 82, ), @@ -406,7 +404,7 @@ class _ChannelListViewState extends State { color: StreamChatTheme.of(context).colorTheme.white, borderRadius: BorderRadius.circular(11), ), - constraints: BoxConstraints.expand( + constraints: const BoxConstraints.expand( height: 16, ), ), @@ -418,7 +416,7 @@ class _ChannelListViewState extends State { color: StreamChatTheme.of(context).colorTheme.white, borderRadius: BorderRadius.circular(11), ), - constraints: BoxConstraints.tightFor( + constraints: const BoxConstraints.tightFor( height: 16, width: 42, ), @@ -430,35 +428,33 @@ class _ChannelListViewState extends State { } } - Widget _buildErrorWidget(BuildContext context, Object error) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only( - right: 2, + Widget _buildErrorWidget(BuildContext context, Object error) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + const TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: EdgeInsets.only( + right: 2, + ), + child: Icon(Icons.error_outline), ), - child: Icon(Icons.error_outline), ), - ), - TextSpan(text: 'Error loading channels'), - ], + TextSpan(text: 'Error loading channels'), + ], + ), + style: Theme.of(context).textTheme.headline6, ), - style: Theme.of(context).textTheme.headline6, - ), - TextButton( - onPressed: () => _channelListController.loadData!(), - child: Text('Retry'), - ), - ], - ), - ); - } + TextButton( + onPressed: () => _channelListController.loadData!(), + child: const Text('Retry'), + ), + ], + ), + ); Widget _listItemBuilder(BuildContext context, int i, List channels) { final channelsProvider = ChannelsBloc.of(context); @@ -471,82 +467,77 @@ class _ChannelListViewState extends State { key: ValueKey('CHANNEL-${channel.id}'), channel: channel, child: Builder( - builder: (context) { - return Slidable( - controller: _slideController, - enabled: widget.swipeToAction, - actionPane: SlidableBehindActionPane(), - actionExtentRatio: 0.12, - secondaryActions: [ + builder: (context) => Slidable( + controller: _slideController, + enabled: widget.swipeToAction, + actionPane: const SlidableBehindActionPane(), + actionExtentRatio: 0.12, + secondaryActions: [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: () { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) => StreamChannel( + channel: channel, + child: ChannelBottomSheet( + onViewInfoTap: () { + widget.onViewInfoTap?.call(channel); + }, + ), + ), + ); + }, + ), + if ([ + 'admin', + 'owner', + ].contains(channel.state!.members + .firstWhereOrNull( + (m) => m.userId == channel.client.state.user?.id) + ?.role)) IconSlideAction( color: backgroundColor, - icon: Icons.more_horiz, - onTap: () { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), + iconWidget: StreamSvgIcon.delete( + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), + onTap: () async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: + 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: StreamChatTheme.of(context).colorTheme.accentRed, ), - context: context, - builder: (context) { - return StreamChannel( - channel: channel, - child: ChannelBottomSheet( - onViewInfoTap: () { - widget.onViewInfoTap?.call(channel); - }, - ), - ); - }, ); + if (res == true) { + await channel.delete(); + } }, ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.user?.id) - ?.role)) - IconSlideAction( - color: backgroundColor, - iconWidget: StreamSvgIcon.delete( - color: StreamChatTheme.of(context).colorTheme.accentRed, - ), - onTap: () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.delete( - color: - StreamChatTheme.of(context).colorTheme.accentRed, - ), - ); - if (res == true) { - await channel.delete(); - } - }, + ], + child: Container( + color: StreamChatTheme.of(context).colorTheme.whiteSnow, + child: widget.channelPreviewBuilder?.call(context, channel) ?? + ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: () => widget.onImageTap?.call(channel), + onTap: (channel) => onTap(channel, widget.channelWidget), ), - ], - child: Container( - color: StreamChatTheme.of(context).colorTheme.whiteSnow, - child: widget.channelPreviewBuilder?.call(context, channel) ?? - ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), - onTap: (channel) => onTap(channel, widget.channelWidget), - ), - ), - ); - }, + ), + ), ), ); } else { @@ -566,12 +557,10 @@ class _ChannelListViewState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) { - return StreamChannel( - channel: client, - child: widget.channelWidget!, - ); - }, + builder: (context) => StreamChannel( + channel: client, + child: widget.channelWidget!, + ), ), ); }; @@ -593,18 +582,18 @@ class _ChannelListViewState extends State { channel: channel, borderRadius: BorderRadius.circular(32), selected: selected, - constraints: BoxConstraints.tightFor( + constraints: const BoxConstraints.tightFor( width: 64, height: 64, ), onTap: () => _getChannelTap(context), ), - SizedBox(height: 7), + const SizedBox(height: 7), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: StreamChannel( channel: channel, - child: ChannelName( + child: const ChannelName( textStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -620,35 +609,34 @@ class _ChannelListViewState extends State { Widget _buildQueryProgressIndicator( context, ChannelsBlocState channelsProvider, - ) { - return StreamBuilder( - stream: channelsProvider.queryChannelsLoading, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( - color: StreamChatTheme.of(context) - .colorTheme - .accentRed - .withOpacity(.2), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Center( - child: Text('Error loading channels'), - ), - ), - ); - } - return snapshot.data! - ? Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: const CircularProgressIndicator(), + ) => + StreamBuilder( + stream: channelsProvider.queryChannelsLoading, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Container( + color: StreamChatTheme.of(context) + .colorTheme + .accentRed + .withOpacity(.2), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: Text('Error loading channels'), ), - ) - : Offstage(); - }); - } + ), + ); + } + return snapshot.data! + ? const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ), + ) + : const Offstage(); + }); Widget _separatorBuilder(context, i) { final effect = StreamChatTheme.of(context).colorTheme.borderBottom; diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 48657419..d76bca0c 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import '../stream_chat_flutter.dart'; /// It shows the current [Channel] name using a [Text] widget. /// -/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates. +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. class ChannelName extends StatelessWidget { /// Instantiate a new ChannelName const ChannelName({ @@ -25,9 +26,8 @@ class ChannelName extends StatelessWidget { return StreamBuilder>( stream: channel.extraDataStream, initialData: channel.extraData, - builder: (context, snapshot) { - return _buildName(snapshot.data!, channel.state?.members, client); - }, + builder: (context, snapshot) => + _buildName(snapshot.data!, channel.state?.members, client), ); } @@ -35,45 +35,46 @@ class ChannelName extends StatelessWidget { Map extraData, List? members, StreamChatState client, - ) { - return LayoutBuilder( - builder: (context, constraints) { - String? title; - if (extraData['name'] == null) { - final otherMembers = - members?.where((member) => member.userId != client.user!.id); - if (otherMembers?.length == 1) { - title = otherMembers!.first.user?.name; - } else if (otherMembers?.isNotEmpty == true) { - final maxWidth = constraints.maxWidth; - final maxChars = maxWidth / (textStyle?.fontSize ?? 1); - var currentChars = 0; - final currentMembers = []; - otherMembers!.forEach((element) { - final newLength = currentChars + (element.user?.name.length ?? 0); - if (newLength < maxChars) { - currentChars = newLength; - currentMembers.add(element); - } - }); + ) => + LayoutBuilder( + builder: (context, constraints) { + String? title; + if (extraData['name'] == null) { + final otherMembers = + members?.where((member) => member.userId != client.user!.id); + if (otherMembers?.length == 1) { + title = otherMembers!.first.user?.name; + } else if (otherMembers?.isNotEmpty == true) { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / (textStyle?.fontSize ?? 1); + var currentChars = 0; + final currentMembers = []; + otherMembers!.forEach((element) { + final newLength = + currentChars + (element.user?.name.length ?? 0); + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); - final exceedingMembers = - otherMembers.length - currentMembers.length; - title = - '${currentMembers.map((e) => e.user?.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + final exceedingMembers = + otherMembers.length - currentMembers.length; + title = + '${currentMembers.map((e) => e.user?.name).join(', ')} ' + '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } else { + title = 'No title'; + } } else { - title = 'No title'; + title = extraData['name']; } - } else { - title = extraData['name']; - } - return Text( - title!, - style: textStyle, - overflow: TextOverflow.ellipsis, - ); - }, - ); - } + return Text( + title!, + style: textStyle, + overflow: TextOverflow.ellipsis, + ); + }, + ); } diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 9c396065..9c793ba6 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -3,23 +3,39 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import '../stream_chat_flutter.dart'; -import 'channel_name.dart'; - /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) /// /// It shows the current [Channel] preview. /// -/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates. +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. /// -/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView]. +/// Usually you don't use this widget as it's the default channel preview +/// used by [ChannelListView]. /// -/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. +/// The widget renders the ui based on the first ancestor of type +/// [StreamChatTheme]. /// Modify it to change the widget appearance. class ChannelPreview extends StatelessWidget { + + /// Constructor for creating [ChannelPreview] + const ChannelPreview({ + required this.channel, + Key? key, + this.onTap, + this.onLongPress, + this.onImageTap, + this.title, + this.subtitle, + this.leading, + this.sendingIndicator, + this.trailing, + }) : super(key: key); + /// Function called when tapping this widget final void Function(Channel)? onTap; @@ -38,156 +54,145 @@ class ChannelPreview extends StatelessWidget { /// Widget rendering the subtitle final Widget? subtitle; - /// Widget rendering the leading element, by default it shows the [ChannelImage] + /// Widget rendering the leading element, by default + /// it shows the [ChannelImage] final Widget? leading; - /// Widget rendering the trailing element, by default it shows the last message date + /// Widget rendering the trailing element, + /// by default it shows the last message date final Widget? trailing; - /// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget + /// Widget rendering the sending indicator, + /// by default it uses the [SendingIndicator] widget final Widget? sendingIndicator; - const ChannelPreview({ - required this.channel, - Key? key, - this.onTap, - this.onLongPress, - this.onImageTap, - this.title, - this.subtitle, - this.leading, - this.sendingIndicator, - this.trailing, - }) : super(key: key); - @override Widget build(BuildContext context) { final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme; return StreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, - builder: (context, snapshot) { - return Opacity( - opacity: snapshot.data! ? 0.5 : 1, - child: ListTile( - visualDensity: VisualDensity.compact, - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - ), - onTap: () { - if (onTap != null) { - onTap!(channel); - } - }, - onLongPress: () { - if (onLongPress != null) { - onLongPress!(channel); - } - }, - leading: leading ?? - ChannelImage( - onTap: onImageTap, - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: title ?? - ChannelName( - textStyle: channelPreviewTheme.title, - ), - ), - StreamBuilder>( - stream: channel.state?.membersStream, - initialData: channel.state?.members, - builder: (context, snapshot) { - if (!snapshot.hasData || - snapshot.data!.isEmpty || - !snapshot.data!.any((Member e) => - e.user!.id == channel.client.state.user?.id)) { - return SizedBox(); - } - return UnreadIndicator( - cid: channel.cid, - ); - }, - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: subtitle ?? _buildSubtitle(context)), - sendingIndicator ?? - Builder( - builder: (context) { - final lastMessage = - channel.state?.messages.lastWhereOrNull( - (m) => !m.isDeleted && m.shadowed != true, - ); - if (lastMessage?.user?.id == - StreamChat.of(context).user?.id) { - return Padding( - padding: const EdgeInsets.only(right: 4), - child: SendingIndicator( - message: lastMessage!, - size: channelPreviewTheme.indicatorIconSize, - isMessageRead: channel.state!.read - ?.where((element) => - element.user.id != - channel.client.state.user!.id) - .where((element) => element.lastRead - .isAfter(lastMessage.createdAt)) - .isNotEmpty == - true, - ), + builder: (context, snapshot) => Opacity( + opacity: snapshot.data! ? 0.5 : 1, + child: ListTile( + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + ), + onTap: () { + if (onTap != null) { + onTap!(channel); + } + }, + onLongPress: () { + if (onLongPress != null) { + onLongPress!(channel); + } + }, + leading: leading ?? + ChannelImage( + onTap: onImageTap, + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: title ?? + ChannelName( + textStyle: channelPreviewTheme.title, + ), + ), + StreamBuilder>( + stream: channel.state?.membersStream, + initialData: channel.state?.members, + builder: (context, snapshot) { + if (!snapshot.hasData || + snapshot.data!.isEmpty || + !snapshot.data!.any((Member e) => + e.user!.id == channel.client.state.user?.id)) { + return const SizedBox(); + } + return UnreadIndicator( + cid: channel.cid, + ); + }, + ), + ], + ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: subtitle ?? _buildSubtitle(context)), + sendingIndicator ?? + Builder( + builder: (context) { + final lastMessage = + channel.state?.messages.lastWhereOrNull( + (m) => !m.isDeleted && m.shadowed != true, ); - } - return SizedBox(); - }, - ), - trailing ?? _buildDate(context), - ], + if (lastMessage?.user?.id == + StreamChat.of(context).user?.id) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SendingIndicator( + message: lastMessage!, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channel.state!.read + ?.where((element) => + element.user.id != + channel.client.state.user!.id) + .where((element) => element.lastRead + .isAfter(lastMessage.createdAt)) + .isNotEmpty == + true, + ), + ); + } + return const SizedBox(); + }, + ), + trailing ?? _buildDate(context), + ], + ), ), - ), + )); + } + + Widget _buildDate(BuildContext context) => StreamBuilder( + stream: channel.lastMessageAtStream, + initialData: channel.lastMessageAt, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox(); + } + final lastMessageAt = snapshot.data!.toLocal(); + + String stringDate; + final now = DateTime.now(); + + final startOfDay = DateTime(now.year, now.month, now.day); + + if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.millisecondsSinceEpoch) { + stringDate = Jiffy(lastMessageAt.toLocal()).jm; + } else if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch) { + stringDate = 'Yesterday'; + } else if (startOfDay.difference(lastMessageAt).inDays < 7) { + stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).yMd; + } + + return Text( + stringDate, + style: + StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, ); - }); - } - - Widget _buildDate(BuildContext context) { - return StreamBuilder( - stream: channel.lastMessageAtStream, - initialData: channel.lastMessageAt, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return SizedBox(); - } - final lastMessageAt = snapshot.data!.toLocal(); - - String stringDate; - final now = DateTime.now(); - - final startOfDay = DateTime(now.year, now.month, now.day); - - if (lastMessageAt.millisecondsSinceEpoch >= - startOfDay.millisecondsSinceEpoch) { - stringDate = Jiffy(lastMessageAt.toLocal()).jm; - } else if (lastMessageAt.millisecondsSinceEpoch >= - startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) { - stringDate = 'Yesterday'; - } else if (startOfDay.difference(lastMessageAt).inDays < 7) { - stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; - } else { - stringDate = Jiffy(lastMessageAt.toLocal()).yMd; - } - - return Text( - stringDate, - style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, - ); - }, - ); - } + }, + ); Widget _buildSubtitle(BuildContext context) { if (channel.isMuted) { @@ -211,66 +216,71 @@ class ChannelPreview extends StatelessWidget { ); } - Widget _buildLastMessage(BuildContext context) { - return StreamBuilder?>( - stream: channel.state!.messagesStream, - initialData: channel.state!.messages, - builder: (context, snapshot) { - final lastMessage = snapshot.data - ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); - if (lastMessage == null) { - return SizedBox(); - } + Widget _buildLastMessage(BuildContext context) => + StreamBuilder?>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, + builder: (context, snapshot) { + final lastMessage = snapshot.data + ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); + if (lastMessage == null) { + return const SizedBox(); + } - var text = lastMessage.text; - final parts = [ - ...lastMessage.attachments.map((e) { - if (e.type == 'image') { - return '📷'; - } else if (e.type == 'video') { - return '🎬'; - } else if (e.type == 'giphy') { - return '[GIF]'; - } - return e == lastMessage.attachments.last - ? (e.title ?? 'File') - : '${e.title ?? 'File'} , '; - }), - lastMessage.text ?? '', - ]; + var text = lastMessage.text; + final parts = [ + ...lastMessage.attachments.map((e) { + if (e.type == 'image') { + return '📷'; + } else if (e.type == 'video') { + return '🎬'; + } else if (e.type == 'giphy') { + return '[GIF]'; + } + return e == lastMessage.attachments.last + ? (e.title ?? 'File') + : '${e.title ?? 'File'} , '; + }), + lastMessage.text ?? '', + ]; - text = parts.join(' '); + text = parts.join(' '); - return Text.rich( - _getDisplayText( - text, - lastMessage.mentionedUsers, - lastMessage.attachments, - StreamChatTheme.of(context).channelPreviewTheme.subtitle?.copyWith( - color: StreamChatTheme.of(context) - .channelPreviewTheme - .subtitle - ?.color, - fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) - ? FontStyle.italic - : FontStyle.normal), - StreamChatTheme.of(context).channelPreviewTheme.subtitle?.copyWith( - color: StreamChatTheme.of(context) - .channelPreviewTheme - .subtitle - ?.color, - fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) - ? FontStyle.italic - : FontStyle.normal, - fontWeight: FontWeight.bold, - ), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - }, - ); - } + return Text.rich( + _getDisplayText( + text, + lastMessage.mentionedUsers, + lastMessage.attachments, + StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle + ?.copyWith( + color: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle + ?.color, + fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal), + StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle + ?.copyWith( + color: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle + ?.color, + fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal, + fontWeight: FontWeight.bold, + ), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }, + ); TextSpan _getDisplayText( String text, diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index 620c41ac..7e4e7f8e 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'stream_chat.dart'; - /// Widget that builds itself based on the latest snapshot of interaction with /// a [Stream] of type [ConnectionStatus]. /// -/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] in case no -/// stream is provided. +/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] +/// in case no stream is provided. class ConnectionStatusBuilder extends StatelessWidget { /// Creates a new ConnectionStatusBuilder const ConnectionStatusBuilder({ @@ -47,11 +46,11 @@ class ConnectionStatusBuilder extends StatelessWidget { if (errorBuilder != null) { return errorBuilder!(context, snapshot.error); } - return Offstage(); + return const Offstage(); } if (!snapshot.hasData) { if (loadingBuilder != null) return loadingBuilder!(context); - return Offstage(); + return const Offstage(); } return statusBuilder(context, snapshot.data!); }, diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index 3674e085..e1b9cfea 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -4,15 +4,20 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; /// It shows a date divider depending on the date difference class DateDivider extends StatelessWidget { - final DateTime dateTime; - final bool uppercase; + /// Constructor for creating a [DateDivider] const DateDivider({ Key? key, required this.dateTime, this.uppercase = false, }) : super(key: key); + /// [DateTime] to display + final DateTime dateTime; + + /// If text is uppercase + final bool uppercase; + @override Widget build(BuildContext context) { final createdAt = Jiffy(dateTime); @@ -22,10 +27,10 @@ class DateDivider extends StatelessWidget { if (Jiffy(createdAt).isSame(now, Units.DAY)) { dayInfo = 'Today'; } else if (Jiffy(createdAt) - .isSame(now.subtract(Duration(days: 1)), Units.DAY)) { + .isSame(now.subtract(const Duration(days: 1)), Units.DAY)) { dayInfo = 'Yesterday'; } else if (Jiffy(createdAt).isAfter( - now.subtract(Duration(days: 7)), + now.subtract(const Duration(days: 7)), Units.DAY, )) { dayInfo = createdAt.EEEE; diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 31a9995e..47a5f2ee 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -3,7 +3,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// Widget to display deleted message class DeletedMessage extends StatelessWidget { + + /// Constructor to create [DeletedMessage] const DeletedMessage({ Key? key, required this.messageTheme, @@ -29,8 +32,7 @@ class DeletedMessage extends StatelessWidget { final bool reverse; @override - Widget build(BuildContext context) { - return Transform( + Widget build(BuildContext context) => Transform( transform: Matrix4.rotationY(reverse ? pi : 0), alignment: Alignment.center, child: Material( @@ -70,5 +72,4 @@ class DeletedMessage extends StatelessWidget { ), ), ); - } } diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 11ff3253..a65272c3 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -9,9 +9,7 @@ final _emojis = Emoji.all(); /// String extension extension StringExtension on String { /// Returns the capitalized string - String capitalize() { - return '${this[0].toUpperCase()}${substring(1)}'; - } + String capitalize() => '${this[0].toUpperCase()}${substring(1)}'; /// Returns whether the string contains only emoji's or not. /// @@ -46,7 +44,9 @@ extension PlatformFileX on PlatformFile { ); } +/// extension InputDecorationX on InputDecoration { + /// InputDecoration merge(InputDecoration? other) { if (other == null) return this; return copyWith( @@ -98,7 +98,9 @@ extension InputDecorationX on InputDecoration { } } +/// Gets text scale factor through context extension BuildContextX on BuildContext { + // ignore: public_member_api_docs double get textScaleFactor => MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; }