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:
Salvatore Giordano
2021-03-08 15:27:52 +01:00
committed by GitHub
parent db04ba021a
commit 5080203c1b
23 changed files with 815 additions and 411 deletions
+4 -3
View File
@@ -84,12 +84,13 @@ 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(
retryTimeout:
(StreamChatClient client, int attempt, ApiError error) =>
Duration(seconds: 1 * attempt), Duration(seconds: 1 * attempt),
shouldRetry: (StreamChatClient client, int attempt, ApiError error) => shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
attempt < 5, attempt < 5,
@@ -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,21 +136,27 @@ 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 ??
<Widget>[
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0), padding: const EdgeInsets.only(right: 10.0),
child: Center( child: Center(
child: ChannelImage( child: ChannelImage(
borderRadius: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.avatarTheme
.borderRadius,
constraints: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.avatarTheme
.constraints,
onTap: onImageTap, onTap: onImageTap,
), ),
), ),
@@ -141,6 +172,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
title ??
ChannelName( ChannelName(
textStyle: StreamChatTheme.of(context) textStyle: StreamChatTheme.of(context)
.channelTheme .channelTheme
@@ -148,11 +180,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
.title, .title,
), ),
SizedBox(height: 2), SizedBox(height: 2),
subtitle ??
ChannelInfo( ChannelInfo(
showTypingIndicator: showTypingIndicator, showTypingIndicator: showTypingIndicator,
channel: channel, channel: channel,
textStyle: StreamChatTheme.of(context) textStyle: StreamChatTheme.of(context)
.channelPreviewTheme .channelTheme
.channelHeaderTheme
.subtitle, .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,12 +118,11 @@ 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 ??
Center(
child: UserAvatar( child: UserAvatar(
user: user, user: user,
showOnlineStatus: false, showOnlineStatus: false,
@@ -120,14 +133,18 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
} }
Scaffold.of(context).openDrawer(); Scaffold.of(context).openDrawer();
}, },
borderRadius: BorderRadius.circular(20), borderRadius: StreamChatTheme.of(context)
constraints: BoxConstraints.tightFor( .channelListHeaderTheme
height: 40, .avatarTheme
width: 40, .borderRadius,
constraints: StreamChatTheme.of(context)
.channelListHeaderTheme
.avatarTheme
.constraints,
), ),
), ),
), actions: actions ??
actions: [ [
StreamNeumorphicButton( StreamNeumorphicButton(
child: IconButton( child: IconButton(
icon: ConnectionStatusBuilder( icon: ConnectionStatusBuilder(
@@ -135,8 +152,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
var color; var color;
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
color = color = StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.accentBlue; .colorTheme
.accentBlue;
break; break;
case ConnectionStatus.connecting: case ConnectionStatus.connecting:
color = Colors.grey; color = Colors.grey;
@@ -158,7 +176,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
) )
], ],
title: Builder( title: Column(
children: [
Builder(
builder: (context) { builder: (context) {
if (titleBuilder != null) { if (titleBuilder != null) {
return titleBuilder(context, status, _client); return titleBuilder(context, status, _client);
@@ -175,6 +195,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
} }
}, },
), ),
subtitle ?? Offstage(),
],
),
), ),
); );
}, },
@@ -202,11 +225,8 @@ 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
.title
.copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -222,11 +242,8 @@ 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
.title
.copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -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,16 +84,17 @@ class ChannelPreview extends StatelessWidget {
onLongPress(channel); onLongPress(channel);
} }
}, },
leading: ChannelImage( leading: leading ??
ChannelImage(
onTap: onImageTap, 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>>(
@@ -93,7 +116,8 @@ 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)),
sendingIndicator ??
Builder( Builder(
builder: (context) { builder: (context) {
final lastMessage = channel.state.messages.lastWhere( final lastMessage = channel.state.messages.lastWhere(
@@ -106,9 +130,7 @@ class ChannelPreview extends StatelessWidget {
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator( child: SendingIndicator(
message: lastMessage, message: lastMessage,
size: StreamChatTheme.of(context) size: channelPreviewTheme.indicatorIconSize,
.channelPreviewTheme
.indicatorIconSize,
isMessageRead: channel.state.read isMessageRead: channel.state.read
?.where((element) => ?.where((element) =>
element.user.id != element.user.id !=
@@ -123,7 +145,7 @@ class ChannelPreview extends StatelessWidget {
return SizedBox(); return SizedBox();
}, },
), ),
_buildDate(context), trailing ?? _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,11 +573,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
], ],
), ),
), ),
Padding( widget.editMessageInputBuilder != null
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: widget.editMessageInputBuilder != null
? widget.editMessageInputBuilder(context, widget.message) ? widget.editMessageInputBuilder(context, widget.message)
: MessageInput( : MessageInput(
editMessage: widget.message, editMessage: widget.message,
@@ -555,7 +583,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
return m; 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,8 +483,18 @@ 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: Padding(
padding: const EdgeInsets.all(1.5),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: theme.messageInputTheme.borderRadius,
color: theme.messageInputTheme.inputBackground,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -467,25 +513,56 @@ class MessageInputState extends State<MessageInput> {
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
controller: textEditingController, controller: textEditingController,
focusNode: _focusNode, focusNode: _focusNode,
style: theme.textTheme.body, style: theme.messageInputTheme.inputTextStyle,
autofocus: false, autofocus: widget.autofocus,
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration( decoration: _getInputDecoration(),
textCapitalization: TextCapitalization.sentences,
),
)
],
),
),
),
),
),
);
}
InputDecoration _getInputDecoration() {
final theme = StreamChatTheme.of(context);
final passedDecoration = theme.messageInputTheme.inputDecoration;
return InputDecoration(
isDense: true, isDense: true,
hintText: _getHint(), hintText: _getHint(),
hintStyle: theme.textTheme.body.copyWith( hintStyle: theme.messageInputTheme.inputTextStyle.copyWith(
color: theme.colorTheme.grey, color: theme.colorTheme.grey,
), ),
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent)), borderSide: BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent)), borderSide: BorderSide(
color: Colors.transparent,
),
),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent)), borderSide: BorderSide(
color: Colors.transparent,
),
),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent)), borderSide: BorderSide(
color: Colors.transparent,
),
),
disabledBorder: OutlineInputBorder( disabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent)), borderSide: BorderSide(
color: Colors.transparent,
),
),
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)), prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)), suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
@@ -517,8 +594,10 @@ class MessageInputState extends State<MessageInput> {
), ),
) )
: null, : null,
suffixIcon: _commandEnabled suffixIcon: Row(
? IconButton( children: [
if (_commandEnabled)
IconButton(
icon: StreamSvgIcon.closeSmall(), icon: StreamSvgIcon.closeSmall(),
splashRadius: 24, splashRadius: 24,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
@@ -529,17 +608,12 @@ class MessageInputState extends State<MessageInput> {
onPressed: () { onPressed: () {
setState(() => _commandEnabled = false); setState(() => _commandEnabled = false);
}, },
)
: null,
), ),
textCapitalization: TextCapitalization.sentences, if (widget.sendButtonLocation == SendButtonLocation.inside)
), _animateSendButton(context),
)
], ],
), ),
), ).merge(passedDecoration);
),
);
} }
Timer _debounce; Timer _debounce;
@@ -621,9 +695,11 @@ class MessageInputState extends State<MessageInput> {
.last .last
.contains('@')) { .contains('@')) {
_mentionsOverlay = _buildMentionsOverlayEntry(); _mentionsOverlay = _buildMentionsOverlayEntry();
if (_mentionsOverlay != null) {
Overlay.of(context).insert(_mentionsOverlay); Overlay.of(context).insert(_mentionsOverlay);
} }
} }
}
void _checkCommands(String s, BuildContext context) { void _checkCommands(String s, BuildContext context) {
if (s.startsWith('/')) { if (s.startsWith('/')) {
@@ -646,10 +722,12 @@ class MessageInputState extends State<MessageInput> {
_commandsOverlay = null; _commandsOverlay = null;
} else { } else {
_commandsOverlay = _buildCommandsOverlayEntry(); _commandsOverlay = _buildCommandsOverlayEntry();
if (_commandsOverlay != null) {
Overlay.of(context).insert(_commandsOverlay); Overlay.of(context).insert(_commandsOverlay);
} }
} }
} }
}
OverlayEntry _buildCommandsOverlayEntry() { OverlayEntry _buildCommandsOverlayEntry() {
final text = textEditingController.text.trimLeft(); final text = textEditingController.text.trimLeft();
@@ -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,8 +103,6 @@ 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,20 +101,27 @@ 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 ??
(showBackButton
? StreamBackButton( ? StreamBackButton(
cid: StreamChannel.of(context).channel.cid, cid: StreamChannel.of(context).channel.cid,
onPressed: onBackPressed, onPressed: onBackPressed,
showUnreads: true, 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,
title: InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
title ??
Text( Text(
'Thread Reply', 'Thread Reply',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -103,6 +130,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
.title, .title,
), ),
SizedBox(height: 2), SizedBox(height: 2),
subtitle ??
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -113,20 +141,22 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.channelTheme .channelTheme
.channelHeaderTheme .channelHeaderTheme
.lastMessageAt, .subtitle,
), ),
Flexible( Flexible(
child: ChannelName( child: ChannelName(
textStyle: StreamChatTheme.of(context) textStyle: StreamChatTheme.of(context)
.channelTheme .channelTheme
.channelHeaderTheme .channelHeaderTheme
.lastMessageAt, .subtitle,
), ),
), ),
], ],
), ),
], ],
), ),
),
),
); );
} }
@@ -14,7 +14,8 @@ 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(
child: StreamBuilder<int>(
stream: cid != null stream: cid != null
? client.state.channels[cid].state.unreadCountStream ? client.state.channels[cid].state.unreadCountStream
: client.state.totalUnreadCountStream, : client.state.totalUnreadCountStream,
@@ -49,6 +50,7 @@ class UnreadIndicator extends StatelessWidget {
), ),
); );
}, },
),
); );
} }
} }
@@ -39,7 +39,9 @@ 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(
fit: BoxFit.cover,
child: ClipRRect(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
borderRadius: borderRadius ?? borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius, streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
@@ -60,6 +62,7 @@ class UserAvatar extends StatelessWidget {
) )
: streamChatTheme.defaultUserImage(context, user), : streamChatTheme.defaultUserImage(context, user),
), ),
),
); );
if (selected) { if (selected) {
@@ -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);
} }