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
+8 -7
View File
@@ -84,16 +84,17 @@ class StreamChatClient {
Duration connectTimeout = const Duration(seconds: 6),
Duration receiveTimeout = const Duration(seconds: 6),
Dio httpClient,
// ignore: avoid_unused_constructor_parameters
RetryPolicy retryPolicy,
this.attachmentFileUploader,
}) {
_retryPolicy ??= RetryPolicy(
retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
Duration(seconds: 1 * attempt),
shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
attempt < 5,
);
_retryPolicy = retryPolicy ??
RetryPolicy(
retryTimeout:
(StreamChatClient client, int attempt, ApiError error) =>
Duration(seconds: 1 * attempt),
shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
attempt < 5,
);
attachmentFileUploader ??= StreamAttachmentFileUploader(this);
@@ -104,10 +104,10 @@ class FileAttachment extends AttachmentWidget {
return getFileTypeImage(attachment.extraData['other']);
},
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Image.memory(kTransparentImage),
return Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
);
},
),
@@ -348,10 +348,10 @@ class GiphyAttachment extends AttachmentWidget {
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Image.memory(kTransparentImage),
return Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
);
},
imageUrl: imageUrl,
@@ -84,10 +84,10 @@ class ImageAttachment extends AttachmentWidget {
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Image.memory(kTransparentImage),
return Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
);
},
imageUrl: imageUrl,
@@ -114,9 +114,9 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
children: [
UserAvatar(
user: members[index].user,
constraints: BoxConstraints(
maxHeight: 64.0,
maxWidth: 64.0,
constraints: BoxConstraints.tightFor(
height: 64.0,
width: 64.0,
),
borderRadius: BorderRadius.circular(32.0),
onlineIndicatorConstraints:
@@ -238,8 +238,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
final channel = StreamChannel.of(context).channel;
await channel.removeMembers([StreamChat.of(context).user.id]);
Navigator.pop(context);
}
@@ -71,6 +71,19 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
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
ChannelHeader({
Key key,
@@ -80,6 +93,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
this.showTypingIndicator = true,
this.onImageTap,
this.showConnectionStateTile = false,
this.title,
this.subtitle,
this.leading,
this.actions,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@@ -87,6 +104,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
final leadingWidget = leading ??
(showBackButton
? StreamBackButton(
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox());
return ConnectionStatusBuilder(
statusBuilder: (context, status) {
var statusString = '';
@@ -111,26 +136,32 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
child: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
leading: showBackButton
? StreamBackButton(
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox(),
leading: leadingWidget,
backgroundColor: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.color,
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Center(
child: ChannelImage(
onTap: onImageTap,
actions: actions ??
<Widget>[
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Center(
child: ChannelImage(
borderRadius: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.avatarTheme
.borderRadius,
constraints: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.avatarTheme
.constraints,
onTap: onImageTap,
),
),
),
),
),
],
],
centerTitle: true,
title: InkWell(
onTap: onTitleTap,
@@ -141,20 +172,23 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ChannelName(
textStyle: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title,
),
title ??
ChannelName(
textStyle: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title,
),
SizedBox(height: 2),
ChannelInfo(
showTypingIndicator: showTypingIndicator,
channel: channel,
textStyle: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle,
),
subtitle ??
ChannelInfo(
showTypingIndicator: showTypingIndicator,
channel: channel,
textStyle: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.subtitle,
),
],
),
),
@@ -57,7 +57,7 @@ class ChannelInfo extends StatelessWidget {
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
.subtitle,
);
} else {
final otherMember = members.firstWhere(
@@ -69,10 +69,7 @@ class ChannelInfo extends StatelessWidget {
if (otherMember.user.online) {
alternativeWidget = Text(
'Online',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
style: textStyle,
);
} else {
alternativeWidget = Text(
@@ -45,7 +45,7 @@ typedef _TitleBuilder = Widget Function(
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status.
/// However you can also pass your own [StreamChatClient] if you don't have it in the widget tree.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [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.
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// Instantiates a ChannelListHeader
@@ -57,6 +57,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
this.onNewChatButtonTap,
this.showConnectionStateTile = false,
this.preNavigationCallback,
this.subtitle,
this.leading,
this.actions,
}) : super(key: key);
/// 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;
/// 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
Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client;
@@ -104,76 +118,85 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
child: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
backgroundColor: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.color,
backgroundColor:
StreamChatTheme.of(context).channelListHeaderTheme.color,
centerTitle: true,
leading: Center(
child: UserAvatar(
user: user,
showOnlineStatus: false,
onTap: onUserAvatarTap ??
(_) {
if (preNavigationCallback != null) {
preNavigationCallback();
}
Scaffold.of(context).openDrawer();
},
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
),
actions: [
StreamNeumorphicButton(
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,
);
},
leading: leading ??
Center(
child: UserAvatar(
user: user,
showOnlineStatus: false,
onTap: onUserAvatarTap ??
(_) {
if (preNavigationCallback != null) {
preNavigationCallback();
}
Scaffold.of(context).openDrawer();
},
borderRadius: StreamChatTheme.of(context)
.channelListHeaderTheme
.avatarTheme
.borderRadius,
constraints: StreamChatTheme.of(context)
.channelListHeaderTheme
.avatarTheme
.constraints,
),
onPressed: onNewChatButtonTap,
),
)
],
title: 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();
}
},
actions: actions ??
[
StreamNeumorphicButton(
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,
),
)
],
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),
Text(
'Searching for Network',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title
.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
style:
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
);
@@ -222,14 +242,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
children: [
Text(
'Offline...',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title
.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
style:
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () async {
@@ -239,8 +256,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
child: Text(
'Try Again',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.channelListHeaderTheme
.title
.copyWith(
fontSize: 16,
@@ -32,16 +32,37 @@ class ChannelPreview extends StatelessWidget {
/// The function called when the image is tapped
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({
@required this.channel,
Key key,
this.onTap,
this.onLongPress,
this.onImageTap,
this.title,
this.subtitle,
this.leading,
this.sendingIndicator,
this.trailing,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
return StreamBuilder<bool>(
stream: channel.isMutedStream,
initialData: channel.isMuted,
@@ -49,6 +70,7 @@ class ChannelPreview extends StatelessWidget {
return Opacity(
opacity: snapshot.data ? 0.5 : 1,
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
@@ -62,17 +84,18 @@ class ChannelPreview extends StatelessWidget {
onLongPress(channel);
}
},
leading: ChannelImage(
onTap: onImageTap,
),
leading: leading ??
ChannelImage(
onTap: onImageTap,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: ChannelName(
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title,
),
child: title ??
ChannelName(
textStyle: channelPreviewTheme.title,
),
),
StreamBuilder<List<Member>>(
stream: channel.state.membersStream,
@@ -93,37 +116,36 @@ class ChannelPreview extends StatelessWidget {
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: _buildSubtitle(context)),
Builder(
builder: (context) {
final lastMessage = channel.state.messages.lastWhere(
(m) => !m.isDeleted && m.shadowed != true,
orElse: () => null,
);
if (lastMessage?.user?.id ==
StreamChat.of(context).user.id) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator(
message: lastMessage,
size: StreamChatTheme.of(context)
.channelPreviewTheme
.indicatorIconSize,
isMessageRead: channel.state.read
?.where((element) =>
element.user.id !=
channel.client.state.user.id)
?.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
?.isNotEmpty ==
true,
),
);
}
return SizedBox();
},
),
_buildDate(context),
Flexible(child: subtitle ?? _buildSubtitle(context)),
sendingIndicator ??
Builder(
builder: (context) {
final lastMessage = channel.state.messages.lastWhere(
(m) => !m.isDeleted && m.shadowed != true,
orElse: () => null,
);
if (lastMessage?.user?.id ==
StreamChat.of(context).user.id) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator(
message: lastMessage,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: channel.state.read
?.where((element) =>
element.user.id !=
channel.client.state.user.id)
?.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
?.isNotEmpty ==
true,
),
);
}
return SizedBox();
},
),
trailing ?? _buildDate(context),
],
),
),
@@ -176,15 +198,7 @@ class ChannelPreview extends StatelessWidget {
),
Text(
' Channel is muted',
style: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.copyWith(
color: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.color,
),
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
],
);
@@ -192,10 +206,7 @@ class ChannelPreview extends StatelessWidget {
return TypingIndicator(
channel: channel,
alternativeWidget: _buildLastMessage(context),
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color:
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
),
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
);
}
@@ -1,6 +1,7 @@
import 'package:characters/characters.dart';
import 'package:emojis/emoji.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
final _emojis = Emoji.all();
@@ -44,3 +45,55 @@ extension PlatformFileX on PlatformFile {
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/material.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/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
@@ -34,6 +35,9 @@ class MessageActionsModal extends StatefulWidget {
final ShapeBorder attachmentShape;
final DisplayWidget showUserAvatar;
/// List of custom actions
final List<MessageAction> customActions;
const MessageActionsModal({
Key key,
@required this.message,
@@ -53,6 +57,7 @@ class MessageActionsModal extends StatefulWidget {
this.messageShape,
this.attachmentShape,
this.reverse = false,
this.customActions = const [],
}) : super(key: key);
@override
@@ -223,6 +228,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
_buildFlagButton(context),
if (widget.showDeleteMessage)
_buildDeleteButton(context),
...widget.customActions.map((action) {
return _buildCustomAction(
context,
action,
);
})
].insertBetween(
Container(
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 {
final client = StreamChat.of(context).client;
@@ -541,21 +573,16 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
],
),
),
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: widget.editMessageInputBuilder != null
? widget.editMessageInputBuilder(context, widget.message)
: MessageInput(
editMessage: widget.message,
preMessageSending: (m) {
FocusScope.of(context).unfocus();
Navigator.pop(context);
return m;
},
),
),
widget.editMessageInputBuilder != null
? widget.editMessageInputBuilder(context, widget.message)
: MessageInput(
editMessage: widget.message,
preMessageSending: (m) {
FocusScope.of(context).unfocus();
Navigator.pop(context);
return m;
},
),
],
),
);
@@ -42,6 +42,15 @@ enum DefaultAttachmentTypes {
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 _kMaxAttachmentSize = 20971520; // 20MB in Bytes
@@ -107,6 +116,11 @@ class MessageInput extends StatefulWidget {
this.focusNode,
this.quotedMessage,
this.onQuotedMessageCleared,
this.sendButtonLocation = SendButtonLocation.outside,
this.autofocus = false,
this.hideSendAsDm = false,
this.idleSendButton,
this.activeSendButton,
}) : super(key: key);
/// Message to edit
@@ -134,6 +148,9 @@ class MessageInput extends StatefulWidget {
/// If true the attachments button will not be displayed
final bool disableAttachments;
/// Hide send as dm checkbox
final bool hideSendAsDm;
/// The text controller of the TextField
final TextEditingController textEditingController;
@@ -155,6 +172,18 @@ class MessageInput extends StatefulWidget {
///
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
MessageInputState createState() => MessageInputState();
@@ -281,7 +310,7 @@ class MessageInputState extends State<MessageInput> {
padding: const EdgeInsets.all(8.0),
child: _buildTextField(context),
),
if (widget.parentMessage != null)
if (widget.parentMessage != null && !widget.hideSendAsDm)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildDmCheckbox(),
@@ -310,9 +339,10 @@ class MessageInputState extends State<MessageInput> {
if (widget.actionsLocation == ActionsLocation.left)
...widget.actions ?? [],
_buildTextInput(context),
_animateSendButton(context),
if (widget.actionsLocation == ActionsLocation.right)
...widget.actions ?? [],
if (widget.sendButtonLocation == SendButtonLocation.outside)
_animateSendButton(context),
],
);
}
@@ -384,14 +414,20 @@ class MessageInputState extends State<MessageInput> {
}
Widget _animateSendButton(BuildContext context) {
final sendButton = widget.activeSendButton != null
? InkWell(
child: widget.activeSendButton,
onTap: sendMessage,
)
: _buildSendButton(context);
return Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedCrossFade(
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: _buildSendButton(context),
secondChild: _buildIdleSendButton(context),
firstChild: sendButton,
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
duration:
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
alignment: Alignment.center,
@@ -447,101 +483,139 @@ class MessageInputState extends State<MessageInput> {
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
border: Border.all(color: theme.colorTheme.greyGainsboro),
borderRadius: theme.messageInputTheme.borderRadius,
gradient: _focusNode.hasFocus
? theme.messageInputTheme.activeBorderGradient
: theme.messageInputTheme.idleBorderGradient,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReplyToMessage(),
_buildAttachments(),
LimitedBox(
maxHeight: widget.maxHeight,
child: TextField(
key: Key('messageInputText'),
enabled: _inputEnabled,
minLines: null,
maxLines: null,
onSubmitted: (_) => sendMessage(),
keyboardType: widget.keyboardType,
controller: textEditingController,
focusNode: _focusNode,
style: theme.textTheme.body,
autofocus: false,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
isDense: true,
hintText: _getHint(),
hintStyle: theme.textTheme.body.copyWith(
color: theme.colorTheme.grey,
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(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildReplyToMessage(),
_buildAttachments(),
LimitedBox(
maxHeight: widget.maxHeight,
child: TextField(
key: Key('messageInputText'),
enabled: _inputEnabled,
minLines: null,
maxLines: null,
onSubmitted: (_) => sendMessage(),
keyboardType: widget.keyboardType,
controller: textEditingController,
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;
void _onChanged(BuildContext context, String s) {
@@ -621,7 +695,9 @@ class MessageInputState extends State<MessageInput> {
.last
.contains('@')) {
_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;
} else {
_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() ??
[];
if (commands.isEmpty) {
return null;
}
RenderBox renderBox = context.findRenderObject();
final size = renderBox.size;
@@ -833,6 +915,7 @@ class MessageInputState extends State<MessageInput> {
child: Material(
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
@@ -1100,6 +1183,10 @@ class MessageInputState extends State<MessageInput> {
})?.toList() ??
[];
if (members.isEmpty) {
return null;
}
RenderBox renderBox = context.findRenderObject();
final size = renderBox.size;
@@ -36,6 +36,7 @@ typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
typedef ThreadTapCallback = void Function(Message, Widget);
typedef OnMessageSwiped = void Function(Message);
typedef OnMessageTap = void Function(Message);
typedef ReplyTapCallback = void Function(Message);
class MessageDetails {
@@ -224,10 +225,10 @@ class MessageListView extends StatefulWidget {
final Map<String, AttachmentBuilder> customAttachmentBuilders;
/// 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
final void Function(Message) onSystemMessageTap;
final OnMessageTap onSystemMessageTap;
// Customize onTap on attachment
final void Function(Message message, Attachment attachment) onAttachmentTap;
@@ -436,7 +437,7 @@ class _MessageListViewState extends State<MessageListView> {
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
.subtitle,
),
),
);
@@ -1,6 +1,7 @@
import 'dart:ui';
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_core/stream_chat_flutter_core.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_portal/flutter_portal.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_reactions_modal.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
final void Function(Message) onMessageTap;
/// List of custom actions shown on message long tap
final List<MessageAction> customActions;
// Customize onTap on attachment
final void Function(Message message, Attachment attachment) onAttachmentTap;
@@ -198,6 +202,7 @@ class MessageWidget extends StatefulWidget {
this.attachmentPadding = EdgeInsets.zero,
this.allRead = false,
this.onQuotedMessageTap,
this.customActions = const [],
this.onAttachmentTap,
}) : attachmentBuilders = {
'image': (context, message, attachment) {
@@ -760,6 +765,7 @@ class _MessageWidgetState extends State<MessageWidget>
!isFailedState &&
widget.onThreadTap != null,
showFlagButton: widget.showFlagButton,
customActions: widget.customActions,
),
);
});
@@ -966,6 +972,7 @@ class _MessageWidgetState extends State<MessageWidget>
user: widget.message.user,
onTap: widget.onUserAvatarTap,
constraints: widget.messageTheme.avatarTheme.constraints,
borderRadius: widget.messageTheme.avatarTheme.borderRadius,
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/reaction_icon.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';
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
@@ -45,12 +46,15 @@ class StreamChatThemeData {
/// The text themes used in the widgets
final TextTheme textTheme;
/// The text themes used in the widgets
/// The color themes used in the widgets
final ColorTheme colorTheme;
/// Theme of the [ChannelPreview]
final ChannelPreviewTheme channelPreviewTheme;
/// Theme of the [ChannelListHeader]
final ChannelListHeaderTheme channelListHeaderTheme;
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
@@ -60,7 +64,7 @@ class StreamChatThemeData {
/// Theme of other users messages
final MessageTheme otherMessageTheme;
/// Theme of other users messages
/// Theme dedicated to the [MessageInput] widget
final MessageInputTheme messageInputTheme;
/// The widget that will be built when the channel image is unavailable
@@ -79,6 +83,7 @@ class StreamChatThemeData {
const StreamChatThemeData({
this.textTheme,
this.colorTheme,
this.channelListHeaderTheme,
this.channelPreviewTheme,
this.channelTheme,
this.otherMessageTheme,
@@ -98,9 +103,7 @@ class StreamChatThemeData {
accentBlue: theme.accentColor,
),
defaultTheme.textTheme,
).copyWith(
// primaryIconTheme: theme.primaryIconTheme,
);
);
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
}
@@ -116,9 +119,12 @@ class StreamChatThemeData {
Widget Function(BuildContext, Channel) defaultChannelImage,
Widget Function(BuildContext, User) defaultUserImage,
IconThemeData primaryIconTheme,
ChannelListHeaderTheme channelListHeaderTheme,
List<ReactionIcon> reactionIcons,
}) =>
StreamChatThemeData(
channelListHeaderTheme:
channelListHeaderTheme ?? this.channelListHeaderTheme,
textTheme: textTheme ?? this.textTheme,
colorTheme: colorTheme ?? this.colorTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
@@ -135,6 +141,9 @@ class StreamChatThemeData {
StreamChatThemeData merge(StreamChatThemeData other) {
if (other == null) return this;
return copyWith(
channelListHeaderTheme:
channelListHeaderTheme?.merge(other.channelListHeaderTheme) ??
other.channelListHeaderTheme,
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
primaryIconTheme: other.primaryIconTheme,
@@ -189,6 +198,17 @@ class StreamChatThemeData {
color: colorTheme.black.withOpacity(.5),
),
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(
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
@@ -200,9 +220,8 @@ class StreamChatThemeData {
),
color: colorTheme.white,
title: textTheme.headlineBold,
lastMessageAt: TextStyle(
fontSize: 11,
color: colorTheme.black.withOpacity(.5),
subtitle: textTheme.footnote.copyWith(
color: Color(0xff7A7A7A),
),
),
),
@@ -249,12 +268,26 @@ class StreamChatThemeData {
),
),
messageInputTheme: MessageInputTheme(
borderRadius: BorderRadius.circular(20),
sendAnimationDuration: Duration(milliseconds: 300),
actionButtonColor: colorTheme.accentBlue,
actionButtonIdleColor: colorTheme.grey,
sendButtonColor: colorTheme.accentBlue,
sendButtonIdleColor: colorTheme.greyGainsboro,
inputBackground: colorTheme.white,
inputTextStyle: textTheme.body,
idleBorderGradient: LinearGradient(
colors: [
colorTheme.greyGainsboro,
colorTheme.greyGainsboro,
],
),
activeBorderGradient: LinearGradient(
colors: [
colorTheme.greyGainsboro,
colorTheme.greyGainsboro,
],
),
),
reactionIcons: [
ReactionIcon(
@@ -809,26 +842,26 @@ class ChannelPreviewTheme {
class ChannelHeaderTheme {
final TextStyle title;
final TextStyle lastMessageAt;
final TextStyle subtitle;
final AvatarTheme avatarTheme;
final Color color;
const ChannelHeaderTheme({
this.title,
this.lastMessageAt,
this.subtitle,
this.avatarTheme,
this.color,
});
ChannelHeaderTheme copyWith({
TextStyle title,
TextStyle lastMessageAt,
TextStyle subtitle,
AvatarTheme avatarTheme,
Color color,
}) =>
ChannelHeaderTheme(
title: title ?? this.title,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
subtitle: subtitle ?? this.subtitle,
avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color,
);
@@ -837,8 +870,48 @@ class ChannelHeaderTheme {
if (other == null) return this;
return copyWith(
title: title?.merge(other.title) ?? other.title,
lastMessageAt:
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
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,
color: other.color,
);
@@ -865,6 +938,21 @@ class MessageInputTheme {
/// Background color of [MessageInput]
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]
const MessageInputTheme({
this.sendAnimationDuration,
@@ -873,6 +961,11 @@ class MessageInputTheme {
this.actionButtonIdleColor,
this.sendButtonIdleColor,
this.inputBackground,
this.inputTextStyle,
this.inputDecoration,
this.activeBorderGradient,
this.idleBorderGradient,
this.borderRadius,
});
/// Returns a new [MessageInputTheme] replacing some of its properties
@@ -883,6 +976,11 @@ class MessageInputTheme {
Color sendButtonColor,
Color actionButtonIdleColor,
Color sendButtonIdleColor,
TextStyle inputTextStyle,
InputDecoration inputDecoration,
Gradient activeBorderGradient,
Gradient idleBorderGradient,
BorderRadius borderRadius,
}) =>
MessageInputTheme(
sendAnimationDuration:
@@ -892,7 +990,12 @@ class MessageInputTheme {
sendButtonColor: sendButtonColor ?? this.sendButtonColor,
actionButtonIdleColor:
actionButtonIdleColor ?? this.actionButtonIdleColor,
inputTextStyle: inputTextStyle ?? this.inputTextStyle,
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]
@@ -905,6 +1008,12 @@ class MessageInputTheme {
actionButtonIdleColor: other.actionButtonIdleColor,
sendButtonColor: other.sendButtonColor,
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]
final VoidCallback onBackPressed;
/// Callback to call when the title is tapped.
final VoidCallback onTitleTap;
/// The message parent of this thread
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
ThreadHeader({
Key key,
@required this.parent,
this.showBackButton = true,
this.onBackPressed,
this.title,
this.subtitle,
this.leading,
this.actions,
this.onTitleTap,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@@ -81,51 +101,61 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
automaticallyImplyLeading: false,
brightness: Theme.of(context).brightness,
elevation: 1,
leading: showBackButton
? StreamBackButton(
cid: StreamChannel.of(context).channel.cid,
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox(),
leading: leading ??
(showBackButton
? StreamBackButton(
cid: StreamChannel.of(context).channel.cid,
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox()),
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
centerTitle: true,
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Thread Reply',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title,
),
SizedBox(height: 2),
Row(
mainAxisSize: MainAxisSize.min,
actions: actions,
title: InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'with ',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
Flexible(
child: ChannelName(
textStyle: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
title ??
Text(
'Thread Reply',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title,
),
SizedBox(height: 2),
subtitle ??
Row(
mainAxisSize: MainAxisSize.min,
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
Widget build(BuildContext context) {
final client = StreamChat.of(context).client;
return StreamBuilder<int>(
stream: cid != null
? client.state.channels[cid].state.unreadCountStream
: client.state.totalUnreadCountStream,
initialData: cid != null
? client.state.channels[cid].state.unreadCount
: client.state.totalUnreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
return IgnorePointer(
child: StreamBuilder<int>(
stream: cid != null
? client.state.channels[cid].state.unreadCountStream
: client.state.totalUnreadCountStream,
initialData: cid != null
? client.state.channels[cid].state.unreadCount
: client.state.totalUnreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
),
),
),
),
),
);
},
);
},
),
);
}
}
@@ -39,26 +39,29 @@ class UserAvatar extends StatelessWidget {
user.extraData['image'] != '';
final streamChatTheme = StreamChatTheme.of(context);
Widget avatar = ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
child: Container(
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
decoration: BoxDecoration(
color: streamChatTheme.colorTheme.accentBlue,
Widget avatar = FittedBox(
fit: BoxFit.cover,
child: ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
child: Container(
constraints: constraints ??
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,
child: Material(
type: MaterialType.circle,
color: streamChatTheme.colorTheme.white,
child: Container(
margin: const EdgeInsets.all(2.0),
constraints: onlineIndicatorConstraints ??
@@ -103,7 +107,6 @@ class UserAvatar extends StatelessWidget {
color: streamChatTheme.colorTheme.accentGreen,
),
),
color: streamChatTheme.colorTheme.white,
),
),
),
@@ -7,6 +7,7 @@ export 'src/channel_name.dart';
export 'src/channel_preview.dart';
export 'src/date_divider.dart';
export 'src/deleted_message.dart';
export 'src/message_action.dart';
export 'src/attachment/attachment.dart';
export 'src/full_screen_media.dart';
export 'src/image_header.dart';
@@ -172,11 +172,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
}));
_subscriptions.add(client
.on(EventType.channelDeleted, EventType.notificationRemovedFromChannel)
.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)
.listen((e) {
final channel = e.channel;
_channelsController
.add(List.from(channels..removeWhere((c) => c.cid == channel.cid)));
_channelsController.add(List.from(
(channels ?? [])..removeWhere((c) => c.cid == channel.cid)));
}));
}
@@ -144,7 +144,7 @@ class _MessageListCoreState extends State<MessageListCore> {
return widget.errorWidgetBuilder(context, snapshot.error);
} else {
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty) {
if (messageList.isEmpty && !_isThreadConversation) {
if (_upToDate) {
return widget.emptyBuilder(context);
}