lint changes
This commit is contained in:
@@ -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';
|
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
@@ -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);
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
@@ -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';
|
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
/// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user