lint changes

This commit is contained in:
Deven Joshi
2021-05-04 17:58:29 +05:30
parent edba3d2ad5
commit 9acf8d1037
13 changed files with 533 additions and 518 deletions
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package: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';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png)
///
@@ -43,16 +39,36 @@ import 'connection_status_builder.dart';
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget.
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel.
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates.
/// Make sure to have a [StreamChannel] ancestor in order to provide the
/// information about the channel.
/// Every part of the widget uses a [StreamBuilder] to render the channel
/// information as soon as it updates.
///
/// By default the widget shows a backButton that calls [Navigator.pop].
/// You can disable this button using the [showBackButton] property of just override the behaviour
/// You can disable this button using the [showBackButton] property of just
/// override the behaviour
/// with [onBackPressed].
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance.
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// Creates a channel header
const ChannelHeader({
Key? key,
this.showBackButton = true,
this.onBackPressed,
this.onTitleTap,
this.showTypingIndicator = true,
this.onImageTap,
this.showConnectionStateTile = false,
this.title,
this.subtitle,
this.leading,
this.actions,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
/// True if this header shows the leading back button
final bool showBackButton;
@@ -69,6 +85,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// If true the typing indicator will be rendered if a user is typing
final bool showTypingIndicator;
/// Show connection tile on header
final bool showConnectionStateTile;
/// Title widget
@@ -84,22 +101,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// By default it shows the [ChannelImage]
final List<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);
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png)
@@ -47,10 +47,12 @@ typedef ViewInfoCallback = void Function(Channel);
/// ```
///
///
/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels.
/// Make sure to have a [StreamChat] ancestor in order to provide the
/// information about the channels.
/// The widget uses a [ListView.custom] to render the list of channels.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// The widget components render the ui based on the first ancestor of
/// type [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelListView extends StatefulWidget {
/// Instantiate a new ChannelListView
@@ -94,8 +96,10 @@ class ChannelListView extends StatefulWidget {
final Map<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';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png)
///
/// It shows the current [Channel] preview.
///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
/// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
///
/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView].
/// Usually you don't use this widget as it's the default channel preview
/// used by [ChannelListView].
///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelPreview extends StatelessWidget {
/// Constructor for creating [ChannelPreview]
const ChannelPreview({
required this.channel,
Key? key,
this.onTap,
this.onLongPress,
this.onImageTap,
this.title,
this.subtitle,
this.leading,
this.sendingIndicator,
this.trailing,
}) : super(key: key);
/// Function called when tapping this widget
final void Function(Channel)? onTap;
@@ -38,156 +54,145 @@ class ChannelPreview extends StatelessWidget {
/// Widget rendering the subtitle
final Widget? subtitle;
/// Widget rendering the leading element, by default it shows the [ChannelImage]
/// Widget rendering the leading element, by default
/// it shows the [ChannelImage]
final Widget? leading;
/// Widget rendering the trailing element, by default it shows the last message date
/// Widget rendering the trailing element,
/// by default it shows the last message date
final Widget? trailing;
/// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget
/// Widget rendering the sending indicator,
/// by default it uses the [SendingIndicator] widget
final Widget? sendingIndicator;
const ChannelPreview({
required this.channel,
Key? key,
this.onTap,
this.onLongPress,
this.onImageTap,
this.title,
this.subtitle,
this.leading,
this.sendingIndicator,
this.trailing,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
return StreamBuilder<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;
}