lint changes
This commit is contained in:
@@ -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 {
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ChannelBottomSheet> {
|
||||
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<ChannelBottomSheet> {
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
onlineIndicatorConstraints:
|
||||
BoxConstraints.tight(Size(12, 12)),
|
||||
BoxConstraints.tight(const Size(12, 12)),
|
||||
),
|
||||
const SizedBox(
|
||||
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/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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -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<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
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/// 
|
||||
@@ -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<String, dynamic>? 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<SortOption<ChannelModel>>? 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<Channel> 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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
}
|
||||
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<ChannelListView> {
|
||||
);
|
||||
}
|
||||
|
||||
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<ChannelListView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildErrorWidget(BuildContext context, Object error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
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<Channel> channels) {
|
||||
final channelsProvider = ChannelsBloc.of(context);
|
||||
@@ -471,82 +467,77 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
||||
channel: channel,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Slidable(
|
||||
controller: _slideController,
|
||||
enabled: widget.swipeToAction,
|
||||
actionPane: SlidableBehindActionPane(),
|
||||
actionExtentRatio: 0.12,
|
||||
secondaryActions: <Widget>[
|
||||
builder: (context) => Slidable(
|
||||
controller: _slideController,
|
||||
enabled: widget.swipeToAction,
|
||||
actionPane: const SlidableBehindActionPane(),
|
||||
actionExtentRatio: 0.12,
|
||||
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(
|
||||
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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
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<ChannelListView> {
|
||||
Widget _buildQueryProgressIndicator(
|
||||
context,
|
||||
ChannelsBlocState channelsProvider,
|
||||
) {
|
||||
return StreamBuilder<bool>(
|
||||
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<bool>(
|
||||
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;
|
||||
|
||||
@@ -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<Map<String, dynamic>>(
|
||||
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<String, dynamic> extraData,
|
||||
List<Member>? 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 = <Member>[];
|
||||
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 = <Member>[];
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// 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<bool>(
|
||||
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: <Widget>[
|
||||
Flexible(
|
||||
child: title ??
|
||||
ChannelName(
|
||||
textStyle: channelPreviewTheme.title,
|
||||
),
|
||||
),
|
||||
StreamBuilder<List<Member>>(
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
Flexible(
|
||||
child: title ??
|
||||
ChannelName(
|
||||
textStyle: channelPreviewTheme.title,
|
||||
),
|
||||
),
|
||||
StreamBuilder<List<Member>>(
|
||||
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: <Widget>[
|
||||
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<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) {
|
||||
if (channel.isMuted) {
|
||||
@@ -211,66 +216,71 @@ class ChannelPreview extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLastMessage(BuildContext context) {
|
||||
return StreamBuilder<List<Message>?>(
|
||||
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<List<Message>?>(
|
||||
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 = <String>[
|
||||
...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 = <String>[
|
||||
...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,
|
||||
|
||||
@@ -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!);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user