lint changes

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