performance fixes

This commit is contained in:
Salvatore Giordano
2021-06-09 16:11:59 +02:00
parent e567e5f12b
commit dc6c3094e9
26 changed files with 1224 additions and 1158 deletions
@@ -1709,7 +1709,7 @@ class ChannelClientState {
final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0); final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0);
/// Unread count getter as a stream /// Unread count getter as a stream
Stream<int> get unreadCountStream => _unreadCountController.stream; Stream<int> get unreadCountStream => _unreadCountController.stream.distinct();
/// Unread count getter /// Unread count getter
int? get unreadCount => _unreadCountController.value; int? get unreadCount => _unreadCountController.value;
+4 -2
View File
@@ -1532,13 +1532,15 @@ class ClientState {
int? get unreadChannels => _unreadChannelsController.valueOrNull; int? get unreadChannels => _unreadChannelsController.valueOrNull;
/// The current unread channels count as a stream /// The current unread channels count as a stream
Stream<int?> get unreadChannelsStream => _unreadChannelsController.stream; Stream<int?> get unreadChannelsStream =>
_unreadChannelsController.stream.distinct();
/// The current total unread messages count /// The current total unread messages count
int? get totalUnreadCount => _totalUnreadCountController.valueOrNull; int? get totalUnreadCount => _totalUnreadCountController.valueOrNull;
/// The current total unread messages count as a stream /// The current total unread messages count as a stream
Stream<int?> get totalUnreadCountStream => _totalUnreadCountController.stream; Stream<int?> get totalUnreadCountStream =>
_totalUnreadCountController.stream.distinct();
/// The current list of channels in memory as a stream /// The current list of channels in memory as a stream
Stream<Map<String?, Channel>?> get channelsStream => Stream<Map<String?, Channel>?> get channelsStream =>
@@ -3,10 +3,10 @@ analyzer:
- extension-methods - extension-methods
exclude: exclude:
- lib/**/*.g.dart - lib/**/*.g.dart
# - example/** - example/**
- lib/src/emoji - lib/src/emoji
- lib/**/*.freezed.dart - lib/**/*.freezed.dart
# - test/** - test/**
linter: linter:
rules: rules:
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.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/upload_progress_indicator.dart';
// ignore: always_use_package_imports // ignore: always_use_package_imports
import 'attachment_widget.dart'; import 'attachment_widget.dart';
@@ -103,7 +103,7 @@ class FileAttachment extends AttachmentWidget {
Widget _getFileTypeImage(BuildContext context) { Widget _getFileTypeImage(BuildContext context) {
if (isImageAttachment) { if (isImageAttachment) {
return Material( return Material(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
type: MaterialType.transparency, type: MaterialType.transparency,
shape: _getDefaultShape(context), shape: _getDefaultShape(context),
child: source.when( child: source.when(
@@ -154,7 +154,7 @@ class FileAttachment extends AttachmentWidget {
if (isVideoAttachment) { if (isVideoAttachment) {
return Material( return Material(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
type: MaterialType.transparency, type: MaterialType.transparency,
shape: _getDefaultShape(context), shape: _getDefaultShape(context),
child: source.when( child: source.when(
@@ -53,7 +53,7 @@ class GiphyAttachment extends AttachmentWidget {
Card( Card(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.white,
elevation: 2, elevation: 2,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(16), topRight: Radius.circular(16),
@@ -74,16 +74,16 @@ class ImageAttachment extends AttachmentWidget {
if (imageUri.host == 'stream-io-cdn.com') { if (imageUri.host == 'stream-io-cdn.com') {
imageUri = imageUri.replace(queryParameters: { imageUri = imageUri.replace(queryParameters: {
...imageUri.queryParameters, ...imageUri.queryParameters,
'h': '500', 'h': '400',
'w': '500', 'w': '400',
'crop': 'center', 'crop': 'center',
'resize': 'crop', 'resize': 'crop',
}); });
} else if (imageUri.host == 'stream-cloud-uploads.imgix.net') { } else if (imageUri.host == 'stream-cloud-uploads.imgix.net') {
imageUri = imageUri.replace(queryParameters: { imageUri = imageUri.replace(queryParameters: {
...imageUri.queryParameters, ...imageUri.queryParameters,
'height': '500', 'height': '400',
'width': '500', 'width': '400',
'fit': 'crop', 'fit': 'crop',
}); });
} }
@@ -92,10 +92,10 @@ class ImageAttachment extends AttachmentWidget {
return _buildImageAttachment( return _buildImageAttachment(
context, context,
CachedNetworkImage( CachedNetworkImage(
cacheKey: imageUri.path, cacheKey: imageUrl,
height: size?.height, height: size?.height,
width: size?.width, width: size?.width,
placeholder: (_, __) { placeholder: (context, __) {
final image = Image.asset( final image = Image.asset(
'images/placeholder.png', 'images/placeholder.png',
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -133,8 +133,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
} }
return InfoTile( return InfoTile(
// ignore: avoid_bool_literals_in_conditional_expressions showMessage: showConnectionStateTile && showStatus,
showMessage: showConnectionStateTile ? showStatus : false,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
textTheme: Theme.of(context).textTheme, textTheme: Theme.of(context).textTheme,
@@ -94,15 +94,17 @@ class ChannelImage extends StatelessWidget {
} else if (channel.state?.members.length == 2) { } else if (channel.state?.members.length == 2) {
final otherMember = channel.state?.members final otherMember = channel.state?.members
.firstWhere((member) => member.user?.id != streamChat.user?.id); .firstWhere((member) => member.user?.id != streamChat.user?.id);
return StreamBuilder<User>( return BetterStreamBuilder<User?>(
stream: streamChat.client.state.usersStream.map( stream: streamChat.client.state.usersStream
(users) => users[otherMember?.userId] ?? otherMember!.user!), .map((users) =>
users[otherMember?.userId] ?? otherMember!.user!)
.distinct(),
initialData: otherMember!.user, initialData: otherMember!.user,
builder: (context, snapshot) => UserAvatar( builder: (context, snapshot) => UserAvatar(
borderRadius: borderRadius ?? borderRadius: borderRadius ??
chatThemeData chatThemeData
.channelPreviewTheme.avatarTheme?.borderRadius, .channelPreviewTheme.avatarTheme?.borderRadius,
user: snapshot.data ?? otherMember.user!, user: snapshot ?? otherMember.user!,
constraints: constraints ?? constraints: constraints ??
chatThemeData chatThemeData
.channelPreviewTheme.avatarTheme?.constraints, .channelPreviewTheme.avatarTheme?.constraints,
@@ -1,11 +1,10 @@
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/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';
@@ -245,10 +244,7 @@ class _ChannelListViewState extends State<ChannelListView> {
} }
} }
return AnimatedSwitcher( return child;
duration: const Duration(milliseconds: 500),
child: child,
);
} }
Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder( Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder(
@@ -473,10 +469,9 @@ class _ChannelListViewState extends State<ChannelListView> {
final channel = channels[i]; final channel = channels[i];
return StreamChannel( return StreamChannel(
key: ValueKey<String>('CHANNEL-${channel.id}'), key: ValueKey<String>('CHANNEL-${channel.cid}'),
channel: channel, channel: channel,
child: Builder( child: Slidable(
builder: (context) => Slidable(
controller: _slideController, controller: _slideController,
enabled: widget.swipeToAction, enabled: widget.swipeToAction,
actionPane: const SlidableBehindActionPane(), actionPane: const SlidableBehindActionPane(),
@@ -554,8 +549,10 @@ class _ChannelListViewState extends State<ChannelListView> {
}, },
), ),
], ],
child: Container( child: DecoratedBox(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.whiteSnow, color: chatThemeData.colorTheme.whiteSnow,
),
child: widget.channelPreviewBuilder?.call(context, channel) ?? child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview( ChannelPreview(
onLongPress: widget.onChannelLongPress, onLongPress: widget.onChannelLongPress,
@@ -565,7 +562,6 @@ class _ChannelListViewState extends State<ChannelListView> {
), ),
), ),
), ),
),
); );
} else { } else {
return _buildQueryProgressIndicator(context, channelsBloc); return _buildQueryProgressIndicator(context, channelsBloc);
@@ -637,12 +633,10 @@ class _ChannelListViewState extends State<ChannelListView> {
context, context,
ChannelsBlocState channelsProvider, ChannelsBlocState channelsProvider,
) => ) =>
StreamBuilder<bool>( BetterStreamBuilder<bool>(
stream: channelsProvider.queryChannelsLoading, stream: channelsProvider.queryChannelsLoading,
initialData: false, initialData: false,
builder: (context, snapshot) { errorBuilder: (context, err) => Container(
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.accentRed .accentRed
@@ -653,17 +647,15 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Text('Error loading channels'), child: Text('Error loading channels'),
), ),
), ),
); ),
} builder: (context, snapshot) => snapshot
return snapshot.data!
? const Center( ? const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
: const Offstage(); : const Offstage());
});
Widget _separatorBuilder(context, i) { Widget _separatorBuilder(context, i) {
final effect = StreamChatTheme.of(context).colorTheme.borderBottom; final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
@@ -1,4 +1,5 @@
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart'
show IterableExtension, ListEquality;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
@@ -69,12 +70,12 @@ class ChannelPreview extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme; final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
final streamChatState = StreamChat.of(context); final streamChatState = StreamChat.of(context);
return BetterStreamBuilder<bool>(
return StreamBuilder<bool>(
stream: channel.isMutedStream, stream: channel.isMutedStream,
initialData: channel.isMuted, initialData: channel.isMuted,
builder: (context, snapshot) => Opacity( builder: (context, snapshot) => AnimatedOpacity(
opacity: snapshot.data! ? 0.5 : 1, opacity: snapshot ? 0.5 : 1,
duration: const Duration(milliseconds: 300),
child: ListTile( child: ListTile(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@@ -103,14 +104,16 @@ class ChannelPreview extends StatelessWidget {
textStyle: channelPreviewTheme.title, textStyle: channelPreviewTheme.title,
), ),
), ),
StreamBuilder<List<Member>>( BetterStreamBuilder<List<Member>?>(
stream: channel.state?.membersStream, stream: channel.state?.membersStream,
initialData: channel.state?.members, initialData: channel.state?.members,
comparator: const ListEquality().equals,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || if (snapshot?.isEmpty == true ||
snapshot.data!.isEmpty || snapshot?.any((Member e) =>
!snapshot.data!.any((Member e) => e.user!.id ==
e.user!.id == channel.client.state.user?.id)) { channel.client.state.user?.id) !=
true) {
return const SizedBox(); return const SizedBox();
} }
return UnreadIndicator( return UnreadIndicator(
@@ -159,14 +162,14 @@ class ChannelPreview extends StatelessWidget {
)); ));
} }
Widget _buildDate(BuildContext context) => StreamBuilder<DateTime?>( Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime?>(
stream: channel.lastMessageAtStream, stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt, initialData: channel.lastMessageAt,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (snapshot == null) {
return const SizedBox(); return const Offstage();
} }
final lastMessageAt = snapshot.data!.toLocal(); final lastMessageAt = snapshot.toLocal();
String stringDate; String stringDate;
final now = DateTime.now(); final now = DateTime.now();
@@ -219,11 +222,11 @@ class ChannelPreview extends StatelessWidget {
} }
Widget _buildLastMessage(BuildContext context) => Widget _buildLastMessage(BuildContext context) =>
StreamBuilder<List<Message>?>( BetterStreamBuilder<List<Message>?>(
stream: channel.state!.messagesStream, stream: channel.state!.messagesStream,
initialData: channel.state!.messages, initialData: channel.state!.messages,
builder: (context, snapshot) { builder: (context, snapshot) {
final lastMessage = snapshot.data final lastMessage = snapshot
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
if (lastMessage == null) { if (lastMessage == null) {
return const SizedBox(); return const SizedBox();
@@ -8,7 +8,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// ///
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream]
/// in case no stream is provided. /// in case no stream is provided.
class ConnectionStatusBuilder extends StatelessWidget { class ConnectionStatusBuilder extends StatefulWidget {
/// Creates a new ConnectionStatusBuilder /// Creates a new ConnectionStatusBuilder
const ConnectionStatusBuilder({ const ConnectionStatusBuilder({
Key? key, Key? key,
@@ -36,20 +36,32 @@ class ConnectionStatusBuilder extends StatelessWidget {
statusBuilder; statusBuilder;
@override @override
Widget build(BuildContext context) { _ConnectionStatusBuilderState createState() =>
final client = StreamChat.of(context).client; _ConnectionStatusBuilderState();
final stream = connectionStatusStream ?? client.wsConnectionStatusStream; }
return BetterStreamBuilder<ConnectionStatus>(
initialData: initialStatus ?? client.wsConnectionStatus, class _ConnectionStatusBuilderState extends State<ConnectionStatusBuilder> {
late StreamChatClient client;
late Stream<ConnectionStatus> stream;
@override
Widget build(BuildContext context) => BetterStreamBuilder<ConnectionStatus>(
initialData: widget.initialStatus ?? client.wsConnectionStatus,
stream: stream, stream: stream,
loadingBuilder: loadingBuilder, loadingBuilder: widget.loadingBuilder,
errorBuilder: (context, error) { errorBuilder: (context, error) {
if (errorBuilder != null) { if (widget.errorBuilder != null) {
return errorBuilder!(context, error); return widget.errorBuilder!(context, error);
} }
return const Offstage(); return const Offstage();
}, },
builder: statusBuilder, builder: widget.statusBuilder,
); );
@override
void didChangeDependencies() {
client = StreamChat.of(context).client;
stream = widget.connectionStatusStream ?? client.wsConnectionStatusStream;
super.didChangeDependencies();
} }
} }
@@ -114325,6 +114325,9 @@ class Emoji {
/// Get all Emojis /// Get all Emojis
static List<Emoji> all() => List.unmodifiable(_emojis); static List<Emoji> all() => List.unmodifiable(_emojis);
static Iterable<String> chars() =>
_emojis.map((e) => e.char).whereType<String>();
/// Returns Emoji by [char] and character /// Returns Emoji by [char] and character
static Emoji? byChar(String char) { static Emoji? byChar(String char) {
return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == char); return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == char);
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
final _emojis = Emoji.all(); final _emojiChars = Emoji.chars();
/// String extension /// String extension
extension StringExtension on String { extension StringExtension on String {
@@ -17,10 +17,10 @@ extension StringExtension on String {
/// 1 to 3 emojis: big size with no text bubble. /// 1 to 3 emojis: big size with no text bubble.
/// 4+ emojis or emojis+text: standard size with text bubble. /// 4+ emojis or emojis+text: standard size with text bubble.
bool get isOnlyEmoji { bool get isOnlyEmoji {
if (isEmpty) return false;
if (length > 3) return false;
final characters = trim().characters; final characters = trim().characters;
if (characters.isEmpty) return false; return characters.every(_emojiChars.contains);
if (characters.length > 3) return false;
return characters.every((c) => _emojis.map((e) => e.char).contains(c));
} }
} }
@@ -40,6 +40,9 @@ class InfoTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
if (!showMessage) {
return child;
}
return PortalEntry( return PortalEntry(
visible: showMessage, visible: showMessage,
portalAnchor: tileAnchor ?? Alignment.topCenter, portalAnchor: tileAnchor ?? Alignment.topCenter,
@@ -147,30 +147,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
widget.message.attachments.any((it) => it.type == 'file') == true; widget.message.attachments.any((it) => it.type == 'file') == true;
final streamChatThemeData = StreamChatTheme.of(context); final streamChatThemeData = StreamChatTheme.of(context);
return GestureDetector(
behavior: HitTestBehavior.translucent, final child = Center(
onTap: () => Navigator.maybePop(context),
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
color: streamChatThemeData.colorTheme.overlay,
),
),
),
if (_showActions)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutBack,
builder: (context, val, snapshot) => Transform.scale(
scale: val,
child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -180,17 +158,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
: CrossAxisAlignment.start, : CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
if (widget.showReactions && if (widget.showReactions &&
(widget.message.status == (widget.message.status == MessageSendingStatus.sent))
MessageSendingStatus.sent))
Align( Align(
alignment: Alignment( alignment: Alignment(
user?.id == widget.message.user?.id user?.id == widget.message.user?.id
? (divFactor >= 1.0 ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor))
? -0.2 : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)),
: (1.2 - divFactor))
: (divFactor >= 1.0
? 0.2
: -(1.2 - divFactor)),
0), 0),
child: ReactionPicker( child: ReactionPicker(
message: widget.message, message: widget.message,
@@ -203,8 +176,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
reverse: widget.reverse, reverse: widget.reverse,
attachmentBorderRadiusGeometry: widget attachmentBorderRadiusGeometry: widget
.attachmentBorderRadiusGeometry .attachmentBorderRadiusGeometry
?.mirrorBorderIfReversed( ?.mirrorBorderIfReversed(reverse: !widget.reverse),
reverse: !widget.reverse),
message: widget.message.copyWith( message: widget.message.copyWith(
text: widget.message.text!.length > 200 text: widget.message.text!.length > 200
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
@@ -224,13 +196,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
textPadding: EdgeInsets.symmetric( textPadding: EdgeInsets.symmetric(
vertical: 8, vertical: 8,
horizontal: horizontal: widget.message.text!.isOnlyEmoji ? 0 : 16.0,
widget.message.text!.isOnlyEmoji ? 0 : 16.0,
), ),
showReactionPickerIndicator: showReactionPickerIndicator: widget.showReactions &&
widget.showReactions && (widget.message.status == MessageSendingStatus.sent),
(widget.message.status ==
MessageSendingStatus.sent),
showSendingIndicator: false, showSendingIndicator: false,
shape: widget.messageShape, shape: widget.messageShape,
attachmentShape: widget.attachmentShape, attachmentShape: widget.attachmentShape,
@@ -252,12 +221,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.stretch,
CrossAxisAlignment.stretch,
children: [ children: [
if (widget.showReplyMessage && if (widget.showReplyMessage &&
widget.message.status == widget.message.status == MessageSendingStatus.sent)
MessageSendingStatus.sent)
_buildReplyButton(context), _buildReplyButton(context),
if (widget.showThreadReplyMessage && if (widget.showThreadReplyMessage &&
(widget.message.status == (widget.message.status ==
@@ -266,14 +233,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
_buildThreadReplyButton(context), _buildThreadReplyButton(context),
if (widget.showResendMessage) if (widget.showResendMessage)
_buildResendMessage(context), _buildResendMessage(context),
if (widget.showEditMessage) if (widget.showEditMessage) _buildEditMessage(context),
_buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context),
if (widget.showCopyMessage) if (widget.showFlagButton) _buildFlagButton(context),
_buildCopyButton(context), if (widget.showPinButton) _buildPinButton(context),
if (widget.showFlagButton)
_buildFlagButton(context),
if (widget.showPinButton)
_buildPinButton(context),
if (widget.showDeleteMessage) if (widget.showDeleteMessage)
_buildDeleteButton(context), _buildDeleteButton(context),
...widget.customActions ...widget.customActions
@@ -284,8 +247,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
].insertBetween( ].insertBetween(
Container( Container(
height: 1, height: 1,
color: streamChatThemeData color: streamChatThemeData.colorTheme.greyWhisper,
.colorTheme.greyWhisper,
), ),
), ),
), ),
@@ -296,9 +258,35 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
), ),
), ),
);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context),
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
color: streamChatThemeData.colorTheme.overlay,
), ),
), ),
), ),
if (_showActions)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutBack,
builder: (context, val, child) => Transform.scale(
scale: val,
child: child,
),
child: child,
),
], ],
), ),
); );
@@ -268,6 +268,8 @@ class MessageInputState extends State<MessageInput> {
/// The editing controller passed to the input TextField /// The editing controller passed to the input TextField
late final TextEditingController textEditingController; late final TextEditingController textEditingController;
late StreamChatThemeData _streamChatTheme;
bool get _hasQuotedMessage => widget.quotedMessage != null; bool get _hasQuotedMessage => widget.quotedMessage != null;
@override @override
@@ -305,9 +307,10 @@ class MessageInputState extends State<MessageInput> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final streamChatThemeData = StreamChatTheme.of(context); Widget child = DecoratedBox(
Widget child = Container( decoration: BoxDecoration(
color: streamChatThemeData.messageInputTheme.inputBackground, color: _streamChatTheme.messageInputTheme.inputBackground,
),
child: SafeArea( child: SafeArea(
child: GestureDetector( child: GestureDetector(
onPanUpdate: (details) { onPanUpdate: (details) {
@@ -332,7 +335,7 @@ class MessageInputState extends State<MessageInput> {
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: StreamSvgIcon.reply( child: StreamSvgIcon.reply(
color: streamChatThemeData.colorTheme.greyGainsboro, color: _streamChatTheme.colorTheme.greyGainsboro,
), ),
), ),
const Text( const Text(
@@ -390,9 +393,7 @@ class MessageInputState extends State<MessageInput> {
], ],
); );
Widget _buildDmCheckbox() { Widget _buildDmCheckbox() => Row(
final streamChatThemeData = StreamChatTheme.of(context);
return Row(
children: [ children: [
Container( Container(
height: 16, height: 16,
@@ -401,7 +402,7 @@ class MessageInputState extends State<MessageInput> {
border: _sendAsDm border: _sendAsDm
? null ? null
: Border.all( : Border.all(
color: streamChatThemeData.colorTheme.black.withOpacity(.5), color: _streamChatTheme.colorTheme.black.withOpacity(.5),
width: 2, width: 2,
), ),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
@@ -410,8 +411,8 @@ class MessageInputState extends State<MessageInput> {
child: Material( child: Material(
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
color: _sendAsDm color: _sendAsDm
? streamChatThemeData.colorTheme.accentBlue ? _streamChatTheme.colorTheme.accentBlue
: streamChatThemeData.colorTheme.white, : _streamChatTheme.colorTheme.white,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -426,7 +427,7 @@ class MessageInputState extends State<MessageInput> {
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: StreamSvgIcon.check( firstChild: StreamSvgIcon.check(
size: 16, size: 16,
color: streamChatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
), ),
secondChild: const SizedBox( secondChild: const SizedBox(
height: 16, height: 16,
@@ -441,14 +442,13 @@ class MessageInputState extends State<MessageInput> {
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text( child: Text(
'Also send as direct message', 'Also send as direct message',
style: streamChatThemeData.textTheme.footnote.copyWith( style: _streamChatTheme.textTheme.footnote.copyWith(
color: streamChatThemeData.colorTheme.black.withOpacity(0.5), color: _streamChatTheme.colorTheme.black.withOpacity(0.5),
), ),
), ),
), ),
], ],
); );
}
Widget _animateSendButton(BuildContext context) { Widget _animateSendButton(BuildContext context) {
final sendButton = widget.activeSendButton != null final sendButton = widget.activeSendButton != null
@@ -463,8 +463,7 @@ class MessageInputState extends State<MessageInput> {
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: sendButton, firstChild: sendButton,
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
duration: duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration!,
alignment: Alignment.center, alignment: Alignment.center,
); );
} }
@@ -478,16 +477,18 @@ class MessageInputState extends State<MessageInput> {
? CrossFadeState.showFirst ? CrossFadeState.showFirst
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: IconButton( firstChild: IconButton(
onPressed: () => setState(() => _actionsShrunk = false), onPressed: () {
if (_actionsShrunk) {
setState(() => _actionsShrunk = false);
}
},
icon: Transform.rotate( icon: Transform.rotate(
angle: (widget.actionsLocation == ActionsLocation.right || angle: (widget.actionsLocation == ActionsLocation.right ||
widget.actionsLocation == ActionsLocation.rightInside) widget.actionsLocation == ActionsLocation.rightInside)
? pi ? pi
: 0, : 0,
child: StreamSvgIcon.emptyCircleLeft( child: StreamSvgIcon.emptyCircleLeft(
color: StreamChatTheme.of(context) color: _streamChatTheme.messageInputTheme.expandButtonColor,
.messageInputTheme
.expandButtonColor,
), ),
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
@@ -522,7 +523,6 @@ class MessageInputState extends State<MessageInput> {
} }
Expanded _buildTextInput(BuildContext context) { Expanded _buildTextInput(BuildContext context) {
final theme = StreamChatTheme.of(context);
final margin = (widget.sendButtonLocation == SendButtonLocation.inside final margin = (widget.sendButtonLocation == SendButtonLocation.inside
? const EdgeInsets.only(right: 8) ? const EdgeInsets.only(right: 8)
: EdgeInsets.zero) + : EdgeInsets.zero) +
@@ -530,23 +530,21 @@ class MessageInputState extends State<MessageInput> {
? const EdgeInsets.only(left: 8) ? const EdgeInsets.only(left: 8)
: EdgeInsets.zero); : EdgeInsets.zero);
return Expanded( return Expanded(
child: Center(
child: Container( child: Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
margin: margin, margin: margin,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: theme.messageInputTheme.borderRadius, borderRadius: _streamChatTheme.messageInputTheme.borderRadius,
gradient: _focusNode.hasFocus gradient: _focusNode.hasFocus
? theme.messageInputTheme.activeBorderGradient ? _streamChatTheme.messageInputTheme.activeBorderGradient
: theme.messageInputTheme.idleBorderGradient, : _streamChatTheme.messageInputTheme.idleBorderGradient,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(1.5), padding: const EdgeInsets.all(1.5),
child: Container( child: DecoratedBox(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: theme.messageInputTheme.borderRadius, borderRadius: _streamChatTheme.messageInputTheme.borderRadius,
color: theme.messageInputTheme.inputBackground, color: _streamChatTheme.messageInputTheme.inputBackground,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -564,7 +562,7 @@ class MessageInputState extends State<MessageInput> {
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
controller: textEditingController, controller: textEditingController,
focusNode: _focusNode, focusNode: _focusNode,
style: theme.messageInputTheme.inputTextStyle, style: _streamChatTheme.messageInputTheme.inputTextStyle,
autofocus: widget.autofocus, autofocus: widget.autofocus,
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
decoration: _getInputDecoration(), decoration: _getInputDecoration(),
@@ -576,18 +574,16 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
), ),
),
); );
} }
InputDecoration _getInputDecoration() { InputDecoration _getInputDecoration() {
final theme = StreamChatTheme.of(context); final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration;
final passedDecoration = theme.messageInputTheme.inputDecoration;
return InputDecoration( return InputDecoration(
isDense: true, isDense: true,
hintText: _getHint(), hintText: _getHint(),
hintStyle: theme.messageInputTheme.inputTextStyle!.copyWith( hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith(
color: theme.colorTheme.grey, color: _streamChatTheme.colorTheme.grey,
), ),
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
@@ -625,7 +621,7 @@ class MessageInputState extends State<MessageInput> {
constraints: BoxConstraints.tight(const Size(64, 24)), constraints: BoxConstraints.tight(const Size(64, 24)),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: theme.colorTheme.accentBlue, color: _streamChatTheme.colorTheme.accentBlue,
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Row( child: Row(
@@ -637,7 +633,8 @@ class MessageInputState extends State<MessageInput> {
), ),
Text( Text(
_chosenCommand?.name.toUpperCase() ?? '', _chosenCommand?.name.toUpperCase() ?? '',
style: theme.textTheme.footnoteBold.copyWith( style:
_streamChatTheme.textTheme.footnoteBold.copyWith(
color: Colors.white, color: Colors.white,
), ),
), ),
@@ -828,33 +825,19 @@ class MessageInputState extends State<MessageInput> {
final renderBox = context.findRenderObject() as RenderBox; final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size; final size = renderBox.size;
return OverlayEntry( final child = Padding(
builder: (context) => Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutExpo,
builder: (context, val, wid) {
final streamChatThemeData = StreamChatTheme.of(context);
return Transform.scale(
scale: val,
child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Card( child: Card(
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
color: streamChatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
child: Container( child: Container(
constraints: BoxConstraints.loose( constraints: BoxConstraints.loose(const Size.fromHeight(400)),
const Size.fromHeight(400)),
decoration: BoxDecoration( decoration: BoxDecoration(
color: streamChatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
borderRadius: BorderRadius.circular(8)), borderRadius: BorderRadius.circular(8)),
child: ListView( child: ListView(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
@@ -870,16 +853,14 @@ class MessageInputState extends State<MessageInput> {
horizontal: 8, horizontal: 8,
), ),
child: StreamSvgIcon.lightning( child: StreamSvgIcon.lightning(
color: streamChatThemeData color: _streamChatTheme.colorTheme.accentBlue,
.colorTheme.accentBlue,
), ),
), ),
Text( Text(
'Instant Commands', 'Instant Commands',
style: TextStyle( style: TextStyle(
color: streamChatThemeData color:
.colorTheme.black _streamChatTheme.colorTheme.black.withOpacity(.5),
.withOpacity(.5),
), ),
) )
], ],
@@ -909,17 +890,14 @@ class MessageInputState extends State<MessageInput> {
TextSpan( TextSpan(
text: c.name.capitalize(), text: c.name.capitalize(),
style: const TextStyle( style: const TextStyle(
fontWeight: fontWeight: FontWeight.bold),
FontWeight.bold),
children: [ children: [
TextSpan( TextSpan(
text: text: ' /${c.name} ${c.args}',
' /${c.name} ${c.args}', style: _streamChatTheme.textTheme.body
style: streamChatThemeData
.textTheme.body
.copyWith( .copyWith(
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
color: streamChatThemeData color: _streamChatTheme
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
.colorTheme .colorTheme
.grey, .grey,
@@ -938,19 +916,35 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
), ),
),
); );
}), return OverlayEntry(
builder: (context) => Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutExpo,
builder: (context, val, child) => Transform.scale(
scale: val,
child: child,
),
child: child,
),
)); ));
} }
Widget _buildFilePickerSection() { Widget _buildFilePickerSection() {
if (!_openFilePickerSection) {
return const Offstage();
}
final _attachmentContainsFile = final _attachmentContainsFile =
_attachments.values.any((it) => it.type == 'file'); _attachments.values.any((it) => it.type == 'file');
final chatThemeData = StreamChatTheme.of(context);
Color _getIconColor(int index) { Color _getIconColor(int index) {
final streamChatThemeData = chatThemeData; final streamChatThemeData = _streamChatTheme;
switch (index) { switch (index) {
case 0: case 0:
return _attachments.isEmpty return _attachments.isEmpty
@@ -982,7 +976,7 @@ class MessageInputState extends State<MessageInput> {
_animateContainer ? const Duration(milliseconds: 300) : Duration.zero, _animateContainer ? const Duration(milliseconds: 300) : Duration.zero,
height: _openFilePickerSection ? _filePickerSize : 0, height: _openFilePickerSection ? _filePickerSize : 0,
child: Material( child: Material(
color: chatThemeData.colorTheme.whiteSmoke, color: _streamChatTheme.colorTheme.whiteSmoke,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -1044,9 +1038,9 @@ class MessageInputState extends State<MessageInput> {
); );
}); });
}, },
child: Container( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
@@ -1057,11 +1051,12 @@ class MessageInputState extends State<MessageInput> {
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Container( child: SizedBox(
width: 40, width: 40,
height: 4, height: 4,
child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: chatThemeData.colorTheme.whiteSmoke, color: _streamChatTheme.colorTheme.whiteSmoke,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
), ),
@@ -1070,15 +1065,17 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
), ),
),
if (_openFilePickerSection) if (_openFilePickerSection)
Expanded( Expanded(
child: Container( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: _PickerWidget( child: _PickerWidget(
filePickerIndex: _filePickerIndex, filePickerIndex: _filePickerIndex,
streamChatTheme: _streamChatTheme,
containsFile: _attachmentContainsFile, containsFile: _attachmentContainsFile,
selectedMedias: _attachments.keys.toList(), selectedMedias: _attachments.keys.toList(),
onAddMoreFilesClick: pickFile, onAddMoreFilesClick: pickFile,
@@ -1150,7 +1147,6 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildCommandIcon(String iconType) { Widget _buildCommandIcon(String iconType) {
final chatThemeData = StreamChatTheme.of(context);
switch (iconType) { switch (iconType) {
case 'giphy': case 'giphy':
return CircleAvatar( return CircleAvatar(
@@ -1161,7 +1157,7 @@ class MessageInputState extends State<MessageInput> {
); );
case 'ban': case 'ban':
return CircleAvatar( return CircleAvatar(
backgroundColor: chatThemeData.colorTheme.accentBlue, backgroundColor: _streamChatTheme.colorTheme.accentBlue,
radius: 12, radius: 12,
child: StreamSvgIcon.iconUserDelete( child: StreamSvgIcon.iconUserDelete(
size: 16, size: 16,
@@ -1170,7 +1166,7 @@ class MessageInputState extends State<MessageInput> {
); );
case 'flag': case 'flag':
return CircleAvatar( return CircleAvatar(
backgroundColor: chatThemeData.colorTheme.accentBlue, backgroundColor: _streamChatTheme.colorTheme.accentBlue,
radius: 12, radius: 12,
child: StreamSvgIcon.flag( child: StreamSvgIcon.flag(
size: 14, size: 14,
@@ -1179,7 +1175,7 @@ class MessageInputState extends State<MessageInput> {
); );
case 'imgur': case 'imgur':
return CircleAvatar( return CircleAvatar(
backgroundColor: chatThemeData.colorTheme.accentBlue, backgroundColor: _streamChatTheme.colorTheme.accentBlue,
radius: 12, radius: 12,
child: ClipOval( child: ClipOval(
child: StreamSvgIcon.imgur( child: StreamSvgIcon.imgur(
@@ -1189,7 +1185,7 @@ class MessageInputState extends State<MessageInput> {
); );
case 'mute': case 'mute':
return CircleAvatar( return CircleAvatar(
backgroundColor: chatThemeData.colorTheme.accentBlue, backgroundColor: _streamChatTheme.colorTheme.accentBlue,
radius: 12, radius: 12,
child: StreamSvgIcon.mute( child: StreamSvgIcon.mute(
size: 16, size: 16,
@@ -1198,7 +1194,7 @@ class MessageInputState extends State<MessageInput> {
); );
case 'unban': case 'unban':
return CircleAvatar( return CircleAvatar(
backgroundColor: chatThemeData.colorTheme.accentBlue, backgroundColor: _streamChatTheme.colorTheme.accentBlue,
radius: 12, radius: 12,
child: StreamSvgIcon.userAdd( child: StreamSvgIcon.userAdd(
size: 16, size: 16,
@@ -1207,7 +1203,7 @@ class MessageInputState extends State<MessageInput> {
); );
case 'unmute': case 'unmute':
return CircleAvatar( return CircleAvatar(
backgroundColor: chatThemeData.colorTheme.accentBlue, backgroundColor: _streamChatTheme.colorTheme.accentBlue,
radius: 12, radius: 12,
child: StreamSvgIcon.volumeUp( child: StreamSvgIcon.volumeUp(
size: 16, size: 16,
@@ -1216,7 +1212,7 @@ class MessageInputState extends State<MessageInput> {
); );
default: default:
return CircleAvatar( return CircleAvatar(
backgroundColor: chatThemeData.colorTheme.accentBlue, backgroundColor: _streamChatTheme.colorTheme.accentBlue,
radius: 12, radius: 12,
child: StreamSvgIcon.lightning( child: StreamSvgIcon.lightning(
size: 16, size: 16,
@@ -1253,32 +1249,18 @@ class MessageInputState extends State<MessageInput> {
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
final renderBox = context.findRenderObject() as RenderBox; final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size; final size = renderBox.size;
final child = Card(
return OverlayEntry(
builder: (context) => Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutExpo,
builder: (context, val, wid) {
final chatThemeData = StreamChatTheme.of(context);
return Transform.scale(
scale: val,
child: Card(
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
elevation: 2, elevation: 2,
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
child: Container( child: Container(
constraints: BoxConstraints.loose(const Size.fromHeight(240)), constraints: BoxConstraints.loose(const Size.fromHeight(240)),
decoration: BoxDecoration( decoration: BoxDecoration(
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
), ),
child: FutureBuilder<List<Member>>( child: FutureBuilder<List<Member>>(
future: queryMembers ?? Future.value(members), future: queryMembers ?? Future.value(members),
@@ -1294,7 +1276,7 @@ class MessageInputState extends State<MessageInput> {
.where((it) => it.user != null) .where((it) => it.user != null)
.map( .map(
(m) => Material( (m) => Material(
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (m.user != null) { if (m.user != null) {
@@ -1304,12 +1286,10 @@ class MessageInputState extends State<MessageInput> {
splits[splits.length - 1] = m.user!.name; splits[splits.length - 1] = m.user!.name;
final rejoin = splits.join('@'); final rejoin = splits.join('@');
textEditingController.value = textEditingController.value = TextEditingValue(
TextEditingValue(
text: rejoin + text: rejoin +
textEditingController.text.substring( textEditingController.text.substring(
textEditingController textEditingController.selection.start),
.selection.start),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: rejoin.length, offset: rejoin.length,
), ),
@@ -1332,9 +1312,21 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
), ),
),
); );
}, return OverlayEntry(
builder: (context) => Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutExpo,
builder: (context, val, child) => Transform.scale(
scale: val,
child: child,
),
child: child,
), ),
), ),
); );
@@ -1363,20 +1355,19 @@ class MessageInputState extends State<MessageInput> {
final renderBox = context.findRenderObject() as RenderBox; final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size; final size = renderBox.size;
return OverlayEntry(builder: (context) { return OverlayEntry(
final chatThemeData = StreamChatTheme.of(context); builder: (context) => Positioned(
return Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom, bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0, left: 0,
right: 0, right: 0,
child: Card( child: Card(
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
elevation: 2, elevation: 2,
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
child: Container( child: Container(
constraints: BoxConstraints.loose(const Size.fromHeight(200)), constraints: BoxConstraints.loose(const Size.fromHeight(200)),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -1387,7 +1378,7 @@ class MessageInputState extends State<MessageInput> {
offset: Offset(0, -4), offset: Offset(0, -4),
), ),
], ],
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
), ),
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
@@ -1400,16 +1391,18 @@ class MessageInputState extends State<MessageInput> {
child: Row( child: Row(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding:
const EdgeInsets.symmetric(horizontal: 8),
child: StreamSvgIcon.smile( child: StreamSvgIcon.smile(
color: chatThemeData.colorTheme.accentBlue, color:
_streamChatTheme.colorTheme.accentBlue,
), ),
), ),
Flexible( Flexible(
child: Text( child: Text(
'Emoji matching "$query"', 'Emoji matching "$query"',
style: TextStyle( style: TextStyle(
color: chatThemeData.colorTheme.black color: _streamChatTheme.colorTheme.black
.withOpacity(.5), .withOpacity(.5),
), ),
), ),
@@ -1443,8 +1436,7 @@ class MessageInputState extends State<MessageInput> {
}), }),
), ),
), ),
); ));
});
} }
void _chooseEmoji(List<String> splits, Emoji emoji) { void _chooseEmoji(List<String> splits, Emoji emoji) {
@@ -1483,7 +1475,7 @@ class MessageInputState extends State<MessageInput> {
reverse: true, reverse: true,
showBorder: !containsUrl, showBorder: !containsUrl,
message: widget.quotedMessage!, message: widget.quotedMessage!,
messageTheme: StreamChatTheme.of(context).otherMessageTheme, messageTheme: _streamChatTheme.otherMessageTheme,
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
); );
} }
@@ -1568,9 +1560,7 @@ class MessageInputState extends State<MessageInput> {
); );
} }
Widget _buildRemoveButton(Attachment attachment) { Widget _buildRemoveButton(Attachment attachment) => SizedBox(
final chatThemeData = StreamChatTheme.of(context);
return SizedBox(
height: 24, height: 24,
width: 24, width: 24,
child: RawMaterialButton( child: RawMaterialButton(
@@ -1584,16 +1574,15 @@ class MessageInputState extends State<MessageInput> {
onPressed: () { onPressed: () {
setState(() => _attachments.remove(attachment.id)); setState(() => _attachments.remove(attachment.id));
}, },
fillColor: chatThemeData.colorTheme.black.withOpacity(.5), fillColor: _streamChatTheme.colorTheme.black.withOpacity(.5),
child: Center( child: Center(
child: StreamSvgIcon.close( child: StreamSvgIcon.close(
size: 24, size: 24,
color: chatThemeData.colorTheme.white, color: _streamChatTheme.colorTheme.white,
), ),
), ),
), ),
); );
}
Widget _buildAttachment(Attachment attachment) { Widget _buildAttachment(Attachment attachment) {
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
@@ -1623,18 +1612,15 @@ class MessageInputState extends State<MessageInput> {
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (_, obj, trace) => errorWidget: (_, obj, trace) =>
getFileTypeImage(attachment.extraData['other'] as String?), getFileTypeImage(attachment.extraData['other'] as String?),
progressIndicatorBuilder: (context, _, progress) { placeholder: (context, _) => Shimmer.fromColors(
final chatThemeData = StreamChatTheme.of(context); baseColor: _streamChatTheme.colorTheme.greyGainsboro,
return Shimmer.fromColors( highlightColor: _streamChatTheme.colorTheme.whiteSmoke,
baseColor: chatThemeData.colorTheme.greyGainsboro,
highlightColor: chatThemeData.colorTheme.whiteSmoke,
child: Image.asset( child: Image.asset(
'images/placeholder.png', 'images/placeholder.png',
fit: BoxFit.cover, fit: BoxFit.cover,
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
), ),
); ),
},
); );
case 'video': case 'video':
return Stack( return Stack(
@@ -1666,14 +1652,13 @@ class MessageInputState extends State<MessageInput> {
Widget _buildCommandButton() { Widget _buildCommandButton() {
final s = textEditingController.text.trim(); final s = textEditingController.text.trim();
final chatThemeData = StreamChatTheme.of(context);
return IconButton( return IconButton(
icon: StreamSvgIcon.lightning( icon: StreamSvgIcon.lightning(
color: s.isNotEmpty color: s.isNotEmpty
? chatThemeData.colorTheme.greyGainsboro ? _streamChatTheme.colorTheme.greyGainsboro
: (_commandsOverlay != null : (_commandsOverlay != null
? chatThemeData.messageInputTheme.actionButtonColor ? _streamChatTheme.messageInputTheme.actionButtonColor
: chatThemeData.messageInputTheme.actionButtonIdleColor), : _streamChatTheme.messageInputTheme.actionButtonIdleColor),
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
@@ -1708,13 +1693,11 @@ class MessageInputState extends State<MessageInput> {
); );
} }
Widget _buildAttachmentButton() { Widget _buildAttachmentButton() => IconButton(
final chatThemeData = StreamChatTheme.of(context);
return IconButton(
icon: StreamSvgIcon.attach( icon: StreamSvgIcon.attach(
color: _openFilePickerSection color: _openFilePickerSection
? chatThemeData.messageInputTheme.actionButtonColor ? _streamChatTheme.messageInputTheme.actionButtonColor
: chatThemeData.messageInputTheme.actionButtonIdleColor, : _streamChatTheme.messageInputTheme.actionButtonIdleColor,
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
@@ -1741,7 +1724,6 @@ class MessageInputState extends State<MessageInput> {
} }
}, },
); );
}
/// Show the attachment modal, making the user choose where to /// Show the attachment modal, making the user choose where to
/// pick a media from /// pick a media from
@@ -1948,8 +1930,7 @@ class MessageInputState extends State<MessageInput> {
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: StreamSvgIcon( child: StreamSvgIcon(
assetName: _getIdleSendIcon(), assetName: _getIdleSendIcon(),
color: color: _streamChatTheme.messageInputTheme.sendButtonIdleColor,
StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor,
), ),
); );
@@ -1965,8 +1946,7 @@ class MessageInputState extends State<MessageInput> {
), ),
icon: StreamSvgIcon( icon: StreamSvgIcon(
assetName: _getSendIcon(), assetName: _getSendIcon(),
color: color: _streamChatTheme.messageInputTheme.sendButtonColor,
StreamChatTheme.of(context).messageInputTheme.sendButtonColor,
), ),
), ),
); );
@@ -2083,9 +2063,8 @@ class MessageInputState extends State<MessageInput> {
StreamSubscription? _keyboardListener; StreamSubscription? _keyboardListener;
void _showErrorAlert(String description) { void _showErrorAlert(String description) {
final chatThemeData = StreamChatTheme.of(context);
showModalBottomSheet( showModalBottomSheet(
backgroundColor: chatThemeData.colorTheme.white, backgroundColor: _streamChatTheme.colorTheme.white,
context: context, context: context,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@@ -2099,7 +2078,7 @@ class MessageInputState extends State<MessageInput> {
height: 26, height: 26,
), ),
StreamSvgIcon.error( StreamSvgIcon.error(
color: chatThemeData.colorTheme.accentRed, color: _streamChatTheme.colorTheme.accentRed,
size: 24, size: 24,
), ),
const SizedBox( const SizedBox(
@@ -2107,7 +2086,7 @@ class MessageInputState extends State<MessageInput> {
), ),
Text( Text(
'Something went wrong', 'Something went wrong',
style: chatThemeData.textTheme.headlineBold, style: _streamChatTheme.textTheme.headlineBold,
), ),
const SizedBox( const SizedBox(
height: 7, height: 7,
@@ -2123,7 +2102,7 @@ class MessageInputState extends State<MessageInput> {
height: 36, height: 36,
), ),
Container( Container(
color: chatThemeData.colorTheme.black.withOpacity(.08), color: _streamChatTheme.colorTheme.black.withOpacity(.08),
height: 1, height: 1,
), ),
Row( Row(
@@ -2135,8 +2114,8 @@ class MessageInputState extends State<MessageInput> {
}, },
child: Text( child: Text(
'OK', 'OK',
style: chatThemeData.textTheme.bodyBold style: _streamChatTheme.textTheme.bodyBold
.copyWith(color: chatThemeData.colorTheme.accentBlue), .copyWith(color: _streamChatTheme.colorTheme.accentBlue),
), ),
), ),
], ],
@@ -2169,6 +2148,7 @@ class MessageInputState extends State<MessageInput> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
_streamChatTheme = StreamChatTheme.of(context);
if (widget.editMessage != null && !_initialized) { if (widget.editMessage != null && !_initialized) {
FocusScope.of(context).requestFocus(_focusNode); FocusScope.of(context).requestFocus(_focusNode);
_initialized = true; _initialized = true;
@@ -2233,6 +2213,7 @@ class _PickerWidget extends StatefulWidget {
required this.selectedMedias, required this.selectedMedias,
required this.onAddMoreFilesClick, required this.onAddMoreFilesClick,
required this.onMediaSelected, required this.onMediaSelected,
required this.streamChatTheme,
}) : super(key: key); }) : super(key: key);
final int filePickerIndex; final int filePickerIndex;
@@ -2240,6 +2221,7 @@ class _PickerWidget extends StatefulWidget {
final List<String> selectedMedias; final List<String> selectedMedias;
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
final void Function(AssetEntity) onMediaSelected; final void Function(AssetEntity) onMediaSelected;
final StreamChatThemeData streamChatTheme;
@override @override
__PickerWidgetState createState() => __PickerWidgetState(); __PickerWidgetState createState() => __PickerWidgetState();
@@ -2266,7 +2248,6 @@ class __PickerWidgetState extends State<_PickerWidget> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
final chatThemeData = StreamChatTheme.of(context);
if (snapshot.data!) { if (snapshot.data!) {
if (widget.containsFile) { if (widget.containsFile) {
return GestureDetector( return GestureDetector(
@@ -2275,12 +2256,12 @@ class __PickerWidgetState extends State<_PickerWidget> {
}, },
child: Container( child: Container(
constraints: const BoxConstraints.expand(), constraints: const BoxConstraints.expand(),
color: chatThemeData.colorTheme.whiteSmoke, color: widget.streamChatTheme.colorTheme.whiteSmoke,
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
'Add more files', 'Add more files',
style: TextStyle( style: TextStyle(
color: chatThemeData.colorTheme.accentBlue, color: widget.streamChatTheme.colorTheme.accentBlue,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -2298,7 +2279,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
PhotoManager.openSetting(); PhotoManager.openSetting();
}, },
child: Container( child: Container(
color: chatThemeData.colorTheme.whiteSmoke, color: widget.streamChatTheme.colorTheme.whiteSmoke,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -2307,21 +2288,21 @@ class __PickerWidgetState extends State<_PickerWidget> {
'svgs/icon_picture_empty_state.svg', 'svgs/icon_picture_empty_state.svg',
package: 'stream_chat_flutter', package: 'stream_chat_flutter',
height: 140, height: 140,
color: chatThemeData.colorTheme.greyGainsboro, color: widget.streamChatTheme.colorTheme.greyGainsboro,
), ),
Text( Text(
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
'Please enable access to your photos \nand videos so you can share them with friends.', 'Please enable access to your photos \nand videos so you can share them with friends.',
style: chatThemeData.textTheme.body style: widget.streamChatTheme.textTheme.body.copyWith(
.copyWith(color: chatThemeData.colorTheme.grey), color: widget.streamChatTheme.colorTheme.grey),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Center( Center(
child: Text( child: Text(
'Allow access to your gallery', 'Allow access to your gallery',
style: chatThemeData.textTheme.bodyBold.copyWith( style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentBlue, color: widget.streamChatTheme.colorTheme.accentBlue,
), ),
), ),
), ),
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
@@ -281,8 +282,10 @@ class _MessageListViewState extends State<MessageListView> {
void Function(Message)? _onThreadTap; void Function(Message)? _onThreadTap;
bool _showScrollToBottom = false; bool _showScrollToBottom = false;
late final ItemPositionsListener _itemPositionListener; late final ItemPositionsListener _itemPositionListener;
late final Stream<Iterable<ItemPosition>> _itemPositionStream;
int? _messageListLength; int? _messageListLength;
StreamChannelState? streamChannel; StreamChannelState? streamChannel;
late StreamChatThemeData _streamTheme;
int? get _initialIndex { int? get _initialIndex {
if (widget.initialScrollIndex != null) return widget.initialScrollIndex; if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
@@ -324,9 +327,7 @@ class _MessageListViewState extends State<MessageListView> {
final MessageListController _messageListController = MessageListController(); final MessageListController _messageListController = MessageListController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => MessageListCore(
final chatThemeData = StreamChatTheme.of(context);
return MessageListCore(
messageFilter: widget.messageFilter, messageFilter: widget.messageFilter,
loadingBuilder: widget.loadingBuilder ?? loadingBuilder: widget.loadingBuilder ??
(context) => const Center( (context) => const Center(
@@ -336,12 +337,12 @@ class _MessageListViewState extends State<MessageListView> {
(context) => Center( (context) => Center(
child: Text( child: Text(
'No chats here yet...', 'No chats here yet...',
style: chatThemeData.textTheme.footnote.copyWith( style: _streamTheme.textTheme.footnote.copyWith(
color: chatThemeData.colorTheme.black.withOpacity(.5)), color: _streamTheme.colorTheme.black.withOpacity(.5)),
), ),
), ),
messageListBuilder: messageListBuilder: widget.messageListBuilder ??
widget.messageListBuilder ?? (context, list) => _buildListView(list), (context, list) => _buildListView(list),
messageListController: _messageListController, messageListController: _messageListController,
parentMessage: widget.parentMessage, parentMessage: widget.parentMessage,
showScrollToBottom: widget.showScrollToBottom, showScrollToBottom: widget.showScrollToBottom,
@@ -349,12 +350,11 @@ class _MessageListViewState extends State<MessageListView> {
(BuildContext context, Object error) => Center( (BuildContext context, Object error) => Center(
child: Text( child: Text(
'Something went wrong', 'Something went wrong',
style: chatThemeData.textTheme.footnote.copyWith( style: _streamTheme.textTheme.footnote.copyWith(
color: chatThemeData.colorTheme.black.withOpacity(.5)), color: _streamTheme.colorTheme.black.withOpacity(.5)),
), ),
), ),
); );
}
Widget _buildListView(List<Message> data) { Widget _buildListView(List<Message> data) {
messages = data; messages = data;
@@ -400,8 +400,7 @@ class _MessageListViewState extends State<MessageListView> {
} }
return InfoTile( return InfoTile(
// ignore: avoid_bool_literals_in_conditional_expressions showMessage: widget.showConnectionStateTile && showStatus,
showMessage: widget.showConnectionStateTile ? showStatus : false,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
message: statusString, message: statusString,
@@ -448,10 +447,9 @@ class _MessageListViewState extends State<MessageListView> {
if (i == 0) return const SizedBox(height: 30); if (i == 0) return const SizedBox(height: 30);
if (i == messages.length + 1) { if (i == messages.length + 1) {
final replyCount = widget.parentMessage!.replyCount; final replyCount = widget.parentMessage!.replyCount;
final chatThemeData = StreamChatTheme.of(context);
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: chatThemeData.colorTheme.bgGradient, gradient: _streamTheme.colorTheme.bgGradient,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -459,7 +457,7 @@ class _MessageListViewState extends State<MessageListView> {
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: chatThemeData style: _streamTheme
.channelTheme.channelHeaderTheme.subtitle, .channelTheme.channelHeaderTheme.subtitle,
), ),
), ),
@@ -571,9 +569,18 @@ class _MessageListViewState extends State<MessageListView> {
if (widget.showScrollToBottom) _buildScrollToBottom(), if (widget.showScrollToBottom) _buildScrollToBottom(),
Positioned( Positioned(
top: 20, top: 20,
child: ValueListenableBuilder<Iterable<ItemPosition>>( child: BetterStreamBuilder<Iterable<ItemPosition>>(
valueListenable: _itemPositionListener.itemPositions, initialData: _itemPositionListener.itemPositions.value,
builder: (context, values, _) { stream: _itemPositionStream,
comparator: (a, b) {
if (a == null) {
return false;
}
final aTop = _getTopElement(a)?.index;
final bTop = _getTopElement(b)?.index;
return aTop == bTop;
},
builder: (context, values) {
final items = _itemPositionListener.itemPositions.value; final items = _itemPositionListener.itemPositions.value;
if (items.isEmpty || messages.isEmpty) { if (items.isEmpty || messages.isEmpty) {
return const SizedBox(); return const SizedBox();
@@ -640,7 +647,6 @@ class _MessageListViewState extends State<MessageListView> {
final showUnreadCount = unreadCount > 0 && final showUnreadCount = unreadCount > 0 &&
streamChannel!.channel.state!.members.any((e) => streamChannel!.channel.state!.members.any((e) =>
e.userId == streamChannel!.channel.client.state.user!.id); e.userId == streamChannel!.channel.client.state.user!.id);
final chatThemeData = StreamChatTheme.of(context);
return Positioned( return Positioned(
bottom: 8, bottom: 8,
right: 8, right: 8,
@@ -650,7 +656,7 @@ class _MessageListViewState extends State<MessageListView> {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
FloatingActionButton( FloatingActionButton(
backgroundColor: chatThemeData.colorTheme.white, backgroundColor: _streamTheme.colorTheme.white,
onPressed: () { onPressed: () {
if (unreadCount > 0) { if (unreadCount > 0) {
streamChannel!.channel.markRead(); streamChannel!.channel.markRead();
@@ -669,7 +675,7 @@ class _MessageListViewState extends State<MessageListView> {
} }
}, },
child: StreamSvgIcon.down( child: StreamSvgIcon.down(
color: chatThemeData.colorTheme.black, color: _streamTheme.colorTheme.black,
), ),
), ),
if (showUnreadCount) if (showUnreadCount)
@@ -700,39 +706,13 @@ class _MessageListViewState extends State<MessageListView> {
Widget _buildLoadingIndicator( Widget _buildLoadingIndicator(
StreamChannelState streamChannel, StreamChannelState streamChannel,
QueryDirection direction, QueryDirection direction,
) { ) =>
final stream = direction == QueryDirection.top _LoadingIndicator(
? streamChannel.queryTopMessages direction: direction,
: streamChannel.queryBottomMessages; streamTheme: _streamTheme,
return BetterStreamBuilder<bool>( streamChannel: streamChannel,
key: Key('LOADING-INDICATOR $direction'), isThreadConversation: _isThreadConversation,
stream: stream,
initialData: false,
errorBuilder: (context, error) => Container(
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.2),
child: const Center(
child: Text('Error loading messages'),
),
),
builder: (context, snapshot) {
if (!snapshot) {
if (!_isThreadConversation && direction == QueryDirection.top) {
return const SizedBox(
height: 52,
width: double.infinity,
); );
}
return const Offstage();
}
return const Center(
child: Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
),
);
},
);
}
Widget _buildTopMessage( Widget _buildTopMessage(
BuildContext context, BuildContext context,
@@ -818,7 +798,6 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember = final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id); members.firstWhere((e) => e.user!.id == currentUser!.id);
final chatThemeData = StreamChatTheme.of(context);
return MessageWidget( return MessageWidget(
showReplyMessage: false, showReplyMessage: false,
showResendMessage: false, showResendMessage: false,
@@ -847,8 +826,8 @@ class _MessageListViewState extends State<MessageListView> {
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
messageTheme: isMyMessage messageTheme: isMyMessage
? chatThemeData.ownMessageTheme ? _streamTheme.ownMessageTheme
: chatThemeData.otherMessageTheme, : _streamTheme.otherMessageTheme,
onShowMessage: widget.onShowMessage, onShowMessage: widget.onShowMessage,
onReturnAction: (action) { onReturnAction: (action) {
switch (action) { switch (action) {
@@ -962,7 +941,6 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember = final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id); members.firstWhere((e) => e.user!.id == currentUser!.id);
final chatThemeData = StreamChatTheme.of(context);
Widget child = MessageWidget( Widget child = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('MESSAGE-${message.id}'),
message: message, message: message,
@@ -1049,8 +1027,8 @@ class _MessageListViewState extends State<MessageListView> {
horizontal: isOnlyEmoji ? 0 : 16.0, horizontal: isOnlyEmoji ? 0 : 16.0,
), ),
messageTheme: isMyMessage messageTheme: isMyMessage
? chatThemeData.ownMessageTheme ? _streamTheme.ownMessageTheme
: chatThemeData.otherMessageTheme, : _streamTheme.otherMessageTheme,
readList: readList, readList: readList,
allRead: allRead, allRead: allRead,
onShowMessage: widget.onShowMessage, onShowMessage: widget.onShowMessage,
@@ -1091,7 +1069,7 @@ class _MessageListViewState extends State<MessageListView> {
widget.onMessageSwiped?.call(message); widget.onMessageSwiped?.call(message);
}, },
backgroundIcon: StreamSvgIcon.reply( backgroundIcon: StreamSvgIcon.reply(
color: chatThemeData.colorTheme.accentBlue, color: _streamTheme.colorTheme.accentBlue,
), ),
child: child, child: child,
), ),
@@ -1101,7 +1079,7 @@ class _MessageListViewState extends State<MessageListView> {
if (!initialMessageHighlightComplete && if (!initialMessageHighlightComplete &&
widget.highlightInitialMessage && widget.highlightInitialMessage &&
_isInitialMessage(message.id)) { _isInitialMessage(message.id)) {
final colorTheme = chatThemeData.colorTheme; final colorTheme = _streamTheme.colorTheme;
final highlightColor = final highlightColor =
widget.messageHighlightColor ?? colorTheme.highlight; widget.messageHighlightColor ?? colorTheme.highlight;
child = TweenAnimationBuilder<Color?>( child = TweenAnimationBuilder<Color?>(
@@ -1131,6 +1109,8 @@ class _MessageListViewState extends State<MessageListView> {
_scrollController = widget.scrollController ?? ItemScrollController(); _scrollController = widget.scrollController ?? ItemScrollController();
_itemPositionListener = _itemPositionListener =
widget.itemPositionListener ?? ItemPositionsListener.create(); widget.itemPositionListener ?? ItemPositionsListener.create();
_itemPositionStream =
valueListenableToStreamAdapter(_itemPositionListener.itemPositions);
_getOnThreadTap(); _getOnThreadTap();
super.initState(); super.initState();
@@ -1139,6 +1119,7 @@ class _MessageListViewState extends State<MessageListView> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
final newStreamChannel = StreamChannel.of(context); final newStreamChannel = StreamChannel.of(context);
_streamTheme = StreamChatTheme.of(context);
if (newStreamChannel != streamChannel) { if (newStreamChannel != streamChannel) {
streamChannel = newStreamChannel; streamChannel = newStreamChannel;
@@ -1209,3 +1190,78 @@ class _MessageListViewState extends State<MessageListView> {
super.dispose(); super.dispose();
} }
} }
class _LoadingIndicator extends StatelessWidget {
const _LoadingIndicator({
Key? key,
required this.streamTheme,
required this.isThreadConversation,
required this.direction,
required this.streamChannel,
}) : super(key: key);
final StreamChatThemeData streamTheme;
final bool isThreadConversation;
final QueryDirection direction;
final StreamChannelState streamChannel;
@override
Widget build(BuildContext context) {
final stream = direction == QueryDirection.top
? streamChannel.queryTopMessages
: streamChannel.queryBottomMessages;
return BetterStreamBuilder<bool>(
key: Key('LOADING-INDICATOR $direction'),
stream: stream,
initialData: false,
errorBuilder: (context, error) => Container(
color: streamTheme.colorTheme.accentRed.withOpacity(.2),
child: const Center(
child: Text('Error loading messages'),
),
),
builder: (context, snapshot) {
if (!snapshot) {
if (!isThreadConversation && direction == QueryDirection.top) {
return const SizedBox(
height: 52,
width: double.infinity,
);
}
return const Offstage();
}
return const Center(
child: Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
),
);
},
);
}
}
Stream<T> valueListenableToStreamAdapter<T>(ValueListenable<T> listenable) {
late StreamController<T> controller;
void listener() {
controller.add(listenable.value);
}
void start() {
listenable.addListener(listener);
}
void end() {
listenable.removeListener(listener);
}
controller = StreamController<T>(
onListen: start,
onPause: end,
onResume: start,
onCancel: end,
);
return controller.stream;
}
@@ -77,33 +77,10 @@ class MessageReactionsModal extends StatelessWidget {
final divFactor = message.attachments.isNotEmpty == true final divFactor = message.attachments.isNotEmpty == true
? 1 ? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutBack,
builder: (context, val, snapshot) {
final hasFileAttachment = final hasFileAttachment =
message.attachments.any((it) => it.type == 'file') == true; message.attachments.any((it) => it.type == 'file') == true;
return GestureDetector(
behavior: HitTestBehavior.translucent, final child = Center(
onTap: () => Navigator.maybePop(context),
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
color: StreamChatTheme.of(context).colorTheme.overlay,
),
),
),
Transform.scale(
scale: val,
child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -116,12 +93,8 @@ class MessageReactionsModal extends StatelessWidget {
Align( Align(
alignment: Alignment( alignment: Alignment(
user!.id == message.user!.id user!.id == message.user!.id
? (divFactor >= 1.0 ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor))
? -0.2 : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)),
: (1.2 - divFactor))
: (divFactor >= 1.0
? 0.2
: -(1.2 - divFactor)),
0), 0),
child: ReactionPicker( child: ReactionPicker(
message: message, message: message,
@@ -147,17 +120,14 @@ class MessageReactionsModal extends StatelessWidget {
shape: messageShape, shape: messageShape,
attachmentShape: attachmentShape, attachmentShape: attachmentShape,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry
attachmentBorderRadiusGeometry ?.mirrorBorderIfReversed(reverse: !reverse),
?.mirrorBorderIfReversed(
reverse: !reverse),
attachmentPadding: EdgeInsets.all( attachmentPadding: EdgeInsets.all(
hasFileAttachment ? 4 : 2, hasFileAttachment ? 4 : 2,
), ),
textPadding: EdgeInsets.symmetric( textPadding: EdgeInsets.symmetric(
vertical: 8, vertical: 8,
horizontal: horizontal: message.text!.isOnlyEmoji ? 0 : 16.0,
message.text!.isOnlyEmoji ? 0 : 16.0,
), ),
showReactionPickerIndicator: showReactions && showReactionPickerIndicator: showReactions &&
(message.status == MessageSendingStatus.sent), (message.status == MessageSendingStatus.sent),
@@ -167,23 +137,51 @@ class MessageReactionsModal extends StatelessWidget {
), ),
if (message.latestReactions?.isNotEmpty == true) ...[ if (message.latestReactions?.isNotEmpty == true) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
_buildReactionCard(context), _buildReactionCard(
context,
user,
),
] ]
], ],
), ),
), ),
), ),
);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context),
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
), ),
child: DecoratedBox(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.overlay,
),
),
),
),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutBack,
builder: (context, val, widget) => Transform.scale(
scale: val,
child: widget,
),
child: child,
), ),
], ],
), ),
); );
},
);
} }
Widget _buildReactionCard(BuildContext context) { Widget _buildReactionCard(BuildContext context, User? user) {
final currentUser = StreamChat.of(context).user;
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
return Card( return Card(
color: chatThemeData.colorTheme.white, color: chatThemeData.colorTheme.white,
@@ -210,7 +208,7 @@ class MessageReactionsModal extends StatelessWidget {
children: message.latestReactions! children: message.latestReactions!
.map((e) => _buildReaction( .map((e) => _buildReaction(
e, e,
currentUser!, user!,
context, context,
)) ))
.toList(), .toList(),
@@ -413,37 +413,39 @@ class _MessageWidgetState extends State<MessageWidget>
bool get showTimeStamp => widget.showTimestamp; bool get showTimeStamp => widget.showTimestamp;
bool get isMessageRead => widget.readList?.isNotEmpty == true; late final bool isMessageRead = widget.readList?.isNotEmpty == true;
bool get showInChannel => widget.showInChannelIndicator; bool get showInChannel => widget.showInChannelIndicator;
bool get hasQuotedMessage => widget.message.quotedMessage != null; bool get hasQuotedMessage => widget.message.quotedMessage != null;
bool get isSendFailed => widget.message.status == MessageSendingStatus.failed; late final bool isSendFailed =
widget.message.status == MessageSendingStatus.failed;
bool get isUpdateFailed => late final bool isUpdateFailed =
widget.message.status == MessageSendingStatus.failed_update; widget.message.status == MessageSendingStatus.failed_update;
bool get isDeleteFailed => late final bool isDeleteFailed =
widget.message.status == MessageSendingStatus.failed_delete; widget.message.status == MessageSendingStatus.failed_delete;
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed; late final bool isFailedState =
isSendFailed || isUpdateFailed || isDeleteFailed;
bool get isGiphy => late final bool isGiphy =
widget.message.attachments.any((element) => element.type == 'giphy') == widget.message.attachments.any((element) => element.type == 'giphy') ==
true; true;
bool get hasNonUrlAttachments => late final bool isOnlyEmoji = widget.message.text?.isOnlyEmoji == true;
widget.message.attachments
late final bool hasNonUrlAttachments = widget.message.attachments
.where((it) => it.ogScrapeUrl == null) .where((it) => it.ogScrapeUrl == null)
.isNotEmpty == .isNotEmpty ==
true; true;
bool get hasUrlAttachments => late final bool hasUrlAttachments =
widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true; widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true;
bool get showBottomRow => late final bool showBottomRow = showThreadReplyIndicator ||
showThreadReplyIndicator ||
showUsername || showUsername ||
showTimeStamp || showTimeStamp ||
showInChannel || showInChannel ||
@@ -453,6 +455,9 @@ class _MessageWidgetState extends State<MessageWidget>
@override @override
bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true; bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true;
late StreamChatThemeData _streamChatTheme;
late StreamChatState _streamChat;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
@@ -466,7 +471,7 @@ class _MessageWidgetState extends State<MessageWidget>
? MaterialType.card ? MaterialType.card
: MaterialType.transparency, : MaterialType.transparency,
color: widget.message.pinned && widget.showPinHighlight color: widget.message.pinned && widget.showPinHighlight
? StreamChatTheme.of(context).colorTheme.highlight ? _streamChatTheme.colorTheme.highlight
: null, : null,
child: Portal( child: Portal(
child: InkWell( child: InkWell(
@@ -527,7 +532,8 @@ class _MessageWidgetState extends State<MessageWidget>
transform: Matrix4.translationValues( transform: Matrix4.translationValues(
widget.reverse ? 12 : -12, 0, 0), widget.reverse ? 12 : -12, 0, 0),
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: 22 * 6.0), maxWidth: 22 * 6.0,
),
child: _buildReactionIndicator(context), child: _buildReactionIndicator(context),
), ),
portalAnchor: portalAnchor:
@@ -572,7 +578,7 @@ class _MessageWidgetState extends State<MessageWidget>
), ),
) )
: Card( : Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
elevation: 0, elevation: 0,
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
horizontal: (isFailedState horizontal: (isFailedState
@@ -626,9 +632,8 @@ class _MessageWidgetState extends State<MessageWidget>
top: -8, top: -8,
child: CustomPaint( child: CustomPaint(
painter: ReactionBubblePainter( painter: ReactionBubblePainter(
StreamChatTheme.of(context) _streamChatTheme
.colorTheme .colorTheme.white,
.white,
Colors.transparent, Colors.transparent,
Colors.transparent, Colors.transparent,
tailCirclesSpace: 1, tailCirclesSpace: 1,
@@ -673,14 +678,20 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
@override
void didChangeDependencies() {
_streamChatTheme = StreamChatTheme.of(context);
_streamChat = StreamChat.of(context);
super.didChangeDependencies();
}
Widget _buildQuotedMessage() { Widget _buildQuotedMessage() {
final isMyMessage = final isMyMessage = widget.message.user?.id == _streamChat.user?.id;
widget.message.user?.id == StreamChat.of(context).user?.id;
final onTap = widget.message.quotedMessage?.isDeleted != true && final onTap = widget.message.quotedMessage?.isDeleted != true &&
widget.onQuotedMessageTap != null widget.onQuotedMessageTap != null
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
: null; : null;
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = _streamChatTheme;
return QuotedMessageWidget( return QuotedMessageWidget(
onTap: onTap, onTap: onTap,
message: widget.message.quotedMessage!, message: widget.message.quotedMessage!,
@@ -695,7 +706,7 @@ class _MessageWidgetState extends State<MessageWidget>
Widget get _bottomRow { Widget get _bottomRow {
if (isDeleted) { if (isDeleted) {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = _streamChatTheme;
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -849,36 +860,16 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
Widget _buildThreadParticipantsIndicator(Iterable<User> threadParticipants) { Widget _buildThreadParticipantsIndicator(Iterable<User> threadParticipants) =>
var padding = 0.0; _ThreadParticipants(
return Stack( streamChatTheme: _streamChatTheme,
children: threadParticipants.map((user) { threadParticipants: threadParticipants,
padding += 8.0;
return Positioned(
right: padding - 8,
bottom: 0,
top: 0,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: StreamChatTheme.of(context).colorTheme.white,
),
padding: const EdgeInsets.all(1),
child: UserAvatar(
user: user,
constraints: BoxConstraints.loose(const Size.fromRadius(7)),
showOnlineStatus: false,
),
),
); );
}).toList(),
);
}
Widget _buildReactionIndicator( Widget _buildReactionIndicator(
BuildContext context, BuildContext context,
) { ) {
final ownId = StreamChat.of(context).user!.id; final ownId = _streamChat.user!.id;
final reactionsMap = <String, Reaction>{}; final reactionsMap = <String, Reaction>{};
widget.message.latestReactions?.forEach((element) { widget.message.latestReactions?.forEach((element) {
if (!reactionsMap.containsKey(element.type) || if (!reactionsMap.containsKey(element.type) ||
@@ -918,7 +909,7 @@ class _MessageWidgetState extends State<MessageWidget>
showDialog( showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay, barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageActionsModal( child: MessageActionsModal(
@@ -969,7 +960,7 @@ class _MessageWidgetState extends State<MessageWidget>
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
showDialog( showDialog(
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay, barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageReactionsModal( child: MessageReactionsModal(
@@ -1000,7 +991,7 @@ class _MessageWidgetState extends State<MessageWidget>
side: hasFiles side: hasFiles
? widget.attachmentBorderSide ?? ? widget.attachmentBorderSide ??
BorderSide( BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: _streamChatTheme.colorTheme.greyWhisper,
) )
: BorderSide.none, : BorderSide.none,
borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero, borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero,
@@ -1010,7 +1001,7 @@ class _MessageWidgetState extends State<MessageWidget>
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder( ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
side: widget.borderSide ?? side: widget.borderSide ??
BorderSide( BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: _streamChatTheme.colorTheme.greyWhisper,
), ),
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
); );
@@ -1101,7 +1092,7 @@ class _MessageWidgetState extends State<MessageWidget>
Text( Text(
widget.readList!.length.toString(), widget.readList!.length.toString(),
style: style.copyWith( style: style.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue, color: _streamChatTheme.colorTheme.accentBlue,
), ),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
@@ -1158,7 +1149,7 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildPinnedMessage(Message message) { Widget _buildPinnedMessage(Message message) {
final pinnedBy = message.pinnedBy; final pinnedBy = message.pinnedBy;
final pinnedByMe = StreamChat.of(context).user!.id == pinnedBy!.id; final pinnedByMe = _streamChat.user!.id == pinnedBy!.id;
return Padding( return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8),
@@ -1174,7 +1165,7 @@ class _MessageWidgetState extends State<MessageWidget>
Text( Text(
'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}', 'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.grey, color: _streamChatTheme.colorTheme.grey,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
@@ -1184,9 +1175,7 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
bool get isOnlyEmoji => widget.message.text!.isOnlyEmoji; late final bool isPinned = widget.message.pinned;
bool get isPinned => widget.message.pinned;
Color? _getBackgroundColor() { Color? _getBackgroundColor() {
if (hasQuotedMessage) { if (hasQuotedMessage) {
@@ -1194,7 +1183,7 @@ class _MessageWidgetState extends State<MessageWidget>
} }
if (hasUrlAttachments) { if (hasUrlAttachments) {
return StreamChatTheme.of(context).colorTheme.blueAlice; return _streamChatTheme.colorTheme.blueAlice;
} }
if (isOnlyEmoji) { if (isOnlyEmoji) {
@@ -1226,6 +1215,45 @@ class _MessageWidgetState extends State<MessageWidget>
} }
} }
class _ThreadParticipants extends StatelessWidget {
const _ThreadParticipants({
Key? key,
required StreamChatThemeData streamChatTheme,
required this.threadParticipants,
}) : _streamChatTheme = streamChatTheme,
super(key: key);
final StreamChatThemeData _streamChatTheme;
final Iterable<User> threadParticipants;
@override
Widget build(BuildContext context) {
var padding = 0.0;
return Stack(
children: threadParticipants.map((user) {
padding += 8.0;
return Positioned(
right: padding - 8,
bottom: 0,
top: 0,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _streamChatTheme.colorTheme.white,
),
padding: const EdgeInsets.all(1),
child: UserAvatar(
user: user,
constraints: BoxConstraints.loose(const Size.fromRadius(7)),
showOnlineStatus: false,
),
),
);
}).toList(),
);
}
}
class _ThreadReplyPainter extends CustomPainter { class _ThreadReplyPainter extends CustomPainter {
const _ThreadReplyPainter({ const _ThreadReplyPainter({
this.context, this.context,
@@ -1,9 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.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:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget builder for quoted message attachment thumnail /// Widget builder for quoted message attachment thumnail
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
@@ -217,7 +217,7 @@ class QuotedMessageWidget extends StatelessWidget {
} }
child = AbsorbPointer(child: child); child = AbsorbPointer(child: child);
return Material( return Material(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
type: MaterialType.transparency, type: MaterialType.transparency,
shape: attachment.type == 'file' ? null : _getDefaultShape(context), shape: attachment.type == 'file' ? null : _getDefaultShape(context),
child: child, child: child,
@@ -1,5 +1,3 @@
import 'dart:math';
import 'package:ezanimation/ezanimation.dart'; import 'package:ezanimation/ezanimation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
@@ -50,13 +48,7 @@ class _ReactionPickerState extends State<ReactionPicker>
triggerAnimations(); triggerAnimations();
} }
return TweenAnimationBuilder<double>( final child = Material(
tween: Tween(begin: 0, end: 1),
curve: Curves.easeInOutBack,
duration: const Duration(milliseconds: 500),
builder: (context, val, wid) => Transform.scale(
scale: val,
child: Material(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
color: chatThemeData.colorTheme.white, color: chatThemeData.colorTheme.white,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
@@ -72,11 +64,18 @@ class _ReactionPickerState extends State<ReactionPicker>
children: reactionIcons children: reactionIcons
.map<Widget>((reactionIcon) { .map<Widget>((reactionIcon) {
final ownReactionIndex = widget.message.ownReactions final ownReactionIndex = widget.message.ownReactions
?.indexWhere((reaction) => ?.indexWhere(
reaction.type == reactionIcon.type) ?? (reaction) => reaction.type == reactionIcon.type) ??
-1; -1;
final index = reactionIcons.indexOf(reactionIcon); final index = reactionIcons.indexOf(reactionIcon);
final child = StreamSvgIcon(
assetName: reactionIcon.assetName,
color: ownReactionIndex != -1
? chatThemeData.colorTheme.accentBlue
: Theme.of(context).iconTheme.color!.withOpacity(.5),
);
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 24, height: 24,
@@ -95,8 +94,7 @@ class _ReactionPickerState extends State<ReactionPicker>
if (ownReactionIndex != -1) { if (ownReactionIndex != -1) {
removeReaction( removeReaction(
context, context,
widget.message widget.message.ownReactions![ownReactionIndex],
.ownReactions![ownReactionIndex],
); );
} else { } else {
sendReaction( sendReaction(
@@ -107,27 +105,12 @@ class _ReactionPickerState extends State<ReactionPicker>
}, },
child: AnimatedBuilder( child: AnimatedBuilder(
animation: animations[index], animation: animations[index],
builder: (context, val) => Transform.scale( builder: (context, child) => Transform.scale(
scale: animations[index].value, scale: animations[index].value,
child: StreamSvgIcon( child: child,
assetName: reactionIcon.assetName,
height: max(
0,
animations[index].value * 24.0,
), ),
width: max( child: child,
0,
animations[index].value * 24.0,
), ),
color: ownReactionIndex != -1
? chatThemeData
.colorTheme.accentBlue
: Theme.of(context)
.iconTheme
.color!
.withOpacity(.5),
),
)),
), ),
); );
}) })
@@ -137,8 +120,18 @@ class _ReactionPickerState extends State<ReactionPicker>
.toList(), .toList(),
), ),
), ),
);
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
curve: Curves.easeInOutBack,
duration: const Duration(milliseconds: 500),
builder: (context, val, widget) => Transform.scale(
scale: val,
child: widget,
), ),
)); child: child,
);
} }
void triggerAnimations() async { void triggerAnimations() async {
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget to show the current list of typing users /// Widget to show the current list of typing users
class TypingIndicator extends StatelessWidget { class TypingIndicator extends StatefulWidget {
/// Instantiate a new TypingIndicator /// Instantiate a new TypingIndicator
const TypingIndicator({ const TypingIndicator({
Key? key, Key? key,
@@ -30,16 +29,21 @@ class TypingIndicator extends StatelessWidget {
/// Alignment of the typing indicator /// Alignment of the typing indicator
final Alignment alignment; final Alignment alignment;
@override
_TypingIndicatorState createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<TypingIndicator> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channelState = final channelState =
channel?.state ?? StreamChannel.of(context).channel.state!; widget.channel?.state ?? StreamChannel.of(context).channel.state!;
final altWidget = Align( final altWidget = Align(
key: const Key('alternative'), key: const Key('alternative'),
alignment: alignment, alignment: widget.alignment,
child: Container( child: Container(
child: alternativeWidget ?? const Offstage(), child: widget.alternativeWidget ?? const Offstage(),
), ),
); );
return BetterStreamBuilder<List<User>>( return BetterStreamBuilder<List<User>>(
@@ -49,10 +53,11 @@ class TypingIndicator extends StatelessWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: snapshot.isNotEmpty == true child: snapshot.isNotEmpty == true
? Padding( ? Padding(
padding: padding, key: const Key('main'),
padding: widget.padding,
child: Align( child: Align(
key: const Key('typings'), key: const Key('typings'),
alignment: alignment, alignment: widget.alignment,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -65,7 +70,7 @@ class TypingIndicator extends StatelessWidget {
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing', ' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing',
maxLines: 1, maxLines: 1,
style: style, style: widget.style,
), ),
], ],
), ),
@@ -40,7 +40,7 @@ class UrlAttachment extends StatelessWidget {
children: [ children: [
if (urlAttachment.imageUrl != null) if (urlAttachment.imageUrl != null)
Container( Container(
clipBehavior: Clip.antiAliasWithSaveLayer, clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.symmetric(horizontal: 8), margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -334,7 +334,7 @@ Widget wrapAttachmentWidget(
bool reverse, bool reverse,
) => ) =>
Material( Material(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
shape: attachmentShape, shape: attachmentShape,
type: MaterialType.transparency, type: MaterialType.transparency,
child: attachmentWidget, child: attachmentWidget,
@@ -25,29 +25,31 @@ class BetterStreamBuilder<T> extends StatefulWidget {
} }
class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> { class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> {
Widget? _child;
T? _lastEvent; T? _lastEvent;
StreamSubscription? _subscription; StreamSubscription? _subscription;
Object? _lastError; Object? _lastError;
@override @override
Widget build(BuildContext context) => _child ?? const Offstage(); Widget build(BuildContext context) {
if (_lastError != null) {
return widget.errorBuilder!(context, _lastError!);
}
if (_lastEvent == null) {
return widget.loadingBuilder?.call(context) ?? const Offstage();
}
return widget.builder(context, _lastEvent ?? widget.initialData);
}
bool _firstTime = true; bool _firstTime = true;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
if (_firstTime) { if (_firstTime) {
if (widget.initialData == null && widget.loadingBuilder != null) {
_child = widget.loadingBuilder!(context);
} else {
_onEvent(widget.initialData);
}
_lastEvent = widget.initialData; _lastEvent = widget.initialData;
_subscription = widget.stream?.listen( _subscription = widget.stream?.listen(
_onEvent, _onEvent,
onError: _onError, onError: _onError,
); );
_firstTime = false; _firstTime = false;
} }
super.didChangeDependencies(); super.didChangeDependencies();
@@ -55,12 +57,6 @@ class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> {
@override @override
void didUpdateWidget(covariant BetterStreamBuilder<T> oldWidget) { void didUpdateWidget(covariant BetterStreamBuilder<T> oldWidget) {
if (_lastError != null && oldWidget.errorBuilder != widget.errorBuilder) {
_onError(_lastError);
} else if (oldWidget.builder != widget.builder) {
_onEvent(_lastEvent);
}
if (oldWidget.stream != widget.stream) { if (oldWidget.stream != widget.stream) {
_subscription?.cancel(); _subscription?.cancel();
_subscription = widget.stream?.listen( _subscription = widget.stream?.listen(
@@ -79,21 +75,22 @@ class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> {
void _onError(error) { void _onError(error) {
if (widget.errorBuilder != null && error != _lastError) { if (widget.errorBuilder != null && error != _lastError) {
setState(() { if (mounted) {
_child = widget.errorBuilder!(context, error); setState(() {});
}); }
_lastError = error; _lastError = error;
} }
} }
void _onEvent(event) { void _onEvent(T event) {
_lastError = null; _lastError = null;
if (widget.comparator != null final isEqual = widget.comparator != null
? widget.comparator!(_lastEvent, event) ? widget.comparator!(_lastEvent, event)
: event != _lastEvent) { : event == _lastEvent;
setState(() { if (!isEqual) {
_child = widget.builder(context, event); if (mounted) {
}); setState(() {});
}
_lastEvent = event; _lastEvent = event;
} }
} }
@@ -346,7 +346,10 @@ class StreamChannelState extends State<StreamChannel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = FutureBuilder<List<bool>>( var child = widget.child;
if (widget.showLoading &&
(initialMessageId != null || channel.state == null)) {
child = FutureBuilder<List<bool>>(
future: Future.wait(_futures), future: Future.wait(_futures),
initialData: [ initialData: [
channel.state != null, channel.state != null,
@@ -366,9 +369,8 @@ class StreamChannelState extends State<StreamChannel> {
return Center(child: Text(message)); return Center(child: Text(message));
} }
final initialized = snapshot.data![0]; final initialized = snapshot.data![0];
// ignore: avoid_bool_literals_in_conditional_expressions final dataLoaded = initialMessageId == null || snapshot.data![1];
final dataLoaded = initialMessageId == null ? true : snapshot.data![1]; if (!initialized || !dataLoaded) {
if (widget.showLoading && (!initialized || !dataLoaded)) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
@@ -376,6 +378,8 @@ class StreamChannelState extends State<StreamChannel> {
return widget.child; return widget.child;
}, },
); );
}
if (initialMessageId != null) { if (initialMessageId != null) {
child = Material(child: child); child = Material(child: child);
} }