feat: added customization options in main widgets (#312)
* polish channelpreview customization options * polish channelheader customization options * polish messageinput customization options * polish threadheader customization options * polish channellistheader customization options * show the parent message if no messages in thread * fix edit message * fix channelinfo textstyle * fix review * fix useravatar * add ontitle tap to thread header * add custom message actions * add borderradius and button builder * add ontap to messageinput custom send button * fix bottom sheet * add doc * use placeholder image * add doc * fix listtile density * remove subtitle from ChannelListHeaderTheme.copyWith * add InputDecoration.merge extension
This commit is contained in:
@@ -84,16 +84,17 @@ class StreamChatClient {
|
|||||||
Duration connectTimeout = const Duration(seconds: 6),
|
Duration connectTimeout = const Duration(seconds: 6),
|
||||||
Duration receiveTimeout = const Duration(seconds: 6),
|
Duration receiveTimeout = const Duration(seconds: 6),
|
||||||
Dio httpClient,
|
Dio httpClient,
|
||||||
// ignore: avoid_unused_constructor_parameters
|
|
||||||
RetryPolicy retryPolicy,
|
RetryPolicy retryPolicy,
|
||||||
this.attachmentFileUploader,
|
this.attachmentFileUploader,
|
||||||
}) {
|
}) {
|
||||||
_retryPolicy ??= RetryPolicy(
|
_retryPolicy = retryPolicy ??
|
||||||
retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
|
RetryPolicy(
|
||||||
Duration(seconds: 1 * attempt),
|
retryTimeout:
|
||||||
shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
|
(StreamChatClient client, int attempt, ApiError error) =>
|
||||||
attempt < 5,
|
Duration(seconds: 1 * attempt),
|
||||||
);
|
shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
|
||||||
|
attempt < 5,
|
||||||
|
);
|
||||||
|
|
||||||
attachmentFileUploader ??= StreamAttachmentFileUploader(this);
|
attachmentFileUploader ??= StreamAttachmentFileUploader(this);
|
||||||
|
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
return getFileTypeImage(attachment.extraData['other']);
|
return getFileTypeImage(attachment.extraData['other']);
|
||||||
},
|
},
|
||||||
placeholder: (_, __) {
|
placeholder: (_, __) {
|
||||||
return Container(
|
return Image.asset(
|
||||||
width: size?.width,
|
'images/placeholder.png',
|
||||||
height: size?.height,
|
fit: BoxFit.cover,
|
||||||
child: Image.memory(kTransparentImage),
|
package: 'stream_chat_flutter',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -348,10 +348,10 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
height: size?.height,
|
height: size?.height,
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
placeholder: (_, __) {
|
placeholder: (_, __) {
|
||||||
return Container(
|
return Image.asset(
|
||||||
width: size?.width,
|
'images/placeholder.png',
|
||||||
height: size?.height,
|
fit: BoxFit.cover,
|
||||||
child: Image.memory(kTransparentImage),
|
package: 'stream_chat_flutter',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
height: size?.height,
|
height: size?.height,
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
placeholder: (_, __) {
|
placeholder: (_, __) {
|
||||||
return Container(
|
return Image.asset(
|
||||||
width: size?.width,
|
'images/placeholder.png',
|
||||||
height: size?.height,
|
fit: BoxFit.cover,
|
||||||
child: Image.memory(kTransparentImage),
|
package: 'stream_chat_flutter',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
children: [
|
children: [
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
user: members[index].user,
|
user: members[index].user,
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints.tightFor(
|
||||||
maxHeight: 64.0,
|
height: 64.0,
|
||||||
maxWidth: 64.0,
|
width: 64.0,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(32.0),
|
borderRadius: BorderRadius.circular(32.0),
|
||||||
onlineIndicatorConstraints:
|
onlineIndicatorConstraints:
|
||||||
@@ -238,8 +238,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
|||||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
var channel = StreamChannel.of(context).channel;
|
|
||||||
if (res == true) {
|
if (res == true) {
|
||||||
|
final channel = StreamChannel.of(context).channel;
|
||||||
await channel.removeMembers([StreamChat.of(context).user.id]);
|
await channel.removeMembers([StreamChat.of(context).user.id]);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,19 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
final bool showConnectionStateTile;
|
final bool showConnectionStateTile;
|
||||||
|
|
||||||
|
/// Title widget
|
||||||
|
final Widget title;
|
||||||
|
|
||||||
|
/// Subtitle widget
|
||||||
|
final Widget subtitle;
|
||||||
|
|
||||||
|
/// Leading widget
|
||||||
|
final Widget leading;
|
||||||
|
|
||||||
|
/// AppBar actions
|
||||||
|
/// By default it shows the [ChannelImage]
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
/// Creates a channel header
|
/// Creates a channel header
|
||||||
ChannelHeader({
|
ChannelHeader({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -80,6 +93,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.showTypingIndicator = true,
|
this.showTypingIndicator = true,
|
||||||
this.onImageTap,
|
this.onImageTap,
|
||||||
this.showConnectionStateTile = false,
|
this.showConnectionStateTile = false,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.actions,
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -87,6 +104,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
|
final leadingWidget = leading ??
|
||||||
|
(showBackButton
|
||||||
|
? StreamBackButton(
|
||||||
|
onPressed: onBackPressed,
|
||||||
|
showUnreads: true,
|
||||||
|
)
|
||||||
|
: SizedBox());
|
||||||
|
|
||||||
return ConnectionStatusBuilder(
|
return ConnectionStatusBuilder(
|
||||||
statusBuilder: (context, status) {
|
statusBuilder: (context, status) {
|
||||||
var statusString = '';
|
var statusString = '';
|
||||||
@@ -111,26 +136,32 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
child: AppBar(
|
child: AppBar(
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
leading: showBackButton
|
leading: leadingWidget,
|
||||||
? StreamBackButton(
|
|
||||||
onPressed: onBackPressed,
|
|
||||||
showUnreads: true,
|
|
||||||
)
|
|
||||||
: SizedBox(),
|
|
||||||
backgroundColor: StreamChatTheme.of(context)
|
backgroundColor: StreamChatTheme.of(context)
|
||||||
.channelTheme
|
.channelTheme
|
||||||
.channelHeaderTheme
|
.channelHeaderTheme
|
||||||
.color,
|
.color,
|
||||||
actions: <Widget>[
|
actions: actions ??
|
||||||
Padding(
|
<Widget>[
|
||||||
padding: const EdgeInsets.only(right: 10.0),
|
Padding(
|
||||||
child: Center(
|
padding: const EdgeInsets.only(right: 10.0),
|
||||||
child: ChannelImage(
|
child: Center(
|
||||||
onTap: onImageTap,
|
child: ChannelImage(
|
||||||
|
borderRadius: StreamChatTheme.of(context)
|
||||||
|
.channelTheme
|
||||||
|
.channelHeaderTheme
|
||||||
|
.avatarTheme
|
||||||
|
.borderRadius,
|
||||||
|
constraints: StreamChatTheme.of(context)
|
||||||
|
.channelTheme
|
||||||
|
.channelHeaderTheme
|
||||||
|
.avatarTheme
|
||||||
|
.constraints,
|
||||||
|
onTap: onImageTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
|
||||||
],
|
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: InkWell(
|
title: InkWell(
|
||||||
onTap: onTitleTap,
|
onTap: onTitleTap,
|
||||||
@@ -141,20 +172,23 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ChannelName(
|
title ??
|
||||||
textStyle: StreamChatTheme.of(context)
|
ChannelName(
|
||||||
.channelTheme
|
textStyle: StreamChatTheme.of(context)
|
||||||
.channelHeaderTheme
|
.channelTheme
|
||||||
.title,
|
.channelHeaderTheme
|
||||||
),
|
.title,
|
||||||
|
),
|
||||||
SizedBox(height: 2),
|
SizedBox(height: 2),
|
||||||
ChannelInfo(
|
subtitle ??
|
||||||
showTypingIndicator: showTypingIndicator,
|
ChannelInfo(
|
||||||
channel: channel,
|
showTypingIndicator: showTypingIndicator,
|
||||||
textStyle: StreamChatTheme.of(context)
|
channel: channel,
|
||||||
.channelPreviewTheme
|
textStyle: StreamChatTheme.of(context)
|
||||||
.subtitle,
|
.channelTheme
|
||||||
),
|
.channelHeaderTheme
|
||||||
|
.subtitle,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
.channelTheme
|
.channelTheme
|
||||||
.channelHeaderTheme
|
.channelHeaderTheme
|
||||||
.lastMessageAt,
|
.subtitle,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final otherMember = members.firstWhere(
|
final otherMember = members.firstWhere(
|
||||||
@@ -69,10 +69,7 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
if (otherMember.user.online) {
|
if (otherMember.user.online) {
|
||||||
alternativeWidget = Text(
|
alternativeWidget = Text(
|
||||||
'Online',
|
'Online',
|
||||||
style: StreamChatTheme.of(context)
|
style: textStyle,
|
||||||
.channelTheme
|
|
||||||
.channelHeaderTheme
|
|
||||||
.lastMessageAt,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
alternativeWidget = Text(
|
alternativeWidget = Text(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ typedef _TitleBuilder = Widget Function(
|
|||||||
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status.
|
/// 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.
|
/// 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 [ChannelTheme.channelHeaderTheme] 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
|
||||||
@@ -57,6 +57,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.onNewChatButtonTap,
|
this.onNewChatButtonTap,
|
||||||
this.showConnectionStateTile = false,
|
this.showConnectionStateTile = false,
|
||||||
this.preNavigationCallback,
|
this.preNavigationCallback,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.actions,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
|
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
|
||||||
@@ -76,6 +79,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
final VoidCallback preNavigationCallback;
|
final VoidCallback preNavigationCallback;
|
||||||
|
|
||||||
|
/// Subtitle widget
|
||||||
|
final Widget subtitle;
|
||||||
|
|
||||||
|
/// Leading widget
|
||||||
|
/// By default it shows the logged in user avatar
|
||||||
|
final Widget leading;
|
||||||
|
|
||||||
|
/// AppBar actions
|
||||||
|
/// By default it shows the new chat button
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final _client = client ?? StreamChat.of(context).client;
|
final _client = client ?? StreamChat.of(context).client;
|
||||||
@@ -104,76 +118,85 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
child: AppBar(
|
child: AppBar(
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
backgroundColor: StreamChatTheme.of(context)
|
backgroundColor:
|
||||||
.channelTheme
|
StreamChatTheme.of(context).channelListHeaderTheme.color,
|
||||||
.channelHeaderTheme
|
|
||||||
.color,
|
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leading: Center(
|
leading: leading ??
|
||||||
child: UserAvatar(
|
Center(
|
||||||
user: user,
|
child: UserAvatar(
|
||||||
showOnlineStatus: false,
|
user: user,
|
||||||
onTap: onUserAvatarTap ??
|
showOnlineStatus: false,
|
||||||
(_) {
|
onTap: onUserAvatarTap ??
|
||||||
if (preNavigationCallback != null) {
|
(_) {
|
||||||
preNavigationCallback();
|
if (preNavigationCallback != null) {
|
||||||
}
|
preNavigationCallback();
|
||||||
Scaffold.of(context).openDrawer();
|
}
|
||||||
},
|
Scaffold.of(context).openDrawer();
|
||||||
borderRadius: BorderRadius.circular(20),
|
},
|
||||||
constraints: BoxConstraints.tightFor(
|
borderRadius: StreamChatTheme.of(context)
|
||||||
height: 40,
|
.channelListHeaderTheme
|
||||||
width: 40,
|
.avatarTheme
|
||||||
),
|
.borderRadius,
|
||||||
),
|
constraints: StreamChatTheme.of(context)
|
||||||
),
|
.channelListHeaderTheme
|
||||||
actions: [
|
.avatarTheme
|
||||||
StreamNeumorphicButton(
|
.constraints,
|
||||||
child: IconButton(
|
|
||||||
icon: ConnectionStatusBuilder(
|
|
||||||
statusBuilder: (context, status) {
|
|
||||||
var color;
|
|
||||||
switch (status) {
|
|
||||||
case ConnectionStatus.connected:
|
|
||||||
color =
|
|
||||||
StreamChatTheme.of(context).colorTheme.accentBlue;
|
|
||||||
break;
|
|
||||||
case ConnectionStatus.connecting:
|
|
||||||
color = Colors.grey;
|
|
||||||
break;
|
|
||||||
case ConnectionStatus.disconnected:
|
|
||||||
color = Colors.grey;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return SvgPicture.asset(
|
|
||||||
'svgs/icon_pen_write.svg',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
width: 24.0,
|
|
||||||
height: 24.0,
|
|
||||||
color: color,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
onPressed: onNewChatButtonTap,
|
|
||||||
),
|
),
|
||||||
)
|
actions: actions ??
|
||||||
],
|
[
|
||||||
title: Builder(
|
StreamNeumorphicButton(
|
||||||
builder: (context) {
|
child: IconButton(
|
||||||
if (titleBuilder != null) {
|
icon: ConnectionStatusBuilder(
|
||||||
return titleBuilder(context, status, _client);
|
statusBuilder: (context, status) {
|
||||||
}
|
var color;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
return _buildConnectedTitleState(context);
|
color = StreamChatTheme.of(context)
|
||||||
case ConnectionStatus.connecting:
|
.colorTheme
|
||||||
return _buildConnectingTitleState(context);
|
.accentBlue;
|
||||||
case ConnectionStatus.disconnected:
|
break;
|
||||||
return _buildDisconnectedTitleState(context, _client);
|
case ConnectionStatus.connecting:
|
||||||
default:
|
color = Colors.grey;
|
||||||
return Offstage();
|
break;
|
||||||
}
|
case ConnectionStatus.disconnected:
|
||||||
},
|
color = Colors.grey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return SvgPicture.asset(
|
||||||
|
'svgs/icon_pen_write.svg',
|
||||||
|
package: 'stream_chat_flutter',
|
||||||
|
width: 24.0,
|
||||||
|
height: 24.0,
|
||||||
|
color: color,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onPressed: onNewChatButtonTap,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
title: Column(
|
||||||
|
children: [
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (titleBuilder != null) {
|
||||||
|
return titleBuilder(context, status, _client);
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case ConnectionStatus.connected:
|
||||||
|
return _buildConnectedTitleState(context);
|
||||||
|
case ConnectionStatus.connecting:
|
||||||
|
return _buildConnectingTitleState(context);
|
||||||
|
case ConnectionStatus.disconnected:
|
||||||
|
return _buildDisconnectedTitleState(context, _client);
|
||||||
|
default:
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
subtitle ?? Offstage(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -202,14 +225,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
'Searching for Network',
|
'Searching for Network',
|
||||||
style: StreamChatTheme.of(context)
|
style:
|
||||||
.channelTheme
|
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
|
||||||
.channelHeaderTheme
|
fontSize: 16,
|
||||||
.title
|
fontWeight: FontWeight.bold,
|
||||||
.copyWith(
|
),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -222,14 +242,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Offline...',
|
'Offline...',
|
||||||
style: StreamChatTheme.of(context)
|
style:
|
||||||
.channelTheme
|
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
|
||||||
.channelHeaderTheme
|
fontSize: 16,
|
||||||
.title
|
fontWeight: FontWeight.bold,
|
||||||
.copyWith(
|
),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -239,8 +256,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Try Again',
|
'Try Again',
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
.channelTheme
|
.channelListHeaderTheme
|
||||||
.channelHeaderTheme
|
|
||||||
.title
|
.title
|
||||||
.copyWith(
|
.copyWith(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -32,16 +32,37 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
/// The function called when the image is tapped
|
/// The function called when the image is tapped
|
||||||
final VoidCallback onImageTap;
|
final VoidCallback onImageTap;
|
||||||
|
|
||||||
|
/// Widget rendering the title
|
||||||
|
final Widget title;
|
||||||
|
|
||||||
|
/// Widget rendering the subtitle
|
||||||
|
final Widget subtitle;
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
final Widget trailing;
|
||||||
|
|
||||||
|
/// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget
|
||||||
|
final Widget sendingIndicator;
|
||||||
|
|
||||||
ChannelPreview({
|
ChannelPreview({
|
||||||
@required this.channel,
|
@required this.channel,
|
||||||
Key key,
|
Key key,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.onImageTap,
|
this.onImageTap,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.sendingIndicator,
|
||||||
|
this.trailing,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
||||||
return StreamBuilder<bool>(
|
return StreamBuilder<bool>(
|
||||||
stream: channel.isMutedStream,
|
stream: channel.isMutedStream,
|
||||||
initialData: channel.isMuted,
|
initialData: channel.isMuted,
|
||||||
@@ -49,6 +70,7 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: snapshot.data ? 0.5 : 1,
|
opacity: snapshot.data ? 0.5 : 1,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
),
|
),
|
||||||
@@ -62,17 +84,18 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
onLongPress(channel);
|
onLongPress(channel);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leading: ChannelImage(
|
leading: leading ??
|
||||||
onTap: onImageTap,
|
ChannelImage(
|
||||||
),
|
onTap: onImageTap,
|
||||||
|
),
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ChannelName(
|
child: title ??
|
||||||
textStyle:
|
ChannelName(
|
||||||
StreamChatTheme.of(context).channelPreviewTheme.title,
|
textStyle: channelPreviewTheme.title,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
StreamBuilder<List<Member>>(
|
StreamBuilder<List<Member>>(
|
||||||
stream: channel.state.membersStream,
|
stream: channel.state.membersStream,
|
||||||
@@ -93,37 +116,36 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(child: _buildSubtitle(context)),
|
Flexible(child: subtitle ?? _buildSubtitle(context)),
|
||||||
Builder(
|
sendingIndicator ??
|
||||||
builder: (context) {
|
Builder(
|
||||||
final lastMessage = channel.state.messages.lastWhere(
|
builder: (context) {
|
||||||
(m) => !m.isDeleted && m.shadowed != true,
|
final lastMessage = channel.state.messages.lastWhere(
|
||||||
orElse: () => null,
|
(m) => !m.isDeleted && m.shadowed != true,
|
||||||
);
|
orElse: () => null,
|
||||||
if (lastMessage?.user?.id ==
|
);
|
||||||
StreamChat.of(context).user.id) {
|
if (lastMessage?.user?.id ==
|
||||||
return Padding(
|
StreamChat.of(context).user.id) {
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
return Padding(
|
||||||
child: SendingIndicator(
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
message: lastMessage,
|
child: SendingIndicator(
|
||||||
size: StreamChatTheme.of(context)
|
message: lastMessage,
|
||||||
.channelPreviewTheme
|
size: channelPreviewTheme.indicatorIconSize,
|
||||||
.indicatorIconSize,
|
isMessageRead: channel.state.read
|
||||||
isMessageRead: channel.state.read
|
?.where((element) =>
|
||||||
?.where((element) =>
|
element.user.id !=
|
||||||
element.user.id !=
|
channel.client.state.user.id)
|
||||||
channel.client.state.user.id)
|
?.where((element) => element.lastRead
|
||||||
?.where((element) => element.lastRead
|
.isAfter(lastMessage.createdAt))
|
||||||
.isAfter(lastMessage.createdAt))
|
?.isNotEmpty ==
|
||||||
?.isNotEmpty ==
|
true,
|
||||||
true,
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
return SizedBox();
|
||||||
return SizedBox();
|
},
|
||||||
},
|
),
|
||||||
),
|
trailing ?? _buildDate(context),
|
||||||
_buildDate(context),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -176,15 +198,7 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
' Channel is muted',
|
' Channel is muted',
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||||
.channelPreviewTheme
|
|
||||||
.subtitle
|
|
||||||
.copyWith(
|
|
||||||
color: StreamChatTheme.of(context)
|
|
||||||
.channelPreviewTheme
|
|
||||||
.subtitle
|
|
||||||
.color,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -192,10 +206,7 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
return TypingIndicator(
|
return TypingIndicator(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
alternativeWidget: _buildLastMessage(context),
|
alternativeWidget: _buildLastMessage(context),
|
||||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||||
color:
|
|
||||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:characters/characters.dart';
|
import 'package:characters/characters.dart';
|
||||||
import 'package:emojis/emoji.dart';
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
final _emojis = Emoji.all();
|
final _emojis = Emoji.all();
|
||||||
@@ -44,3 +45,55 @@ extension PlatformFileX on PlatformFile {
|
|||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension InputDecorationX on InputDecoration {
|
||||||
|
InputDecoration merge(InputDecoration other) {
|
||||||
|
if (other == null) return this;
|
||||||
|
return copyWith(
|
||||||
|
icon: other?.icon,
|
||||||
|
labelText: other?.labelText,
|
||||||
|
labelStyle: labelStyle?.merge(other.labelStyle) ?? other.labelStyle,
|
||||||
|
helperText: other?.helperText,
|
||||||
|
helperStyle: helperStyle?.merge(other.helperStyle) ?? other.helperStyle,
|
||||||
|
helperMaxLines: other?.helperMaxLines,
|
||||||
|
hintText: other?.hintText,
|
||||||
|
hintStyle: hintStyle?.merge(other.hintStyle) ?? other.hintStyle,
|
||||||
|
hintTextDirection: other?.hintTextDirection,
|
||||||
|
hintMaxLines: other?.hintMaxLines,
|
||||||
|
errorText: other?.errorText,
|
||||||
|
errorStyle: errorStyle?.merge(other.errorStyle) ?? other.errorStyle,
|
||||||
|
errorMaxLines: other?.errorMaxLines,
|
||||||
|
floatingLabelBehavior: other?.floatingLabelBehavior,
|
||||||
|
isCollapsed: other?.isCollapsed,
|
||||||
|
isDense: other?.isDense,
|
||||||
|
contentPadding: other?.contentPadding,
|
||||||
|
prefixIcon: other?.prefixIcon,
|
||||||
|
prefix: other?.prefix,
|
||||||
|
prefixText: other?.prefixText,
|
||||||
|
prefixIconConstraints: other?.prefixIconConstraints,
|
||||||
|
prefixStyle: prefixStyle?.merge(other.prefixStyle) ?? other.prefixStyle,
|
||||||
|
suffixIcon: other?.suffixIcon,
|
||||||
|
suffix: other?.suffix,
|
||||||
|
suffixText: other?.suffixText,
|
||||||
|
suffixStyle: suffixStyle?.merge(other.suffixStyle) ?? other.suffixStyle,
|
||||||
|
suffixIconConstraints: other?.suffixIconConstraints,
|
||||||
|
counter: other?.counter,
|
||||||
|
counterText: other?.counterText,
|
||||||
|
counterStyle:
|
||||||
|
counterStyle?.merge(other.counterStyle) ?? other.counterStyle,
|
||||||
|
filled: other?.filled,
|
||||||
|
fillColor: other?.fillColor,
|
||||||
|
focusColor: other?.focusColor,
|
||||||
|
hoverColor: other?.hoverColor,
|
||||||
|
errorBorder: other?.errorBorder,
|
||||||
|
focusedBorder: other?.focusedBorder,
|
||||||
|
focusedErrorBorder: other?.focusedErrorBorder,
|
||||||
|
disabledBorder: other?.disabledBorder,
|
||||||
|
enabledBorder: other?.enabledBorder,
|
||||||
|
border: other?.border,
|
||||||
|
enabled: other?.enabled,
|
||||||
|
semanticCounterText: other?.semanticCounterText,
|
||||||
|
alignLabelWithHint: other?.alignLabelWithHint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// Class describing a message action
|
||||||
|
class MessageAction {
|
||||||
|
/// leading widget
|
||||||
|
final Widget leading;
|
||||||
|
|
||||||
|
/// title widget
|
||||||
|
final Widget title;
|
||||||
|
|
||||||
|
/// callback called on tap
|
||||||
|
final OnMessageTap onTap;
|
||||||
|
|
||||||
|
/// returns a new instance of a [MessageAction]
|
||||||
|
MessageAction({
|
||||||
|
this.leading,
|
||||||
|
this.title,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/message_action.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
@@ -34,6 +35,9 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
final ShapeBorder attachmentShape;
|
final ShapeBorder attachmentShape;
|
||||||
final DisplayWidget showUserAvatar;
|
final DisplayWidget showUserAvatar;
|
||||||
|
|
||||||
|
/// List of custom actions
|
||||||
|
final List<MessageAction> customActions;
|
||||||
|
|
||||||
const MessageActionsModal({
|
const MessageActionsModal({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.message,
|
@required this.message,
|
||||||
@@ -53,6 +57,7 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.messageShape,
|
this.messageShape,
|
||||||
this.attachmentShape,
|
this.attachmentShape,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
|
this.customActions = const [],
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -223,6 +228,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
_buildFlagButton(context),
|
_buildFlagButton(context),
|
||||||
if (widget.showDeleteMessage)
|
if (widget.showDeleteMessage)
|
||||||
_buildDeleteButton(context),
|
_buildDeleteButton(context),
|
||||||
|
...widget.customActions.map((action) {
|
||||||
|
return _buildCustomAction(
|
||||||
|
context,
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
})
|
||||||
].insertBetween(
|
].insertBetween(
|
||||||
Container(
|
Container(
|
||||||
height: 1,
|
height: 1,
|
||||||
@@ -248,6 +259,27 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InkWell _buildCustomAction(
|
||||||
|
BuildContext context,
|
||||||
|
MessageAction messageAction,
|
||||||
|
) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
messageAction.onTap?.call(widget.message);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
messageAction.leading ?? Offstage(),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
messageAction.title ?? Offstage(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showFlagDialog() async {
|
void _showFlagDialog() async {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
|
|
||||||
@@ -541,21 +573,16 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
widget.editMessageInputBuilder != null
|
||||||
padding: EdgeInsets.only(
|
? widget.editMessageInputBuilder(context, widget.message)
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
: MessageInput(
|
||||||
),
|
editMessage: widget.message,
|
||||||
child: widget.editMessageInputBuilder != null
|
preMessageSending: (m) {
|
||||||
? widget.editMessageInputBuilder(context, widget.message)
|
FocusScope.of(context).unfocus();
|
||||||
: MessageInput(
|
Navigator.pop(context);
|
||||||
editMessage: widget.message,
|
return m;
|
||||||
preMessageSending: (m) {
|
},
|
||||||
FocusScope.of(context).unfocus();
|
),
|
||||||
Navigator.pop(context);
|
|
||||||
return m;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ enum DefaultAttachmentTypes {
|
|||||||
file,
|
file,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Available locations for the sendMessage button relative to the textField
|
||||||
|
enum SendButtonLocation {
|
||||||
|
/// inside the textField
|
||||||
|
inside,
|
||||||
|
|
||||||
|
/// outside the textField
|
||||||
|
outside,
|
||||||
|
}
|
||||||
|
|
||||||
const _kMinMediaPickerSize = 360.0;
|
const _kMinMediaPickerSize = 360.0;
|
||||||
|
|
||||||
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||||
@@ -107,6 +116,11 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.quotedMessage,
|
this.quotedMessage,
|
||||||
this.onQuotedMessageCleared,
|
this.onQuotedMessageCleared,
|
||||||
|
this.sendButtonLocation = SendButtonLocation.outside,
|
||||||
|
this.autofocus = false,
|
||||||
|
this.hideSendAsDm = false,
|
||||||
|
this.idleSendButton,
|
||||||
|
this.activeSendButton,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to edit
|
/// Message to edit
|
||||||
@@ -134,6 +148,9 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// If true the attachments button will not be displayed
|
/// If true the attachments button will not be displayed
|
||||||
final bool disableAttachments;
|
final bool disableAttachments;
|
||||||
|
|
||||||
|
/// Hide send as dm checkbox
|
||||||
|
final bool hideSendAsDm;
|
||||||
|
|
||||||
/// The text controller of the TextField
|
/// The text controller of the TextField
|
||||||
final TextEditingController textEditingController;
|
final TextEditingController textEditingController;
|
||||||
|
|
||||||
@@ -155,6 +172,18 @@ class MessageInput extends StatefulWidget {
|
|||||||
///
|
///
|
||||||
final VoidCallback onQuotedMessageCleared;
|
final VoidCallback onQuotedMessageCleared;
|
||||||
|
|
||||||
|
/// The location of the send button
|
||||||
|
final SendButtonLocation sendButtonLocation;
|
||||||
|
|
||||||
|
/// Autofocus property passed to the TextField
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// Send button widget in an idle state
|
||||||
|
final Widget idleSendButton;
|
||||||
|
|
||||||
|
/// Send button widget in an active state
|
||||||
|
final Widget activeSendButton;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -281,7 +310,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: _buildTextField(context),
|
child: _buildTextField(context),
|
||||||
),
|
),
|
||||||
if (widget.parentMessage != null)
|
if (widget.parentMessage != null && !widget.hideSendAsDm)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: _buildDmCheckbox(),
|
child: _buildDmCheckbox(),
|
||||||
@@ -310,9 +339,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (widget.actionsLocation == ActionsLocation.left)
|
if (widget.actionsLocation == ActionsLocation.left)
|
||||||
...widget.actions ?? [],
|
...widget.actions ?? [],
|
||||||
_buildTextInput(context),
|
_buildTextInput(context),
|
||||||
_animateSendButton(context),
|
|
||||||
if (widget.actionsLocation == ActionsLocation.right)
|
if (widget.actionsLocation == ActionsLocation.right)
|
||||||
...widget.actions ?? [],
|
...widget.actions ?? [],
|
||||||
|
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||||
|
_animateSendButton(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -384,14 +414,20 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _animateSendButton(BuildContext context) {
|
Widget _animateSendButton(BuildContext context) {
|
||||||
|
final sendButton = widget.activeSendButton != null
|
||||||
|
? InkWell(
|
||||||
|
child: widget.activeSendButton,
|
||||||
|
onTap: sendMessage,
|
||||||
|
)
|
||||||
|
: _buildSendButton(context);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: AnimatedCrossFade(
|
child: AnimatedCrossFade(
|
||||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||||
? CrossFadeState.showFirst
|
? CrossFadeState.showFirst
|
||||||
: CrossFadeState.showSecond,
|
: CrossFadeState.showSecond,
|
||||||
firstChild: _buildSendButton(context),
|
firstChild: sendButton,
|
||||||
secondChild: _buildIdleSendButton(context),
|
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
|
||||||
duration:
|
duration:
|
||||||
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
|
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@@ -447,101 +483,139 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20.0),
|
borderRadius: theme.messageInputTheme.borderRadius,
|
||||||
border: Border.all(color: theme.colorTheme.greyGainsboro),
|
gradient: _focusNode.hasFocus
|
||||||
|
? theme.messageInputTheme.activeBorderGradient
|
||||||
|
: theme.messageInputTheme.idleBorderGradient,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Padding(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: const EdgeInsets.all(1.5),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Container(
|
||||||
children: [
|
clipBehavior: Clip.antiAlias,
|
||||||
_buildReplyToMessage(),
|
decoration: BoxDecoration(
|
||||||
_buildAttachments(),
|
borderRadius: theme.messageInputTheme.borderRadius,
|
||||||
LimitedBox(
|
color: theme.messageInputTheme.inputBackground,
|
||||||
maxHeight: widget.maxHeight,
|
),
|
||||||
child: TextField(
|
child: Column(
|
||||||
key: Key('messageInputText'),
|
mainAxisSize: MainAxisSize.min,
|
||||||
enabled: _inputEnabled,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
minLines: null,
|
children: [
|
||||||
maxLines: null,
|
_buildReplyToMessage(),
|
||||||
onSubmitted: (_) => sendMessage(),
|
_buildAttachments(),
|
||||||
keyboardType: widget.keyboardType,
|
LimitedBox(
|
||||||
controller: textEditingController,
|
maxHeight: widget.maxHeight,
|
||||||
focusNode: _focusNode,
|
child: TextField(
|
||||||
style: theme.textTheme.body,
|
key: Key('messageInputText'),
|
||||||
autofocus: false,
|
enabled: _inputEnabled,
|
||||||
textAlignVertical: TextAlignVertical.center,
|
minLines: null,
|
||||||
decoration: InputDecoration(
|
maxLines: null,
|
||||||
isDense: true,
|
onSubmitted: (_) => sendMessage(),
|
||||||
hintText: _getHint(),
|
keyboardType: widget.keyboardType,
|
||||||
hintStyle: theme.textTheme.body.copyWith(
|
controller: textEditingController,
|
||||||
color: theme.colorTheme.grey,
|
focusNode: _focusNode,
|
||||||
|
style: theme.messageInputTheme.inputTextStyle,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
decoration: _getInputDecoration(),
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
)
|
||||||
borderSide: BorderSide(color: Colors.transparent)),
|
],
|
||||||
focusedBorder: OutlineInputBorder(
|
),
|
||||||
borderSide: BorderSide(color: Colors.transparent)),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.transparent)),
|
|
||||||
errorBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.transparent)),
|
|
||||||
disabledBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: Colors.transparent)),
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
|
|
||||||
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
|
|
||||||
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
|
|
||||||
prefixIcon: _commandEnabled
|
|
||||||
? Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: theme.colorTheme.accentBlue,
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.only(right: 4, left: 8),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
StreamSvgIcon.lightning(
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16.0,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
_chosenCommand?.name?.toUpperCase() ?? '',
|
|
||||||
style: StreamChatTheme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.footnoteBold
|
|
||||||
.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
suffixIcon: _commandEnabled
|
|
||||||
? IconButton(
|
|
||||||
icon: StreamSvgIcon.closeSmall(),
|
|
||||||
splashRadius: 24,
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
constraints: BoxConstraints.tightFor(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() => _commandEnabled = false);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InputDecoration _getInputDecoration() {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
final passedDecoration = theme.messageInputTheme.inputDecoration;
|
||||||
|
return InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
hintText: _getHint(),
|
||||||
|
hintStyle: theme.messageInputTheme.inputTextStyle.copyWith(
|
||||||
|
color: theme.colorTheme.grey,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
|
||||||
|
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
|
||||||
|
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
|
||||||
|
prefixIcon: _commandEnabled
|
||||||
|
? Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
color: theme.colorTheme.accentBlue,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(right: 4, left: 8),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
StreamSvgIcon.lightning(
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16.0,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_chosenCommand?.name?.toUpperCase() ?? '',
|
||||||
|
style: StreamChatTheme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.footnoteBold
|
||||||
|
.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
suffixIcon: Row(
|
||||||
|
children: [
|
||||||
|
if (_commandEnabled)
|
||||||
|
IconButton(
|
||||||
|
icon: StreamSvgIcon.closeSmall(),
|
||||||
|
splashRadius: 24,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _commandEnabled = false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||||
|
_animateSendButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).merge(passedDecoration);
|
||||||
|
}
|
||||||
|
|
||||||
Timer _debounce;
|
Timer _debounce;
|
||||||
|
|
||||||
void _onChanged(BuildContext context, String s) {
|
void _onChanged(BuildContext context, String s) {
|
||||||
@@ -621,7 +695,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
.last
|
.last
|
||||||
.contains('@')) {
|
.contains('@')) {
|
||||||
_mentionsOverlay = _buildMentionsOverlayEntry();
|
_mentionsOverlay = _buildMentionsOverlayEntry();
|
||||||
Overlay.of(context).insert(_mentionsOverlay);
|
if (_mentionsOverlay != null) {
|
||||||
|
Overlay.of(context).insert(_mentionsOverlay);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +722,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_commandsOverlay = null;
|
_commandsOverlay = null;
|
||||||
} else {
|
} else {
|
||||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||||
Overlay.of(context).insert(_commandsOverlay);
|
if (_commandsOverlay != null) {
|
||||||
|
Overlay.of(context).insert(_commandsOverlay);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -661,6 +739,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
?.toList() ??
|
?.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
|
if (commands.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
RenderBox renderBox = context.findRenderObject();
|
RenderBox renderBox = context.findRenderObject();
|
||||||
final size = renderBox.size;
|
final size = renderBox.size;
|
||||||
|
|
||||||
@@ -833,6 +915,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@@ -1100,6 +1183,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
})?.toList() ??
|
})?.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
|
if (members.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
RenderBox renderBox = context.findRenderObject();
|
RenderBox renderBox = context.findRenderObject();
|
||||||
final size = renderBox.size;
|
final size = renderBox.size;
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
|||||||
typedef ThreadTapCallback = void Function(Message, Widget);
|
typedef ThreadTapCallback = void Function(Message, Widget);
|
||||||
|
|
||||||
typedef OnMessageSwiped = void Function(Message);
|
typedef OnMessageSwiped = void Function(Message);
|
||||||
|
typedef OnMessageTap = void Function(Message);
|
||||||
typedef ReplyTapCallback = void Function(Message);
|
typedef ReplyTapCallback = void Function(Message);
|
||||||
|
|
||||||
class MessageDetails {
|
class MessageDetails {
|
||||||
@@ -224,10 +225,10 @@ class MessageListView extends StatefulWidget {
|
|||||||
final Map<String, AttachmentBuilder> customAttachmentBuilders;
|
final Map<String, AttachmentBuilder> customAttachmentBuilders;
|
||||||
|
|
||||||
/// Called when any message is tapped except a system message (use [onSystemMessageTap] instead)
|
/// Called when any message is tapped except a system message (use [onSystemMessageTap] instead)
|
||||||
final void Function(Message) onMessageTap;
|
final OnMessageTap onMessageTap;
|
||||||
|
|
||||||
/// Called when system message is tapped
|
/// Called when system message is tapped
|
||||||
final void Function(Message) onSystemMessageTap;
|
final OnMessageTap onSystemMessageTap;
|
||||||
|
|
||||||
// Customize onTap on attachment
|
// Customize onTap on attachment
|
||||||
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
||||||
@@ -436,7 +437,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
.channelTheme
|
.channelTheme
|
||||||
.channelHeaderTheme
|
.channelHeaderTheme
|
||||||
.lastMessageAt,
|
.subtitle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/message_action.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 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_portal/flutter_portal.dart';
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/message_action.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
|
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
||||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||||
@@ -146,6 +147,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Function called when message is tapped
|
/// Function called when message is tapped
|
||||||
final void Function(Message) onMessageTap;
|
final void Function(Message) onMessageTap;
|
||||||
|
|
||||||
|
/// List of custom actions shown on message long tap
|
||||||
|
final List<MessageAction> customActions;
|
||||||
|
|
||||||
// Customize onTap on attachment
|
// Customize onTap on attachment
|
||||||
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
||||||
|
|
||||||
@@ -198,6 +202,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.attachmentPadding = EdgeInsets.zero,
|
this.attachmentPadding = EdgeInsets.zero,
|
||||||
this.allRead = false,
|
this.allRead = false,
|
||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
|
this.customActions = const [],
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachment) {
|
'image': (context, message, attachment) {
|
||||||
@@ -760,6 +765,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
!isFailedState &&
|
!isFailedState &&
|
||||||
widget.onThreadTap != null,
|
widget.onThreadTap != null,
|
||||||
showFlagButton: widget.showFlagButton,
|
showFlagButton: widget.showFlagButton,
|
||||||
|
customActions: widget.customActions,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -966,6 +972,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
user: widget.message.user,
|
user: widget.message.user,
|
||||||
onTap: widget.onUserAvatarTap,
|
onTap: widget.onUserAvatarTap,
|
||||||
constraints: widget.messageTheme.avatarTheme.constraints,
|
constraints: widget.messageTheme.avatarTheme.constraints,
|
||||||
|
borderRadius: widget.messageTheme.avatarTheme.borderRadius,
|
||||||
showOnlineStatus: false,
|
showOnlineStatus: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:stream_chat_flutter/src/channel_preview.dart';
|
|||||||
import 'package:stream_chat_flutter/src/message_input.dart';
|
import 'package:stream_chat_flutter/src/message_input.dart';
|
||||||
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/utils.dart';
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
|
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
|
||||||
@@ -45,12 +46,15 @@ class StreamChatThemeData {
|
|||||||
/// The text themes used in the widgets
|
/// The text themes used in the widgets
|
||||||
final TextTheme textTheme;
|
final TextTheme textTheme;
|
||||||
|
|
||||||
/// The text themes used in the widgets
|
/// The color themes used in the widgets
|
||||||
final ColorTheme colorTheme;
|
final ColorTheme colorTheme;
|
||||||
|
|
||||||
/// Theme of the [ChannelPreview]
|
/// Theme of the [ChannelPreview]
|
||||||
final ChannelPreviewTheme channelPreviewTheme;
|
final ChannelPreviewTheme channelPreviewTheme;
|
||||||
|
|
||||||
|
/// Theme of the [ChannelListHeader]
|
||||||
|
final ChannelListHeaderTheme channelListHeaderTheme;
|
||||||
|
|
||||||
/// Theme of the chat widgets dedicated to a channel
|
/// Theme of the chat widgets dedicated to a channel
|
||||||
final ChannelTheme channelTheme;
|
final ChannelTheme channelTheme;
|
||||||
|
|
||||||
@@ -60,7 +64,7 @@ class StreamChatThemeData {
|
|||||||
/// Theme of other users messages
|
/// Theme of other users messages
|
||||||
final MessageTheme otherMessageTheme;
|
final MessageTheme otherMessageTheme;
|
||||||
|
|
||||||
/// Theme of other users messages
|
/// Theme dedicated to the [MessageInput] widget
|
||||||
final MessageInputTheme messageInputTheme;
|
final MessageInputTheme messageInputTheme;
|
||||||
|
|
||||||
/// The widget that will be built when the channel image is unavailable
|
/// The widget that will be built when the channel image is unavailable
|
||||||
@@ -79,6 +83,7 @@ class StreamChatThemeData {
|
|||||||
const StreamChatThemeData({
|
const StreamChatThemeData({
|
||||||
this.textTheme,
|
this.textTheme,
|
||||||
this.colorTheme,
|
this.colorTheme,
|
||||||
|
this.channelListHeaderTheme,
|
||||||
this.channelPreviewTheme,
|
this.channelPreviewTheme,
|
||||||
this.channelTheme,
|
this.channelTheme,
|
||||||
this.otherMessageTheme,
|
this.otherMessageTheme,
|
||||||
@@ -98,9 +103,7 @@ class StreamChatThemeData {
|
|||||||
accentBlue: theme.accentColor,
|
accentBlue: theme.accentColor,
|
||||||
),
|
),
|
||||||
defaultTheme.textTheme,
|
defaultTheme.textTheme,
|
||||||
).copyWith(
|
);
|
||||||
// primaryIconTheme: theme.primaryIconTheme,
|
|
||||||
);
|
|
||||||
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
|
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,9 +119,12 @@ class StreamChatThemeData {
|
|||||||
Widget Function(BuildContext, Channel) defaultChannelImage,
|
Widget Function(BuildContext, Channel) defaultChannelImage,
|
||||||
Widget Function(BuildContext, User) defaultUserImage,
|
Widget Function(BuildContext, User) defaultUserImage,
|
||||||
IconThemeData primaryIconTheme,
|
IconThemeData primaryIconTheme,
|
||||||
|
ChannelListHeaderTheme channelListHeaderTheme,
|
||||||
List<ReactionIcon> reactionIcons,
|
List<ReactionIcon> reactionIcons,
|
||||||
}) =>
|
}) =>
|
||||||
StreamChatThemeData(
|
StreamChatThemeData(
|
||||||
|
channelListHeaderTheme:
|
||||||
|
channelListHeaderTheme ?? this.channelListHeaderTheme,
|
||||||
textTheme: textTheme ?? this.textTheme,
|
textTheme: textTheme ?? this.textTheme,
|
||||||
colorTheme: colorTheme ?? this.colorTheme,
|
colorTheme: colorTheme ?? this.colorTheme,
|
||||||
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
|
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
|
||||||
@@ -135,6 +141,9 @@ class StreamChatThemeData {
|
|||||||
StreamChatThemeData merge(StreamChatThemeData other) {
|
StreamChatThemeData merge(StreamChatThemeData other) {
|
||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
|
channelListHeaderTheme:
|
||||||
|
channelListHeaderTheme?.merge(other.channelListHeaderTheme) ??
|
||||||
|
other.channelListHeaderTheme,
|
||||||
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
|
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
|
||||||
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
|
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
|
||||||
primaryIconTheme: other.primaryIconTheme,
|
primaryIconTheme: other.primaryIconTheme,
|
||||||
@@ -189,6 +198,17 @@ class StreamChatThemeData {
|
|||||||
color: colorTheme.black.withOpacity(.5),
|
color: colorTheme.black.withOpacity(.5),
|
||||||
),
|
),
|
||||||
indicatorIconSize: 16.0),
|
indicatorIconSize: 16.0),
|
||||||
|
channelListHeaderTheme: ChannelListHeaderTheme(
|
||||||
|
avatarTheme: AvatarTheme(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
color: colorTheme.white,
|
||||||
|
title: textTheme.headlineBold,
|
||||||
|
),
|
||||||
channelTheme: ChannelTheme(
|
channelTheme: ChannelTheme(
|
||||||
channelHeaderTheme: ChannelHeaderTheme(
|
channelHeaderTheme: ChannelHeaderTheme(
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
@@ -200,9 +220,8 @@ class StreamChatThemeData {
|
|||||||
),
|
),
|
||||||
color: colorTheme.white,
|
color: colorTheme.white,
|
||||||
title: textTheme.headlineBold,
|
title: textTheme.headlineBold,
|
||||||
lastMessageAt: TextStyle(
|
subtitle: textTheme.footnote.copyWith(
|
||||||
fontSize: 11,
|
color: Color(0xff7A7A7A),
|
||||||
color: colorTheme.black.withOpacity(.5),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -249,12 +268,26 @@ class StreamChatThemeData {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
messageInputTheme: MessageInputTheme(
|
messageInputTheme: MessageInputTheme(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
sendAnimationDuration: Duration(milliseconds: 300),
|
sendAnimationDuration: Duration(milliseconds: 300),
|
||||||
actionButtonColor: colorTheme.accentBlue,
|
actionButtonColor: colorTheme.accentBlue,
|
||||||
actionButtonIdleColor: colorTheme.grey,
|
actionButtonIdleColor: colorTheme.grey,
|
||||||
sendButtonColor: colorTheme.accentBlue,
|
sendButtonColor: colorTheme.accentBlue,
|
||||||
sendButtonIdleColor: colorTheme.greyGainsboro,
|
sendButtonIdleColor: colorTheme.greyGainsboro,
|
||||||
inputBackground: colorTheme.white,
|
inputBackground: colorTheme.white,
|
||||||
|
inputTextStyle: textTheme.body,
|
||||||
|
idleBorderGradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
colorTheme.greyGainsboro,
|
||||||
|
colorTheme.greyGainsboro,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
activeBorderGradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
colorTheme.greyGainsboro,
|
||||||
|
colorTheme.greyGainsboro,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
reactionIcons: [
|
reactionIcons: [
|
||||||
ReactionIcon(
|
ReactionIcon(
|
||||||
@@ -809,26 +842,26 @@ class ChannelPreviewTheme {
|
|||||||
|
|
||||||
class ChannelHeaderTheme {
|
class ChannelHeaderTheme {
|
||||||
final TextStyle title;
|
final TextStyle title;
|
||||||
final TextStyle lastMessageAt;
|
final TextStyle subtitle;
|
||||||
final AvatarTheme avatarTheme;
|
final AvatarTheme avatarTheme;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
const ChannelHeaderTheme({
|
const ChannelHeaderTheme({
|
||||||
this.title,
|
this.title,
|
||||||
this.lastMessageAt,
|
this.subtitle,
|
||||||
this.avatarTheme,
|
this.avatarTheme,
|
||||||
this.color,
|
this.color,
|
||||||
});
|
});
|
||||||
|
|
||||||
ChannelHeaderTheme copyWith({
|
ChannelHeaderTheme copyWith({
|
||||||
TextStyle title,
|
TextStyle title,
|
||||||
TextStyle lastMessageAt,
|
TextStyle subtitle,
|
||||||
AvatarTheme avatarTheme,
|
AvatarTheme avatarTheme,
|
||||||
Color color,
|
Color color,
|
||||||
}) =>
|
}) =>
|
||||||
ChannelHeaderTheme(
|
ChannelHeaderTheme(
|
||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
subtitle: subtitle ?? this.subtitle,
|
||||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||||
color: color ?? this.color,
|
color: color ?? this.color,
|
||||||
);
|
);
|
||||||
@@ -837,8 +870,48 @@ class ChannelHeaderTheme {
|
|||||||
if (other == null) return this;
|
if (other == null) return this;
|
||||||
return copyWith(
|
return copyWith(
|
||||||
title: title?.merge(other.title) ?? other.title,
|
title: title?.merge(other.title) ?? other.title,
|
||||||
lastMessageAt:
|
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
|
||||||
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
|
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||||
|
color: other.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Theme dedicated to the [ChannelListHeader]
|
||||||
|
class ChannelListHeaderTheme {
|
||||||
|
/// Style of the title text
|
||||||
|
final TextStyle title;
|
||||||
|
|
||||||
|
/// Theme dedicated to the userAvatar
|
||||||
|
final AvatarTheme avatarTheme;
|
||||||
|
|
||||||
|
/// Background color of the appbar
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// Returns a new [ChannelListHeaderTheme]
|
||||||
|
const ChannelListHeaderTheme({
|
||||||
|
this.title,
|
||||||
|
this.avatarTheme,
|
||||||
|
this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Returns a new [ChannelListHeaderTheme] replacing some of its properties
|
||||||
|
ChannelListHeaderTheme copyWith({
|
||||||
|
TextStyle title,
|
||||||
|
AvatarTheme avatarTheme,
|
||||||
|
Color color,
|
||||||
|
}) =>
|
||||||
|
ChannelListHeaderTheme(
|
||||||
|
title: title ?? this.title,
|
||||||
|
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||||
|
color: color ?? this.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Merges [this] [ChannelListHeaderTheme] with the [other]
|
||||||
|
ChannelListHeaderTheme merge(ChannelListHeaderTheme other) {
|
||||||
|
if (other == null) return this;
|
||||||
|
return copyWith(
|
||||||
|
title: title?.merge(other.title) ?? other.title,
|
||||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||||
color: other.color,
|
color: other.color,
|
||||||
);
|
);
|
||||||
@@ -865,6 +938,21 @@ class MessageInputTheme {
|
|||||||
/// Background color of [MessageInput]
|
/// Background color of [MessageInput]
|
||||||
final Color inputBackground;
|
final Color inputBackground;
|
||||||
|
|
||||||
|
/// TextStyle of [MessageInput]
|
||||||
|
final TextStyle inputTextStyle;
|
||||||
|
|
||||||
|
/// InputDecoration of [MessageInput]
|
||||||
|
final InputDecoration inputDecoration;
|
||||||
|
|
||||||
|
/// Border gradient when the [MessageInput] is not focused
|
||||||
|
final Gradient idleBorderGradient;
|
||||||
|
|
||||||
|
/// Border gradient when the [MessageInput] is focused
|
||||||
|
final Gradient activeBorderGradient;
|
||||||
|
|
||||||
|
/// Border radius of [MessageInput]
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
|
||||||
/// Returns a new [MessageInputTheme]
|
/// Returns a new [MessageInputTheme]
|
||||||
const MessageInputTheme({
|
const MessageInputTheme({
|
||||||
this.sendAnimationDuration,
|
this.sendAnimationDuration,
|
||||||
@@ -873,6 +961,11 @@ class MessageInputTheme {
|
|||||||
this.actionButtonIdleColor,
|
this.actionButtonIdleColor,
|
||||||
this.sendButtonIdleColor,
|
this.sendButtonIdleColor,
|
||||||
this.inputBackground,
|
this.inputBackground,
|
||||||
|
this.inputTextStyle,
|
||||||
|
this.inputDecoration,
|
||||||
|
this.activeBorderGradient,
|
||||||
|
this.idleBorderGradient,
|
||||||
|
this.borderRadius,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Returns a new [MessageInputTheme] replacing some of its properties
|
/// Returns a new [MessageInputTheme] replacing some of its properties
|
||||||
@@ -883,6 +976,11 @@ class MessageInputTheme {
|
|||||||
Color sendButtonColor,
|
Color sendButtonColor,
|
||||||
Color actionButtonIdleColor,
|
Color actionButtonIdleColor,
|
||||||
Color sendButtonIdleColor,
|
Color sendButtonIdleColor,
|
||||||
|
TextStyle inputTextStyle,
|
||||||
|
InputDecoration inputDecoration,
|
||||||
|
Gradient activeBorderGradient,
|
||||||
|
Gradient idleBorderGradient,
|
||||||
|
BorderRadius borderRadius,
|
||||||
}) =>
|
}) =>
|
||||||
MessageInputTheme(
|
MessageInputTheme(
|
||||||
sendAnimationDuration:
|
sendAnimationDuration:
|
||||||
@@ -892,7 +990,12 @@ class MessageInputTheme {
|
|||||||
sendButtonColor: sendButtonColor ?? this.sendButtonColor,
|
sendButtonColor: sendButtonColor ?? this.sendButtonColor,
|
||||||
actionButtonIdleColor:
|
actionButtonIdleColor:
|
||||||
actionButtonIdleColor ?? this.actionButtonIdleColor,
|
actionButtonIdleColor ?? this.actionButtonIdleColor,
|
||||||
|
inputTextStyle: inputTextStyle ?? this.inputTextStyle,
|
||||||
sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor,
|
sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor,
|
||||||
|
inputDecoration: inputDecoration ?? this.inputDecoration,
|
||||||
|
activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient,
|
||||||
|
idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient,
|
||||||
|
borderRadius: borderRadius ?? this.borderRadius,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Merges [this] [MessageInputTheme] with the [other]
|
/// Merges [this] [MessageInputTheme] with the [other]
|
||||||
@@ -905,6 +1008,12 @@ class MessageInputTheme {
|
|||||||
actionButtonIdleColor: other.actionButtonIdleColor,
|
actionButtonIdleColor: other.actionButtonIdleColor,
|
||||||
sendButtonColor: other.sendButtonColor,
|
sendButtonColor: other.sendButtonColor,
|
||||||
sendButtonIdleColor: other.sendButtonIdleColor,
|
sendButtonIdleColor: other.sendButtonIdleColor,
|
||||||
|
inputTextStyle: other.inputTextStyle,
|
||||||
|
inputDecoration: inputDecoration?.merge(other.inputDecoration) ??
|
||||||
|
other.inputDecoration,
|
||||||
|
activeBorderGradient: other.activeBorderGradient,
|
||||||
|
idleBorderGradient: other.idleBorderGradient,
|
||||||
|
borderRadius: other.borderRadius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,15 +63,35 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// By default it calls [Navigator.pop]
|
/// By default it calls [Navigator.pop]
|
||||||
final VoidCallback onBackPressed;
|
final VoidCallback onBackPressed;
|
||||||
|
|
||||||
|
/// Callback to call when the title is tapped.
|
||||||
|
final VoidCallback onTitleTap;
|
||||||
|
|
||||||
/// The message parent of this thread
|
/// The message parent of this thread
|
||||||
final Message parent;
|
final Message parent;
|
||||||
|
|
||||||
|
/// Title widget
|
||||||
|
final Widget title;
|
||||||
|
|
||||||
|
/// Subtitle widget
|
||||||
|
final Widget subtitle;
|
||||||
|
|
||||||
|
/// Leading widget
|
||||||
|
final Widget leading;
|
||||||
|
|
||||||
|
/// AppBar actions
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
/// Instantiate a new ThreadHeader
|
/// Instantiate a new ThreadHeader
|
||||||
ThreadHeader({
|
ThreadHeader({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.parent,
|
@required this.parent,
|
||||||
this.showBackButton = true,
|
this.showBackButton = true,
|
||||||
this.onBackPressed,
|
this.onBackPressed,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.leading,
|
||||||
|
this.actions,
|
||||||
|
this.onTitleTap,
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -81,51 +101,61 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
leading: showBackButton
|
leading: leading ??
|
||||||
? StreamBackButton(
|
(showBackButton
|
||||||
cid: StreamChannel.of(context).channel.cid,
|
? StreamBackButton(
|
||||||
onPressed: onBackPressed,
|
cid: StreamChannel.of(context).channel.cid,
|
||||||
showUnreads: true,
|
onPressed: onBackPressed,
|
||||||
)
|
showUnreads: true,
|
||||||
: SizedBox(),
|
)
|
||||||
|
: SizedBox()),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Column(
|
actions: actions,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
title: InkWell(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
onTap: onTitleTap,
|
||||||
children: [
|
child: Container(
|
||||||
Text(
|
height: preferredSize.height,
|
||||||
'Thread Reply',
|
child: Column(
|
||||||
style: StreamChatTheme.of(context)
|
|
||||||
.channelTheme
|
|
||||||
.channelHeaderTheme
|
|
||||||
.title,
|
|
||||||
),
|
|
||||||
SizedBox(height: 2),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
title ??
|
||||||
'with ',
|
Text(
|
||||||
style: StreamChatTheme.of(context)
|
'Thread Reply',
|
||||||
.channelTheme
|
style: StreamChatTheme.of(context)
|
||||||
.channelHeaderTheme
|
.channelTheme
|
||||||
.lastMessageAt,
|
.channelHeaderTheme
|
||||||
),
|
.title,
|
||||||
Flexible(
|
),
|
||||||
child: ChannelName(
|
SizedBox(height: 2),
|
||||||
textStyle: StreamChatTheme.of(context)
|
subtitle ??
|
||||||
.channelTheme
|
Row(
|
||||||
.channelHeaderTheme
|
mainAxisSize: MainAxisSize.min,
|
||||||
.lastMessageAt,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
|
Text(
|
||||||
|
'with ',
|
||||||
|
style: StreamChatTheme.of(context)
|
||||||
|
.channelTheme
|
||||||
|
.channelHeaderTheme
|
||||||
|
.subtitle,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: ChannelName(
|
||||||
|
textStyle: StreamChatTheme.of(context)
|
||||||
|
.channelTheme
|
||||||
|
.channelHeaderTheme
|
||||||
|
.subtitle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,41 +14,43 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
return StreamBuilder<int>(
|
return IgnorePointer(
|
||||||
stream: cid != null
|
child: StreamBuilder<int>(
|
||||||
? client.state.channels[cid].state.unreadCountStream
|
stream: cid != null
|
||||||
: client.state.totalUnreadCountStream,
|
? client.state.channels[cid].state.unreadCountStream
|
||||||
initialData: cid != null
|
: client.state.totalUnreadCountStream,
|
||||||
? client.state.channels[cid].state.unreadCount
|
initialData: cid != null
|
||||||
: client.state.totalUnreadCount,
|
? client.state.channels[cid].state.unreadCount
|
||||||
builder: (context, snapshot) {
|
: client.state.totalUnreadCount,
|
||||||
if (!snapshot.hasData || snapshot.data == 0) {
|
builder: (context, snapshot) {
|
||||||
return SizedBox();
|
if (!snapshot.hasData || snapshot.data == 0) {
|
||||||
}
|
return SizedBox();
|
||||||
return Material(
|
}
|
||||||
borderRadius: BorderRadius.circular(8),
|
return Material(
|
||||||
color: StreamChatTheme.of(context)
|
borderRadius: BorderRadius.circular(8),
|
||||||
.channelPreviewTheme
|
color: StreamChatTheme.of(context)
|
||||||
.unreadCounterColor,
|
.channelPreviewTheme
|
||||||
child: Padding(
|
.unreadCounterColor,
|
||||||
padding: const EdgeInsets.only(
|
child: Padding(
|
||||||
left: 5.0,
|
padding: const EdgeInsets.only(
|
||||||
right: 5.0,
|
left: 5.0,
|
||||||
top: 2,
|
right: 5.0,
|
||||||
bottom: 1,
|
top: 2,
|
||||||
),
|
bottom: 1,
|
||||||
child: Center(
|
),
|
||||||
child: Text(
|
child: Center(
|
||||||
'${snapshot.data}',
|
child: Text(
|
||||||
style: TextStyle(
|
'${snapshot.data}',
|
||||||
fontSize: 11,
|
style: TextStyle(
|
||||||
color: Colors.white,
|
fontSize: 11,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,26 +39,29 @@ class UserAvatar extends StatelessWidget {
|
|||||||
user.extraData['image'] != '';
|
user.extraData['image'] != '';
|
||||||
final streamChatTheme = StreamChatTheme.of(context);
|
final streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
Widget avatar = ClipRRect(
|
Widget avatar = FittedBox(
|
||||||
clipBehavior: Clip.antiAlias,
|
fit: BoxFit.cover,
|
||||||
borderRadius: borderRadius ??
|
child: ClipRRect(
|
||||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Container(
|
borderRadius: borderRadius ??
|
||||||
constraints: constraints ??
|
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
|
||||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
constraints: constraints ??
|
||||||
color: streamChatTheme.colorTheme.accentBlue,
|
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: streamChatTheme.colorTheme.accentBlue,
|
||||||
|
),
|
||||||
|
child: hasImage
|
||||||
|
? CachedNetworkImage(
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
imageUrl: user.extraData['image'],
|
||||||
|
errorWidget: (_, __, ___) {
|
||||||
|
return streamChatTheme.defaultUserImage(context, user);
|
||||||
|
},
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: streamChatTheme.defaultUserImage(context, user),
|
||||||
),
|
),
|
||||||
child: hasImage
|
|
||||||
? CachedNetworkImage(
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
imageUrl: user.extraData['image'],
|
|
||||||
errorWidget: (_, __, ___) {
|
|
||||||
return streamChatTheme.defaultUserImage(context, user);
|
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: streamChatTheme.defaultUserImage(context, user),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,6 +94,7 @@ class UserAvatar extends StatelessWidget {
|
|||||||
alignment: onlineIndicatorAlignment,
|
alignment: onlineIndicatorAlignment,
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.circle,
|
type: MaterialType.circle,
|
||||||
|
color: streamChatTheme.colorTheme.white,
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.all(2.0),
|
margin: const EdgeInsets.all(2.0),
|
||||||
constraints: onlineIndicatorConstraints ??
|
constraints: onlineIndicatorConstraints ??
|
||||||
@@ -103,7 +107,6 @@ class UserAvatar extends StatelessWidget {
|
|||||||
color: streamChatTheme.colorTheme.accentGreen,
|
color: streamChatTheme.colorTheme.accentGreen,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
color: streamChatTheme.colorTheme.white,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export 'src/channel_name.dart';
|
|||||||
export 'src/channel_preview.dart';
|
export 'src/channel_preview.dart';
|
||||||
export 'src/date_divider.dart';
|
export 'src/date_divider.dart';
|
||||||
export 'src/deleted_message.dart';
|
export 'src/deleted_message.dart';
|
||||||
|
export 'src/message_action.dart';
|
||||||
export 'src/attachment/attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
export 'src/full_screen_media.dart';
|
export 'src/full_screen_media.dart';
|
||||||
export 'src/image_header.dart';
|
export 'src/image_header.dart';
|
||||||
|
|||||||
@@ -172,11 +172,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
_subscriptions.add(client
|
_subscriptions.add(client
|
||||||
.on(EventType.channelDeleted, EventType.notificationRemovedFromChannel)
|
.on(
|
||||||
|
EventType.channelDeleted,
|
||||||
|
EventType.notificationRemovedFromChannel,
|
||||||
|
)
|
||||||
.listen((e) {
|
.listen((e) {
|
||||||
final channel = e.channel;
|
final channel = e.channel;
|
||||||
_channelsController
|
_channelsController.add(List.from(
|
||||||
.add(List.from(channels..removeWhere((c) => c.cid == channel.cid)));
|
(channels ?? [])..removeWhere((c) => c.cid == channel.cid)));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class _MessageListCoreState extends State<MessageListCore> {
|
|||||||
return widget.errorWidgetBuilder(context, snapshot.error);
|
return widget.errorWidgetBuilder(context, snapshot.error);
|
||||||
} else {
|
} else {
|
||||||
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||||
if (messageList.isEmpty) {
|
if (messageList.isEmpty && !_isThreadConversation) {
|
||||||
if (_upToDate) {
|
if (_upToDate) {
|
||||||
return widget.emptyBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user