Merge branch 'develop' into hotfix/messageHighlight
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
|
||||
- Fixed message highlight animation alignment in `MessageListView`
|
||||
|
||||
✅ Added
|
||||
|
||||
- `MessageListViewThemeData` now accepts a `DecorationImage` as a background image for `MessageListView`.
|
||||
|
||||
## 3.1.1
|
||||
|
||||
- Updated `stream_chat_flutter_core` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
@@ -63,6 +63,12 @@ class MyApp extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
messageListViewTheme: const MessageListViewThemeData(
|
||||
backgroundColor: Colors.grey,
|
||||
backgroundImage: DecorationImage(
|
||||
image: AssetImage('assets/background_doodle.png'),
|
||||
fit: BoxFit.cover,
|
||||
)),
|
||||
otherMessageTheme: MessageThemeData(
|
||||
messageBackgroundColor: colorTheme.textHighEmphasis,
|
||||
messageTextStyle: TextStyle(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: example
|
||||
name: stream_chat_flutter_example
|
||||
description: A new Flutter project.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
@@ -27,17 +27,9 @@ dependencies:
|
||||
cupertino_icons: ^1.0.3
|
||||
flutter:
|
||||
sdk: flutter
|
||||
# stream_chat:
|
||||
# path: ../../stream_chat
|
||||
# stream_chat_flutter_core:
|
||||
# path: ../../stream_chat_flutter_core
|
||||
stream_chat_flutter:
|
||||
path: ../
|
||||
stream_chat_localizations:
|
||||
path: ../../stream_chat_localizations
|
||||
stream_chat_persistence:
|
||||
path: ../../stream_chat_persistence
|
||||
|
||||
stream_chat_flutter: ^2.2.1
|
||||
stream_chat_localizations: ^1.1.0
|
||||
stream_chat_persistence: ^2.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -55,8 +47,8 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
assets:
|
||||
- assets/background_doodle.png
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
|
||||
+3
-3
@@ -141,7 +141,7 @@ class _PreparingState extends StatelessWidget {
|
||||
uploaded: 0,
|
||||
total: double.maxFinite.toInt(),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -181,7 +181,7 @@ class _InProgressState extends StatelessWidget {
|
||||
uploaded: sent,
|
||||
total: total,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -234,7 +234,7 @@ class _FailedState extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class AttachmentError extends StatelessWidget {
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentError
|
||||
.withOpacity(.1),
|
||||
.withOpacity(0.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
|
||||
@@ -253,7 +253,8 @@ class FileAttachment extends AttachmentWidget {
|
||||
if (message.status == MessageSendingStatus.sent) {
|
||||
trailingWidget = IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(
|
||||
color: theme.colorTheme.textHighEmphasis),
|
||||
color: theme.colorTheme.textHighEmphasis,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
|
||||
@@ -297,7 +297,7 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(.5),
|
||||
.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
|
||||
@@ -99,12 +99,14 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
() {
|
||||
final attachment = message.attachments[currentIndex];
|
||||
final isImage = attachment.type == 'image';
|
||||
final Future<String?> Function(Attachment,
|
||||
{void Function(int, int) progressCallback})
|
||||
saveFile = fileDownloader ?? _downloadAttachment;
|
||||
final Future<String?> Function(Attachment,
|
||||
{void Function(int, int) progressCallback})
|
||||
saveImage = imageDownloader ?? _downloadAttachment;
|
||||
final Future<String?> Function(
|
||||
Attachment, {
|
||||
void Function(int, int) progressCallback,
|
||||
}) saveFile = fileDownloader ?? _downloadAttachment;
|
||||
final Future<String?> Function(
|
||||
Attachment, {
|
||||
void Function(int, int) progressCallback,
|
||||
}) saveImage = imageDownloader ?? _downloadAttachment;
|
||||
final downloader = isImage ? saveImage : saveFile;
|
||||
|
||||
final progressNotifier =
|
||||
@@ -183,7 +185,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/unread_indicator.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Back button implementation
|
||||
// ignore: prefer-match-file-name
|
||||
class StreamBackButton extends StatelessWidget {
|
||||
/// Constructor for creating back button
|
||||
const StreamBackButton({
|
||||
|
||||
@@ -77,7 +77,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
UserAvatar(
|
||||
user: members
|
||||
.firstWhere(
|
||||
(e) => e.user?.id != userAsMember.user?.id)
|
||||
(e) => e.user?.id != userAsMember.user?.id,
|
||||
)
|
||||
.user!,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 64,
|
||||
@@ -93,7 +94,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
Text(
|
||||
members
|
||||
.firstWhere(
|
||||
(e) => e.user?.id != userAsMember.user?.id)
|
||||
(e) => e.user?.id != userAsMember.user?.id,
|
||||
)
|
||||
.user
|
||||
?.name ??
|
||||
'',
|
||||
|
||||
@@ -185,7 +185,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
onPressed: onNewChatButtonTap,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
title: Column(
|
||||
children: [
|
||||
|
||||
@@ -565,7 +565,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
'owner',
|
||||
].contains(channel.state!.members
|
||||
.firstWhereOrNull(
|
||||
(m) => m.userId == channel.client.state.currentUser?.id)
|
||||
(m) => m.userId == channel.client.state.currentUser?.id,
|
||||
)
|
||||
?.role))
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
|
||||
@@ -72,85 +72,82 @@ class ChannelPreview extends StatelessWidget {
|
||||
final channelPreviewTheme = ChannelPreviewTheme.of(context);
|
||||
final streamChatState = StreamChat.of(context);
|
||||
return BetterStreamBuilder<bool>(
|
||||
stream: channel.isMutedStream,
|
||||
initialData: channel.isMuted,
|
||||
builder: (context, data) => AnimatedOpacity(
|
||||
opacity: data ? 0.5 : 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
onTap: () => onTap?.call(channel),
|
||||
onLongPress: () => onLongPress?.call(channel),
|
||||
leading: leading ?? ChannelAvatar(onTap: onImageTap),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: title ??
|
||||
ChannelName(
|
||||
textStyle: channelPreviewTheme.titleStyle,
|
||||
),
|
||||
stream: channel.isMutedStream,
|
||||
initialData: channel.isMuted,
|
||||
builder: (context, data) => AnimatedOpacity(
|
||||
opacity: data ? 0.5 : 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
onTap: () => onTap?.call(channel),
|
||||
onLongPress: () => onLongPress?.call(channel),
|
||||
leading: leading ?? ChannelAvatar(onTap: onImageTap),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: title ??
|
||||
ChannelName(
|
||||
textStyle: channelPreviewTheme.titleStyle,
|
||||
),
|
||||
BetterStreamBuilder<List<Member>>(
|
||||
stream: channel.state?.membersStream,
|
||||
initialData: channel.state?.members,
|
||||
comparator: const ListEquality().equals,
|
||||
builder: (context, members) {
|
||||
if (members.isEmpty ||
|
||||
members.any((Member e) =>
|
||||
e.user!.id ==
|
||||
channel.client.state.currentUser?.id) !=
|
||||
true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return UnreadIndicator(
|
||||
cid: channel.cid,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(child: subtitle ?? _buildSubtitle(context)),
|
||||
sendingIndicator ??
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final lastMessage =
|
||||
channel.state?.messages.lastWhereOrNull(
|
||||
(m) => !m.isDeleted && m.shadowed != true,
|
||||
);
|
||||
if (lastMessage?.user?.id ==
|
||||
streamChatState.currentUser?.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: SendingIndicator(
|
||||
message: lastMessage!,
|
||||
size: channelPreviewTheme.indicatorIconSize,
|
||||
isMessageRead: channel.state!.read
|
||||
.where((element) =>
|
||||
element.user.id !=
|
||||
channel
|
||||
.client.state.currentUser!.id)
|
||||
.where((element) => element.lastRead
|
||||
.isAfter(lastMessage.createdAt))
|
||||
.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
trailing ?? _buildDate(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
BetterStreamBuilder<List<Member>>(
|
||||
stream: channel.state?.membersStream,
|
||||
initialData: channel.state?.members,
|
||||
comparator: const ListEquality().equals,
|
||||
builder: (context, members) {
|
||||
if (members.isEmpty ||
|
||||
!members.any((Member e) =>
|
||||
e.user!.id == channel.client.state.currentUser?.id)) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return UnreadIndicator(
|
||||
cid: channel.cid,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(child: subtitle ?? _buildSubtitle(context)),
|
||||
sendingIndicator ??
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final lastMessage =
|
||||
channel.state?.messages.lastWhereOrNull(
|
||||
(m) => !m.isDeleted && !m.shadowed,
|
||||
);
|
||||
if (lastMessage?.user?.id ==
|
||||
streamChatState.currentUser?.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: SendingIndicator(
|
||||
message: lastMessage!,
|
||||
size: channelPreviewTheme.indicatorIconSize,
|
||||
isMessageRead: channel.state!.read
|
||||
.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.currentUser!.id)
|
||||
.where((element) => element.lastRead
|
||||
.isAfter(lastMessage.createdAt))
|
||||
.isNotEmpty,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
trailing ?? _buildDate(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime>(
|
||||
@@ -246,10 +243,11 @@ class ChannelPreview extends StatelessWidget {
|
||||
lastMessage.mentionedUsers,
|
||||
lastMessage.attachments,
|
||||
channelPreviewTheme.subtitleStyle?.copyWith(
|
||||
color: channelPreviewTheme.subtitleStyle?.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal),
|
||||
color: channelPreviewTheme.subtitleStyle?.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
channelPreviewTheme.subtitleStyle?.copyWith(
|
||||
color: channelPreviewTheme.subtitleStyle?.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
|
||||
@@ -49,8 +49,9 @@ class CommandsOverlay extends StatelessWidget {
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shrinkWrap: true,
|
||||
@@ -72,9 +73,9 @@ class CommandsOverlay extends StatelessWidget {
|
||||
context.translations.instantCommandsLabel,
|
||||
style: TextStyle(
|
||||
color: _streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5),
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -102,7 +103,8 @@ class CommandsOverlay extends StatelessWidget {
|
||||
TextSpan(
|
||||
text: c.name.capitalize(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' /${c.name} ${c.args}',
|
||||
@@ -132,7 +134,9 @@ class CommandsOverlay extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCommandIcon(
|
||||
StreamChatThemeData _streamChatTheme, String iconType) {
|
||||
StreamChatThemeData _streamChatTheme,
|
||||
String iconType,
|
||||
) {
|
||||
switch (iconType) {
|
||||
case 'giphy':
|
||||
return CircleAvatar(
|
||||
|
||||
@@ -21,25 +21,16 @@ class DateDivider extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final createdAt = Jiffy(dateTime);
|
||||
final now = DateTime.now();
|
||||
final now = Jiffy(DateTime.now());
|
||||
|
||||
String dayInfo;
|
||||
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
|
||||
var dayInfo = createdAt.MMMd;
|
||||
if (createdAt.isSame(now, Units.DAY)) {
|
||||
dayInfo = context.translations.todayLabel;
|
||||
} else if (Jiffy(createdAt)
|
||||
.isSame(now.subtract(const Duration(days: 1)), Units.DAY)) {
|
||||
} else if (createdAt.isSame(now.subtract(days: 1), Units.DAY)) {
|
||||
dayInfo = context.translations.yesterdayLabel;
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
now.subtract(const Duration(days: 7)),
|
||||
Units.DAY,
|
||||
)) {
|
||||
} else if (createdAt.isAfter(now.subtract(days: 7), Units.DAY)) {
|
||||
dayInfo = createdAt.EEEE;
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
Jiffy(now).subtract(years: 1),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.MMMd;
|
||||
} else {
|
||||
} else if (createdAt.isAfter(now.subtract(years: 1), Units.DAY)) {
|
||||
dayInfo = createdAt.MMMd;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,10 +81,10 @@ class EmojiOverlay extends StatelessWidget {
|
||||
),
|
||||
style: TextStyle(
|
||||
color: _streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5),
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -127,7 +127,8 @@ extension FlipBorder on BorderRadius {
|
||||
topLeft: topRight,
|
||||
topRight: topLeft,
|
||||
bottomLeft: bottomRight,
|
||||
bottomRight: bottomLeft)
|
||||
bottomRight: bottomLeft,
|
||||
)
|
||||
: this;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
await Future.wait(videoPackages.values.map(
|
||||
(it) => it.initialize(),
|
||||
));
|
||||
setState(() {});
|
||||
setState(() {}); // ignore: no-empty-block
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -96,83 +96,83 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
body: Stack(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, snapshot) => PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (val) {
|
||||
animation: _controller,
|
||||
builder: (context, snapshot) => PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (val) {
|
||||
setState(() {
|
||||
_currentPage = val;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
if (attachment.type == 'image' ||
|
||||
attachment.type == 'giphy') {
|
||||
final imageUrl = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
return PhotoView(
|
||||
loadingBuilder: (context, image) => const Offstage(),
|
||||
imageProvider: (imageUrl == null &&
|
||||
attachment.localUri != null &&
|
||||
attachment.file?.bytes != null)
|
||||
? Image.memory(attachment.file!.bytes!).image
|
||||
: CachedNetworkImageProvider(imageUrl!),
|
||||
maxScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.mediaAttachments,
|
||||
),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: ColorTween(
|
||||
begin: ChannelHeaderTheme.of(context).color,
|
||||
end: Colors.black,
|
||||
).lerp(_controller.value),
|
||||
),
|
||||
onTapUp: (a, b, c) {
|
||||
setState(() {
|
||||
_currentPage = val;
|
||||
_optionsShown = !_optionsShown;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
if (attachment.type == 'image' ||
|
||||
attachment.type == 'giphy') {
|
||||
final imageUrl = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
return PhotoView(
|
||||
loadingBuilder: (context, image) =>
|
||||
const Offstage(),
|
||||
imageProvider: (imageUrl == null &&
|
||||
attachment.localUri != null &&
|
||||
attachment.file?.bytes != null)
|
||||
? Image.memory(attachment.file!.bytes!).image
|
||||
: CachedNetworkImageProvider(imageUrl!),
|
||||
maxScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.mediaAttachments,
|
||||
),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: ColorTween(
|
||||
begin: ChannelHeaderTheme.of(context).color,
|
||||
end: Colors.black,
|
||||
).lerp(_controller.value),
|
||||
),
|
||||
onTapUp: (a, b, c) {
|
||||
setState(() {
|
||||
_optionsShown = !_optionsShown;
|
||||
});
|
||||
if (_controller.isCompleted) {
|
||||
_controller.reverse();
|
||||
} else {
|
||||
_controller.forward();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (attachment.type == 'video') {
|
||||
final controller = videoPackages[attachment.id]!;
|
||||
if (!controller.initialized) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_optionsShown = !_optionsShown;
|
||||
});
|
||||
if (_controller.isCompleted) {
|
||||
_controller.reverse();
|
||||
} else {
|
||||
_controller.forward();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 50,
|
||||
),
|
||||
child: Chewie(
|
||||
controller: controller.chewieController!,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (_controller.isCompleted) {
|
||||
_controller.reverse();
|
||||
} else {
|
||||
_controller.forward();
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
itemCount: widget.mediaAttachments.length,
|
||||
)),
|
||||
);
|
||||
} else if (attachment.type == 'video') {
|
||||
final controller = videoPackages[attachment.id]!;
|
||||
if (!controller.initialized) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_optionsShown = !_optionsShown;
|
||||
});
|
||||
if (_controller.isCompleted) {
|
||||
_controller.reverse();
|
||||
} else {
|
||||
_controller.forward();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 50,
|
||||
),
|
||||
child: Chewie(
|
||||
controller: controller.chewieController!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
itemCount: widget.mediaAttachments.length,
|
||||
),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _optionsShown ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
@@ -67,19 +66,6 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
class _GalleryFooterState extends State<GalleryFooter> {
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final FocusNode _messageFocusNode = FocusNode();
|
||||
|
||||
final List<Channel> _selectedChannels = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_messageFocusNode.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const showShareButton = !kIsWeb;
|
||||
@@ -143,8 +129,9 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
context.translations.galleryPaginationText(
|
||||
currentPage: widget.currentPage,
|
||||
totalPages: widget.totalPages),
|
||||
currentPage: widget.currentPage,
|
||||
totalPages: widget.totalPages,
|
||||
),
|
||||
style: galleryFooterThemeData.titleTextStyle,
|
||||
),
|
||||
],
|
||||
@@ -267,6 +254,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
@@ -280,7 +268,6 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: UserAvatar(
|
||||
user: widget.message.user!,
|
||||
constraints:
|
||||
@@ -301,25 +288,4 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends the current message
|
||||
Future sendMessage() async {
|
||||
final text = _messageController.text.trim();
|
||||
|
||||
final attachments = widget.message.attachments;
|
||||
|
||||
_messageController.clear();
|
||||
|
||||
for (final channel in _selectedChannels) {
|
||||
final message = Message(
|
||||
text: text,
|
||||
attachments: [attachments[widget.currentPage]],
|
||||
);
|
||||
|
||||
await channel.sendMessage(message);
|
||||
}
|
||||
|
||||
_selectedChannels.clear();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ class DemoPainter extends CustomPainter {
|
||||
final p4 = pointsList.indexOf(off4);
|
||||
|
||||
squares.add(
|
||||
Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient));
|
||||
Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,17 +124,18 @@ class DemoPainter extends CustomPainter {
|
||||
final fontSize = username.length == 2 ? textSize : textSize * 1.5;
|
||||
|
||||
TextPainter(
|
||||
text: TextSpan(
|
||||
text: username,
|
||||
style: TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
text: TextSpan(
|
||||
text: username,
|
||||
style: TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textDirection: TextDirection.ltr)
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textDirection: TextDirection.ltr,
|
||||
)
|
||||
..layout(maxWidth: size.width)
|
||||
..paint(
|
||||
canvas,
|
||||
@@ -168,8 +170,8 @@ class DemoPainter extends CustomPainter {
|
||||
final sign1 = rand.nextInt(2) == 1 ? 1 : -1;
|
||||
final sign2 = rand.nextInt(2) == 1 ? 1 : -1;
|
||||
|
||||
final dx = 0.6 * sign1 * rand.nextInt(size.width ~/ columnCount);
|
||||
final dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount);
|
||||
final dx = sign1 * 0.6 * rand.nextInt(size.width ~/ columnCount);
|
||||
final dy = sign2 * 0.6 * rand.nextInt(size.height ~/ rowCount);
|
||||
|
||||
transformedList.add(Offset(orgDx + dx, orgDy + dy));
|
||||
}
|
||||
@@ -223,8 +225,12 @@ class Offset4 {
|
||||
/// Draw the polygon on canvas
|
||||
void draw(Canvas canvas, List<Offset> points) {
|
||||
final paint = Paint()
|
||||
..color = Color.fromARGB(255, Random().nextInt(255),
|
||||
Random().nextInt(255), Random().nextInt(255))
|
||||
..color = Color.fromARGB(
|
||||
255,
|
||||
Random().nextInt(255),
|
||||
Random().nextInt(255),
|
||||
Random().nextInt(255),
|
||||
)
|
||||
..shader = ui.Gradient.linear(
|
||||
points[p1],
|
||||
points[p3],
|
||||
|
||||
@@ -300,8 +300,10 @@ abstract class Translations {
|
||||
String get youText;
|
||||
|
||||
/// Gallery footer pagination text
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages});
|
||||
String galleryPaginationText({
|
||||
required int currentPage,
|
||||
required int totalPages,
|
||||
});
|
||||
|
||||
/// The text shown for "File"
|
||||
String get fileText;
|
||||
@@ -665,8 +667,10 @@ class DefaultTranslations implements Translations {
|
||||
String get youText => 'You';
|
||||
|
||||
@override
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
String galleryPaginationText({
|
||||
required int currentPage,
|
||||
required int totalPages,
|
||||
}) =>
|
||||
'${currentPage + 1} of $totalPages';
|
||||
|
||||
@override
|
||||
|
||||
@@ -129,7 +129,7 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -144,9 +144,9 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
_getMedia();
|
||||
}
|
||||
|
||||
void _getMedia() async {
|
||||
Future<void> _getMedia() async {
|
||||
final assetList = await PhotoManager.getAssetPathList().then((value) {
|
||||
if (value.isNotEmpty == true) {
|
||||
if (value.isNotEmpty) {
|
||||
return value.singleWhere((element) => element.isAll);
|
||||
}
|
||||
});
|
||||
@@ -178,7 +178,9 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(
|
||||
MediaThumbnailProvider key, DecoderCallback decode) =>
|
||||
MediaThumbnailProvider key,
|
||||
DecoderCallback decode,
|
||||
) =>
|
||||
MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: 1,
|
||||
@@ -188,7 +190,9 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
||||
);
|
||||
|
||||
Future<ui.Codec> _loadAsync(
|
||||
MediaThumbnailProvider key, DecoderCallback decode) async {
|
||||
MediaThumbnailProvider key,
|
||||
DecoderCallback decode,
|
||||
) async {
|
||||
assert(key == this, 'Checks MediaThumbnailProvider');
|
||||
final bytes = await media.thumbData;
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
final size = mediaQueryData.size;
|
||||
final user = StreamChat.of(context).currentUser;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
final roughMaxSize = size.width * 2 / 3;
|
||||
var messageTextLength = widget.message.text!.length;
|
||||
if (widget.message.quotedMessage != null) {
|
||||
var quotedMessageLength =
|
||||
@@ -119,7 +119,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
final roughSentenceSize = messageTextLength *
|
||||
(widget.messageTheme.messageTextStyle?.fontSize ?? 1) *
|
||||
1.2;
|
||||
final divFactor = widget.message.attachments.isNotEmpty == true
|
||||
final divFactor = widget.message.attachments.isNotEmpty
|
||||
? 1
|
||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||
|
||||
@@ -142,14 +142,15 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
(widget.message.status == MessageSendingStatus.sent))
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
user?.id == widget.message.user?.id
|
||||
? (divFactor >= 1.0
|
||||
? -0.2 - shiftFactor
|
||||
: (1.2 - divFactor))
|
||||
: (divFactor >= 1.0
|
||||
? 0.2 + shiftFactor
|
||||
: -(1.2 - divFactor)),
|
||||
0),
|
||||
user?.id == widget.message.user?.id
|
||||
? (divFactor >= 1.0
|
||||
? -0.2 - shiftFactor
|
||||
: (1.2 - divFactor))
|
||||
: (divFactor >= 1.0
|
||||
? shiftFactor + 0.2
|
||||
: -(1.2 - divFactor)),
|
||||
0,
|
||||
),
|
||||
child: ReactionPicker(
|
||||
message: widget.message,
|
||||
),
|
||||
@@ -194,7 +195,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
.map((action) => _buildCustomAction(
|
||||
context,
|
||||
action,
|
||||
))
|
||||
)),
|
||||
].insertBetween(
|
||||
Container(
|
||||
height: 1,
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/overlays.dart';
|
||||
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
@@ -373,11 +373,13 @@ class MessageInputState extends State<MessageInput> {
|
||||
_parseExistingMessage(widget.editMessage ?? widget.initialMessage!);
|
||||
}
|
||||
textEditingController.addListener(_onChangedDebounced);
|
||||
_focusNode.addListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_openFilePickerSection = false;
|
||||
}
|
||||
});
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
}
|
||||
|
||||
void _focusNodeListener() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_openFilePickerSection = false;
|
||||
}
|
||||
}
|
||||
|
||||
int _timeOut = 0;
|
||||
@@ -536,7 +538,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
? null
|
||||
: Border.all(
|
||||
color: _streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5),
|
||||
.withOpacity(0.5),
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
@@ -640,7 +642,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
secondChild: widget.disableAttachments &&
|
||||
!widget.showCommandsButton &&
|
||||
widget.actions.isNotEmpty != true
|
||||
!widget.actions.isNotEmpty
|
||||
? const Offstage()
|
||||
: Wrap(
|
||||
children: <Widget>[
|
||||
@@ -706,7 +708,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
decoration: _getInputDecoration(context),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -827,6 +829,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (value.isNotEmpty) {
|
||||
// ignore: no-empty-block
|
||||
channel.keyStroke(widget.parentMessage?.id).catchError((e) {});
|
||||
}
|
||||
|
||||
@@ -1028,8 +1031,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
_attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.image,
|
||||
camera: true);
|
||||
pickFile(
|
||||
DefaultAttachmentTypes.image,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@@ -1042,8 +1047,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
_attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.video,
|
||||
camera: true);
|
||||
pickFile(
|
||||
DefaultAttachmentTypes.video,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -1195,8 +1202,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
textEditingController.value = TextEditingValue(
|
||||
text: rejoin +
|
||||
textEditingController.text
|
||||
.substring(textEditingController.selection.start),
|
||||
textEditingController.text.substring(
|
||||
textEditingController.selection.start,
|
||||
),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: rejoin.length,
|
||||
),
|
||||
@@ -1254,8 +1262,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildReplyToMessage() {
|
||||
if (!_hasQuotedMessage) return const Offstage();
|
||||
final containsUrl = widget.quotedMessage!.attachments
|
||||
.any((element) => element.titleLink != null) ==
|
||||
true;
|
||||
.any((element) => element.titleLink != null);
|
||||
return QuotedMessageWidget(
|
||||
reverse: true,
|
||||
showBorder: !containsUrl,
|
||||
@@ -1360,7 +1367,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
setState(() => _attachments.remove(attachment.id));
|
||||
},
|
||||
fillColor:
|
||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.5),
|
||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
child: Center(
|
||||
child: StreamSvgIcon.close(
|
||||
size: 24,
|
||||
@@ -1839,10 +1846,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
backgroundColor: _streamChatTheme.colorTheme.barsBg,
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
)),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -1875,7 +1883,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.08),
|
||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08),
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
@@ -1908,6 +1916,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
@override
|
||||
void dispose() {
|
||||
textEditingController.dispose();
|
||||
_focusNode.removeListener(_focusNodeListener);
|
||||
_stopSlowMode();
|
||||
_onChangedDebounced.cancel();
|
||||
super.dispose();
|
||||
@@ -2018,7 +2027,8 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.body.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.textLowEmphasis),
|
||||
color: widget.streamChatTheme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
@@ -60,6 +60,7 @@ typedef OnMessageTap = void Function(Message);
|
||||
typedef ReplyTapCallback = void Function(Message);
|
||||
|
||||
/// Class for message details
|
||||
// ignore: prefer-match-file-name
|
||||
class MessageDetails {
|
||||
/// Constructor for creating [MessageDetails]
|
||||
MessageDetails(
|
||||
@@ -358,8 +359,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
child: Text(
|
||||
context.translations.emptyChatMessagesText,
|
||||
style: _streamTheme.textTheme.footnote.copyWith(
|
||||
color: _streamTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5)),
|
||||
color: _streamTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
messageListBuilder: widget.messageListBuilder ??
|
||||
@@ -371,8 +373,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
child: Text(
|
||||
context.translations.genericErrorText,
|
||||
style: _streamTheme.textTheme.footnote.copyWith(
|
||||
color: _streamTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5)),
|
||||
color: _streamTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -383,7 +386,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
if (_messageListLength != null) {
|
||||
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
|
||||
if (_itemPositionListener.itemPositions.value.isNotEmpty == true) {
|
||||
if (_itemPositionListener.itemPositions.value.isNotEmpty) {
|
||||
final first = _itemPositionListener.itemPositions.value.first;
|
||||
final diff = newMessagesListLength - _messageListLength!;
|
||||
if (diff > 0) {
|
||||
@@ -635,10 +638,14 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
);
|
||||
|
||||
final backgroundColor = MessageListViewTheme.of(context).backgroundColor;
|
||||
final backgroundImage = MessageListViewTheme.of(context).backgroundImage;
|
||||
|
||||
if (backgroundColor != null) {
|
||||
return ColoredBox(
|
||||
color: backgroundColor,
|
||||
if (backgroundColor != null || backgroundImage != null) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
image: backgroundImage,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@@ -674,7 +681,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
child: BetterStreamBuilder<Iterable<ItemPosition>>(
|
||||
initialData: _itemPositionListener.itemPositions.value,
|
||||
stream: _valueListenableToStreamAdapter(
|
||||
_itemPositionListener.itemPositions),
|
||||
_itemPositionListener.itemPositions,
|
||||
),
|
||||
comparator: (a, b) {
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
@@ -961,7 +969,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
final userId = StreamChat.of(context).currentUser!.id;
|
||||
final isMyMessage = message.user!.id == userId;
|
||||
final isMyMessage = message.user?.id == userId;
|
||||
final nextMessage = index - 1 >= 0 ? messages[index - 1] : null;
|
||||
final isNextUserSame =
|
||||
nextMessage != null && message.user!.id == nextMessage.user!.id;
|
||||
@@ -984,7 +992,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
final hasFileAttachment =
|
||||
message.attachments.any((it) => it.type == 'file') == true;
|
||||
message.attachments.any((it) => it.type == 'file');
|
||||
|
||||
final isThreadMessage =
|
||||
message.parentId != null && message.showInChannel == true;
|
||||
@@ -1016,7 +1024,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
|
||||
final hasUrlAttachment =
|
||||
message.attachments.any((it) => it.titleLink != null) == true;
|
||||
message.attachments.any((it) => it.titleLink != null);
|
||||
|
||||
final borderSide =
|
||||
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
|
||||
@@ -1259,10 +1267,11 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
if (widget.onThreadTap != null) {
|
||||
_onThreadTap = (Message message) {
|
||||
widget.onThreadTap!(
|
||||
message,
|
||||
widget.threadBuilder != null
|
||||
? widget.threadBuilder!(context, message)
|
||||
: null);
|
||||
message,
|
||||
widget.threadBuilder != null
|
||||
? widget.threadBuilder!(context, message)
|
||||
: null,
|
||||
);
|
||||
};
|
||||
} else if (widget.threadBuilder != null) {
|
||||
_onThreadTap = (Message message) {
|
||||
@@ -1271,7 +1280,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BetterStreamBuilder<Message>(
|
||||
stream: streamChannel!.channel.state!.messagesStream.map(
|
||||
(messages) => messages.firstWhere((m) => m.id == message.id)),
|
||||
(messages) => messages.firstWhere((m) => m.id == message.id),
|
||||
),
|
||||
initialData: message,
|
||||
builder: (_, data) => StreamChannel(
|
||||
channel: streamChannel!.channel,
|
||||
@@ -1320,7 +1330,7 @@ class _LoadingIndicator extends StatelessWidget {
|
||||
stream: stream,
|
||||
initialData: false,
|
||||
errorBuilder: (context, error) => Container(
|
||||
color: streamTheme.colorTheme.accentError.withOpacity(.2),
|
||||
color: streamTheme.colorTheme.accentError.withOpacity(0.2),
|
||||
child: Center(
|
||||
child: Text(context.translations.loadingMessagesError),
|
||||
),
|
||||
|
||||
@@ -46,11 +46,11 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final user = StreamChat.of(context).currentUser;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
final roughMaxSize = size.width * 2 / 3;
|
||||
var messageTextLength = message.text!.length;
|
||||
if (message.quotedMessage != null) {
|
||||
var quotedMessageLength = message.quotedMessage!.text!.length + 40;
|
||||
if (message.quotedMessage!.attachments.isNotEmpty == true) {
|
||||
if (message.quotedMessage!.attachments.isNotEmpty) {
|
||||
quotedMessageLength += 40;
|
||||
}
|
||||
if (quotedMessageLength > messageTextLength) {
|
||||
@@ -60,7 +60,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
final roughSentenceSize = messageTextLength *
|
||||
(messageTheme.messageTextStyle?.fontSize ?? 1) *
|
||||
1.2;
|
||||
final divFactor = message.attachments.isNotEmpty == true
|
||||
final divFactor = message.attachments.isNotEmpty
|
||||
? 1
|
||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||
|
||||
@@ -80,14 +80,15 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
(message.status == MessageSendingStatus.sent))
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
user!.id == message.user!.id
|
||||
? (divFactor >= 1.0
|
||||
? -0.2 - shiftFactor
|
||||
: (1.2 - divFactor))
|
||||
: (divFactor >= 1.0
|
||||
? 0.2 + shiftFactor
|
||||
: -(1.2 - divFactor)),
|
||||
0),
|
||||
user!.id == message.user!.id
|
||||
? (divFactor >= 1.0
|
||||
? -0.2 - shiftFactor
|
||||
: (1.2 - divFactor))
|
||||
: (divFactor >= 1.0
|
||||
? shiftFactor + 0.2
|
||||
: -(1.2 - divFactor)),
|
||||
0,
|
||||
),
|
||||
child: ReactionPicker(
|
||||
message: message,
|
||||
),
|
||||
@@ -102,7 +103,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
context,
|
||||
user,
|
||||
),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -146,11 +146,12 @@ class MessageSearchItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
TextSpan _getDisplayText(
|
||||
String text,
|
||||
List<User> mentions,
|
||||
List<Attachment> attachments,
|
||||
TextStyle? normalTextStyle,
|
||||
TextStyle? mentionsTextStyle) {
|
||||
String text,
|
||||
List<User> mentions,
|
||||
List<Attachment> attachments,
|
||||
TextStyle? normalTextStyle,
|
||||
TextStyle? mentionsTextStyle,
|
||||
) {
|
||||
final textList = text.split(' ');
|
||||
final resList = <TextSpan>[];
|
||||
for (final e in textList) {
|
||||
|
||||
@@ -217,7 +217,9 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
);
|
||||
|
||||
Widget _listItemBuilder(
|
||||
BuildContext context, GetMessageResponse getMessageResponse) {
|
||||
BuildContext context,
|
||||
GetMessageResponse getMessageResponse,
|
||||
) {
|
||||
if (widget.itemBuilder != null) {
|
||||
return widget.itemBuilder!(context, getMessageResponse);
|
||||
}
|
||||
@@ -231,33 +233,34 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
|
||||
return StreamBuilder<bool>(
|
||||
stream: messageSearchBloc.queryMessagesLoading,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentError
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: Text(context.translations.loadingMessagesError),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
stream: messageSearchBloc.queryMessagesLoading,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: snapshot.data!
|
||||
? const CircularProgressIndicator()
|
||||
: Container(),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentError
|
||||
.withOpacity(0.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: Text(context.translations.loadingMessagesError),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: snapshot.data!
|
||||
? const CircularProgressIndicator()
|
||||
: Container(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListView(List<GetMessageResponse> data) {
|
||||
|
||||
@@ -88,7 +88,9 @@ class MessageText extends StatelessWidget {
|
||||
for (final user in message.mentionedUsers.toSet()) {
|
||||
final userName = user.name;
|
||||
messageTextToRender = messageTextToRender.replaceAll(
|
||||
'@$userName', '[@$userName](@${userName.replaceAll(' ', '')})');
|
||||
'@$userName',
|
||||
'[@$userName](@${userName.replaceAll(' ', '')})',
|
||||
);
|
||||
}
|
||||
return messageTextToRender;
|
||||
}
|
||||
|
||||
@@ -575,8 +575,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
|
||||
|
||||
bool get isGiphy =>
|
||||
widget.message.attachments.any((element) => element.type == 'giphy') ==
|
||||
true;
|
||||
widget.message.attachments.any((element) => element.type == 'giphy');
|
||||
|
||||
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
|
||||
|
||||
@@ -596,7 +595,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
isDeleted;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true;
|
||||
bool get wantKeepAlive => widget.message.attachments.isNotEmpty;
|
||||
|
||||
late StreamChatThemeData _streamChatTheme;
|
||||
late StreamChatState _streamChat;
|
||||
@@ -674,7 +673,10 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
child: PortalEntry(
|
||||
portal: Container(
|
||||
transform: Matrix4.translationValues(
|
||||
widget.reverse ? 12 : -12, 0, 0),
|
||||
widget.reverse ? 12 : -12,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 22 * 6.0,
|
||||
),
|
||||
@@ -704,13 +706,15 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
? Container(
|
||||
// ignore: lines_longer_than_80_chars
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
// ignore: lines_longer_than_80_chars
|
||||
widget.showUserAvatar ==
|
||||
// ignore: lines_longer_than_80_chars
|
||||
DisplayWidget.gone
|
||||
? 0
|
||||
: 4.0),
|
||||
horizontal:
|
||||
// ignore: lines_longer_than_80_chars
|
||||
widget.showUserAvatar ==
|
||||
// ignore: lines_longer_than_80_chars
|
||||
DisplayWidget
|
||||
.gone
|
||||
? 0
|
||||
: 4.0,
|
||||
),
|
||||
child: DeletedMessage(
|
||||
borderRadiusGeometry: widget
|
||||
.borderRadiusGeometry,
|
||||
@@ -794,7 +798,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
widget.message.user != null) ...[
|
||||
_buildUserAvatar(),
|
||||
const SizedBox(width: 4),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
if (showBottomRow)
|
||||
@@ -856,7 +860,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
: chatThemeData.ownMessageTheme,
|
||||
reverse: widget.reverse,
|
||||
padding: EdgeInsets.only(
|
||||
right: 8, left: 8, top: 8, bottom: hasNonUrlAttachments ? 8 : 0),
|
||||
right: 8,
|
||||
left: 8,
|
||||
top: 8,
|
||||
bottom: hasNonUrlAttachments ? 8 : 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -979,7 +987,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
return widget.usernameBuilder!(context, widget.message);
|
||||
}
|
||||
return Text(
|
||||
widget.message.user!.name,
|
||||
widget.message.user?.name ?? '',
|
||||
maxLines: 1,
|
||||
key: usernameKey,
|
||||
style: widget.messageTheme.messageAuthorStyle,
|
||||
@@ -1054,64 +1062,64 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
showDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
barrierColor: _streamChatTheme.colorTheme.overlay,
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: MessageActionsModal(
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
text: (widget.message.text?.length ?? 0) > 200
|
||||
? '${widget.message.text!.substring(0, 200)}...'
|
||||
: widget.message.text,
|
||||
),
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: false,
|
||||
padding: const EdgeInsets.all(0),
|
||||
showReactionPickerIndicator: widget.showReactions &&
|
||||
(widget.message.status == MessageSendingStatus.sent),
|
||||
showPinHighlight: false,
|
||||
showUserAvatar: widget.message.user!.id ==
|
||||
channel.client.state.currentUser!.id
|
||||
? DisplayWidget.gone
|
||||
: DisplayWidget.show,
|
||||
),
|
||||
onCopyTap: (message) =>
|
||||
Clipboard.setData(ClipboardData(text: message.text)),
|
||||
messageTheme: widget.messageTheme,
|
||||
reverse: widget.reverse,
|
||||
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
|
||||
message: widget.message,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
onThreadReplyTap: widget.onThreadTap,
|
||||
showResendMessage: widget.showResendMessage &&
|
||||
(isSendFailed || isUpdateFailed),
|
||||
showCopyMessage: widget.showCopyMessage &&
|
||||
!isFailedState &&
|
||||
widget.message.text?.trim().isNotEmpty == true,
|
||||
showEditMessage: widget.showEditMessage &&
|
||||
!isDeleteFailed &&
|
||||
widget.message.attachments
|
||||
.any((element) => element.type == 'giphy') !=
|
||||
true,
|
||||
showReactions: widget.showReactions,
|
||||
showReplyMessage: widget.showReplyMessage &&
|
||||
!isFailedState &&
|
||||
widget.onReplyTap != null,
|
||||
showThreadReplyMessage: widget.showThreadReplyMessage &&
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
showPinButton: widget.showPinButton,
|
||||
customActions: widget.customActions,
|
||||
),
|
||||
));
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
barrierColor: _streamChatTheme.colorTheme.overlay,
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: MessageActionsModal(
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
text: (widget.message.text?.length ?? 0) > 200
|
||||
? '${widget.message.text!.substring(0, 200)}...'
|
||||
: widget.message.text,
|
||||
),
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: false,
|
||||
padding: const EdgeInsets.all(0),
|
||||
showReactionPickerIndicator: widget.showReactions &&
|
||||
(widget.message.status == MessageSendingStatus.sent),
|
||||
showPinHighlight: false,
|
||||
showUserAvatar:
|
||||
widget.message.user!.id == channel.client.state.currentUser!.id
|
||||
? DisplayWidget.gone
|
||||
: DisplayWidget.show,
|
||||
),
|
||||
onCopyTap: (message) =>
|
||||
Clipboard.setData(ClipboardData(text: message.text)),
|
||||
messageTheme: widget.messageTheme,
|
||||
reverse: widget.reverse,
|
||||
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
|
||||
message: widget.message,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
onThreadReplyTap: widget.onThreadTap,
|
||||
showResendMessage:
|
||||
widget.showResendMessage && (isSendFailed || isUpdateFailed),
|
||||
showCopyMessage: widget.showCopyMessage &&
|
||||
!isFailedState &&
|
||||
widget.message.text?.trim().isNotEmpty == true,
|
||||
showEditMessage: widget.showEditMessage &&
|
||||
!isDeleteFailed &&
|
||||
!widget.message.attachments
|
||||
.any((element) => element.type == 'giphy'),
|
||||
showReactions: widget.showReactions,
|
||||
showReplyMessage: widget.showReplyMessage &&
|
||||
!isFailedState &&
|
||||
widget.onReplyTap != null,
|
||||
showThreadReplyMessage: widget.showThreadReplyMessage &&
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
showPinButton: widget.showPinButton,
|
||||
customActions: widget.customActions,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMessageReactionsModalBottomSheet(BuildContext context) {
|
||||
@@ -1291,8 +1299,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
? widget.messageTheme.copyWith(
|
||||
messageTextStyle:
|
||||
widget.messageTheme.messageTextStyle!.copyWith(
|
||||
fontSize: 42,
|
||||
))
|
||||
fontSize: 42,
|
||||
),
|
||||
)
|
||||
: widget.messageTheme,
|
||||
),
|
||||
),
|
||||
@@ -1326,7 +1335,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1432,8 +1441,12 @@ class _ThreadReplyPainter extends CustomPainter {
|
||||
|
||||
final path = Path()
|
||||
..moveTo(reverse ? size.width : 0, 0)
|
||||
..quadraticBezierTo(reverse ? size.width : 0, size.height * 0.38,
|
||||
reverse ? size.width : 0, size.height * 0.50)
|
||||
..quadraticBezierTo(
|
||||
reverse ? size.width : 0,
|
||||
size.height * 0.38,
|
||||
reverse ? size.width : 0,
|
||||
size.height * 0.5,
|
||||
)
|
||||
..quadraticBezierTo(
|
||||
reverse ? size.width : 0,
|
||||
size.height,
|
||||
|
||||
+17
-17
@@ -2,23 +2,6 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
|
||||
/// Class that contains the parameters for building an overlay entry
|
||||
class OverlayOptions {
|
||||
/// Constructs a new overlay options object
|
||||
/// [visible] - the visibility of the overlay
|
||||
/// [widget] - the widget to be displayed
|
||||
OverlayOptions({
|
||||
required this.visible,
|
||||
required this.widget,
|
||||
});
|
||||
|
||||
/// the visibility of the overlay
|
||||
final bool visible;
|
||||
|
||||
/// the widget to be displayed
|
||||
final Widget widget;
|
||||
}
|
||||
|
||||
/// Widget that renders a single overlay widget from a list of [overlayOptions]
|
||||
/// It shows the first one that is visible
|
||||
class MultiOverlay extends StatelessWidget {
|
||||
@@ -61,3 +44,20 @@ class MultiOverlay extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Class that contains the parameters for building an overlay entry
|
||||
class OverlayOptions {
|
||||
/// Constructs a new overlay options object
|
||||
/// [visible] - the visibility of the overlay
|
||||
/// [widget] - the widget to be displayed
|
||||
OverlayOptions({
|
||||
required this.visible,
|
||||
required this.widget,
|
||||
});
|
||||
|
||||
/// the visibility of the overlay
|
||||
final bool visible;
|
||||
|
||||
/// the widget to be displayed
|
||||
final Widget widget;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.network(widget.attachment.assetUrl!)
|
||||
..initialize().then((_) {
|
||||
// ignore: no-empty-block
|
||||
setState(() {}); //when your thumbnail will show.
|
||||
});
|
||||
}
|
||||
@@ -95,10 +96,10 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
/// Callback for tap on widget
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
bool get _hasAttachments => message.attachments.isNotEmpty == true;
|
||||
bool get _hasAttachments => message.attachments.isNotEmpty;
|
||||
|
||||
bool get _containsLinkAttachment =>
|
||||
message.attachments.any((element) => element.titleLink != null) == true;
|
||||
message.attachments.any((element) => element.titleLink != null);
|
||||
|
||||
bool get _containsText => message.text?.isNotEmpty == true;
|
||||
|
||||
@@ -140,12 +141,14 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
messageTheme: isOnlyEmoji && _containsText
|
||||
? messageTheme.copyWith(
|
||||
messageTextStyle: messageTheme.messageTextStyle?.copyWith(
|
||||
fontSize: 32,
|
||||
))
|
||||
fontSize: 32,
|
||||
),
|
||||
)
|
||||
: messageTheme.copyWith(
|
||||
messageTextStyle: messageTheme.messageTextStyle?.copyWith(
|
||||
fontSize: 12,
|
||||
)),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8));
|
||||
@@ -186,7 +189,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
attachment.imageUrl!,
|
||||
attachment.thumbUrl!,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -275,7 +278,8 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: getFileTypeImage(
|
||||
attachment.extraData['mime_type'] as String?),
|
||||
attachment.extraData['mime_type'] as String?,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ class ReactionBubble extends StatelessWidget {
|
||||
size: 16,
|
||||
color: (!highlightOwnReactions || reaction.user?.id == userId)
|
||||
? chatThemeData.colorTheme.accentPrimary
|
||||
: chatThemeData.colorTheme.textHighEmphasis.withOpacity(.5),
|
||||
: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,10 +62,11 @@ class _ReactionPickerState extends State<ReactionPicker>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionIcons
|
||||
.map<Widget>((reactionIcon) {
|
||||
final ownReactionIndex = widget.message.ownReactions
|
||||
?.indexWhere(
|
||||
(reaction) => reaction.type == reactionIcon.type) ??
|
||||
-1;
|
||||
final ownReactionIndex =
|
||||
widget.message.ownReactions?.indexWhere(
|
||||
(reaction) => reaction.type == reactionIcon.type,
|
||||
) ??
|
||||
-1;
|
||||
final index = reactionIcons.indexOf(reactionIcon);
|
||||
|
||||
final child = reactionIcon.builder(
|
||||
|
||||
@@ -75,7 +75,8 @@ class StreamChat extends StatefulWidget {
|
||||
|
||||
if (streamChatState == null) {
|
||||
throw Exception(
|
||||
'You must have a StreamChat widget at the top of your widget tree');
|
||||
'You must have a StreamChat widget at the top of your widget tree',
|
||||
);
|
||||
}
|
||||
|
||||
return streamChatState;
|
||||
|
||||
@@ -145,7 +145,7 @@ class StreamChatThemeData {
|
||||
) {
|
||||
final accentColor = colorTheme.accentPrimary;
|
||||
final iconTheme =
|
||||
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5));
|
||||
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5));
|
||||
final channelHeaderTheme = ChannelHeaderThemeData(
|
||||
avatarTheme: AvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -174,7 +174,7 @@ class StreamChatThemeData {
|
||||
color: const Color(0xff7A7A7A),
|
||||
),
|
||||
lastMessageAtStyle: textTheme.footnote.copyWith(
|
||||
color: colorTheme.textHighEmphasis.withOpacity(.5),
|
||||
color: colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
indicatorIconSize: 16,
|
||||
);
|
||||
@@ -280,7 +280,7 @@ class StreamChatThemeData {
|
||||
return StreamSvgIcon.loveReaction(
|
||||
color: highlighted
|
||||
? theme.colorTheme.accentPrimary
|
||||
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
||||
size: size,
|
||||
);
|
||||
},
|
||||
@@ -292,7 +292,7 @@ class StreamChatThemeData {
|
||||
return StreamSvgIcon.thumbsUpReaction(
|
||||
color: highlighted
|
||||
? theme.colorTheme.accentPrimary
|
||||
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
||||
size: size,
|
||||
);
|
||||
},
|
||||
@@ -304,7 +304,7 @@ class StreamChatThemeData {
|
||||
return StreamSvgIcon.thumbsDownReaction(
|
||||
color: highlighted
|
||||
? theme.colorTheme.accentPrimary
|
||||
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
||||
size: size,
|
||||
);
|
||||
},
|
||||
@@ -316,7 +316,7 @@ class StreamChatThemeData {
|
||||
return StreamSvgIcon.lolReaction(
|
||||
color: highlighted
|
||||
? theme.colorTheme.accentPrimary
|
||||
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
||||
size: size,
|
||||
);
|
||||
},
|
||||
@@ -328,7 +328,7 @@ class StreamChatThemeData {
|
||||
return StreamSvgIcon.wutReaction(
|
||||
color: highlighted
|
||||
? theme.colorTheme.accentPrimary
|
||||
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
||||
size: size,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A style that overrides the default appearance of various avatar widgets.
|
||||
// ignore: prefer-match-file-name
|
||||
class AvatarThemeData with Diagnosticable {
|
||||
/// Creates an [AvatarThemeData].
|
||||
const AvatarThemeData({
|
||||
|
||||
@@ -25,13 +25,33 @@ class ColorTheme {
|
||||
stops: [0, 1],
|
||||
),
|
||||
this.borderTop = const Effect(
|
||||
sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08),
|
||||
sigmaX: 0,
|
||||
sigmaY: -1,
|
||||
color: Color(0xff000000),
|
||||
blur: 0,
|
||||
alpha: 0.08,
|
||||
),
|
||||
this.borderBottom = const Effect(
|
||||
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08),
|
||||
sigmaX: 0,
|
||||
sigmaY: 1,
|
||||
color: Color(0xff000000),
|
||||
blur: 0,
|
||||
alpha: 0.08,
|
||||
),
|
||||
this.shadowIconButton = const Effect(
|
||||
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4),
|
||||
sigmaX: 0,
|
||||
sigmaY: 2,
|
||||
color: Color(0xff000000),
|
||||
alpha: 0.5,
|
||||
blur: 4,
|
||||
),
|
||||
this.modalShadow = const Effect(
|
||||
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8),
|
||||
sigmaX: 0,
|
||||
sigmaY: 0,
|
||||
color: Color(0xff000000),
|
||||
alpha: 1,
|
||||
blur: 8,
|
||||
),
|
||||
}) : brightness = Brightness.light;
|
||||
|
||||
/// Initialise with dark theme
|
||||
|
||||
@@ -151,11 +151,20 @@ class GalleryFooterThemeData with Diagnosticable {
|
||||
bottomSheetBarrierColor:
|
||||
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
|
||||
bottomSheetBackgroundColor: Color.lerp(
|
||||
a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t),
|
||||
a.bottomSheetBackgroundColor,
|
||||
b.bottomSheetBackgroundColor,
|
||||
t,
|
||||
),
|
||||
bottomSheetPhotosTextStyle: TextStyle.lerp(
|
||||
a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t),
|
||||
a.bottomSheetPhotosTextStyle,
|
||||
b.bottomSheetPhotosTextStyle,
|
||||
t,
|
||||
),
|
||||
bottomSheetCloseIconColor: Color.lerp(
|
||||
a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t),
|
||||
a.bottomSheetCloseIconColor,
|
||||
b.bottomSheetCloseIconColor,
|
||||
t,
|
||||
),
|
||||
);
|
||||
|
||||
/// Merges one [GalleryFooterThemeData] with another.
|
||||
@@ -208,10 +217,16 @@ class GalleryFooterThemeData with Diagnosticable {
|
||||
..add(ColorProperty('gridIconButtonColor', gridIconButtonColor))
|
||||
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor))
|
||||
..add(ColorProperty(
|
||||
'bottomSheetBackgroundColor', bottomSheetBackgroundColor))
|
||||
'bottomSheetBackgroundColor',
|
||||
bottomSheetBackgroundColor,
|
||||
))
|
||||
..add(DiagnosticsProperty(
|
||||
'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle))
|
||||
'bottomSheetPhotosTextStyle',
|
||||
bottomSheetPhotosTextStyle,
|
||||
))
|
||||
..add(ColorProperty(
|
||||
'bottomSheetCloseIconColor', bottomSheetCloseIconColor));
|
||||
'bottomSheetCloseIconColor',
|
||||
bottomSheetCloseIconColor,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,17 +60,23 @@ class MessageListViewThemeData with Diagnosticable {
|
||||
/// Creates a [MessageListViewThemeData].
|
||||
const MessageListViewThemeData({
|
||||
this.backgroundColor,
|
||||
this.backgroundImage,
|
||||
});
|
||||
|
||||
/// The color of the [MessageListView] background.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The image of the [MessageListView] background.
|
||||
final DecorationImage? backgroundImage;
|
||||
|
||||
/// Copies this [MessageListViewThemeData] to another.
|
||||
MessageListViewThemeData copyWith({
|
||||
Color? backgroundColor,
|
||||
DecorationImage? backgroundImage,
|
||||
}) =>
|
||||
MessageListViewThemeData(
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
backgroundImage: backgroundImage ?? this.backgroundImage,
|
||||
);
|
||||
|
||||
/// Linearly interpolate between two [MessageListView] themes.
|
||||
@@ -83,6 +89,7 @@ class MessageListViewThemeData with Diagnosticable {
|
||||
) =>
|
||||
MessageListViewThemeData(
|
||||
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
|
||||
backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage,
|
||||
);
|
||||
|
||||
/// Merges one [MessageListViewThemeData] with another.
|
||||
@@ -90,6 +97,7 @@ class MessageListViewThemeData with Diagnosticable {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
backgroundColor: other.backgroundColor,
|
||||
backgroundImage: other.backgroundImage,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,14 +106,20 @@ class MessageListViewThemeData with Diagnosticable {
|
||||
identical(this, other) ||
|
||||
other is MessageListViewThemeData &&
|
||||
runtimeType == other.runtimeType &&
|
||||
backgroundColor == other.backgroundColor;
|
||||
backgroundColor == other.backgroundColor &&
|
||||
backgroundImage == other.backgroundImage;
|
||||
|
||||
@override
|
||||
int get hashCode => backgroundColor.hashCode;
|
||||
int get hashCode => backgroundColor.hashCode + backgroundImage.hashCode;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('backgroundColor', backgroundColor));
|
||||
properties
|
||||
..add(ColorProperty('backgroundColor', backgroundColor))
|
||||
..add(
|
||||
DiagnosticsProperty<DecorationImage>('backgroundImage', backgroundImage,
|
||||
defaultValue: null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
|
||||
|
||||
/// Class for getting message theme
|
||||
// ignore: prefer-match-file-name
|
||||
class MessageThemeData with Diagnosticable {
|
||||
/// Creates a [MessageThemeData].
|
||||
const MessageThemeData({
|
||||
@@ -105,7 +106,10 @@ class MessageThemeData with Diagnosticable {
|
||||
messageTextStyle:
|
||||
TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t),
|
||||
reactionsBackgroundColor: Color.lerp(
|
||||
a.reactionsBackgroundColor, b.reactionsBackgroundColor, t),
|
||||
a.reactionsBackgroundColor,
|
||||
b.reactionsBackgroundColor,
|
||||
t,
|
||||
),
|
||||
reactionsBorderColor:
|
||||
Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t),
|
||||
reactionsMaskColor:
|
||||
|
||||
@@ -48,7 +48,7 @@ class TypingIndicator extends StatelessWidget {
|
||||
.map((e) => e.key)),
|
||||
builder: (context, data) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: data.isNotEmpty == true
|
||||
child: data.isNotEmpty
|
||||
? Padding(
|
||||
key: const Key('main'),
|
||||
padding: padding,
|
||||
|
||||
@@ -92,7 +92,9 @@ class UserAvatar extends StatelessWidget {
|
||||
streamChatTheme
|
||||
.ownMessageTheme.avatarTheme?.borderRadius,
|
||||
image: DecorationImage(
|
||||
image: imageProvider, fit: BoxFit.cover),
|
||||
image: imageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -90,12 +90,13 @@ class UserItem extends StatelessWidget {
|
||||
Widget _buildLastActive(BuildContext context) {
|
||||
final chatTheme = StreamChatTheme.of(context);
|
||||
return Text(
|
||||
user.online == true
|
||||
user.online
|
||||
? context.translations.userOnlineText
|
||||
: '${context.translations.userLastOnlineText} '
|
||||
'${Jiffy(user.lastActive).fromNow()}',
|
||||
style: chatTheme.textTheme.footnote.copyWith(
|
||||
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(.5)),
|
||||
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class UserListView extends StatefulWidget {
|
||||
this.listBuilder,
|
||||
this.userListController,
|
||||
}) : assert(
|
||||
crossAxisCount == 1 || groupAlphabetically == false,
|
||||
crossAxisCount == 1 || !groupAlphabetically,
|
||||
'Cannot group alphabetically when crossAxisCount > 1',
|
||||
),
|
||||
limit = limit ?? pagination?.limit ?? 30,
|
||||
@@ -407,33 +407,34 @@ class _UserListViewState extends State<UserListView>
|
||||
|
||||
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) =>
|
||||
StreamBuilder<bool>(
|
||||
stream: usersProvider.queryUsersLoading,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentError
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: Text(context.translations.loadingUsersError),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
stream: usersProvider.queryUsersLoading,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: snapshot.data!
|
||||
? const CircularProgressIndicator()
|
||||
: Container(),
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentError
|
||||
.withOpacity(0.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: Text(context.translations.loadingUsersError),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: snapshot.data!
|
||||
? const CircularProgressIndicator()
|
||||
: Container(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget _separatorBuilder(context, i) => Container(
|
||||
height: 1,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Displays a list of users who reacted
|
||||
class UserReactionDisplay extends StatelessWidget {
|
||||
/// Constructor for creating a [UserReactionDisplay]
|
||||
const UserReactionDisplay({
|
||||
Key? key,
|
||||
required this.reactionToEmoji,
|
||||
required this.message,
|
||||
this.size = 30,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Reaction map
|
||||
final Map<String, String> reactionToEmoji;
|
||||
|
||||
/// Message which is reacted to
|
||||
final Message message;
|
||||
|
||||
/// Size of Icon
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: Colors.black87,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionToEmoji.keys.map((reactionType) {
|
||||
final firstUserReaction = message.latestReactions!
|
||||
.firstWhere((element) => element.type == reactionType,
|
||||
//ignore: unnecessary_parenthesis
|
||||
orElse: (() => null) as Reaction Function()?);
|
||||
|
||||
if (firstUserReaction.user == null) {
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
icon: Container(),
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
icon: UserAvatar(
|
||||
user: firstUserReaction.user!,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: size - 5,
|
||||
maxWidth: size - 5,
|
||||
),
|
||||
onTap: (user) {},
|
||||
),
|
||||
onPressed: () {},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -29,78 +29,82 @@ Future<bool?> showConfirmationDialog(
|
||||
}) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return showModalBottomSheet(
|
||||
useRootNavigator: false,
|
||||
backgroundColor: chatThemeData.colorTheme.barsBg,
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
useRootNavigator: false,
|
||||
backgroundColor: chatThemeData.colorTheme.barsBg,
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
)),
|
||||
builder: (context) {
|
||||
final effect = chatThemeData.colorTheme.borderTop;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 26),
|
||||
if (icon != null) icon,
|
||||
const SizedBox(height: 26),
|
||||
),
|
||||
),
|
||||
builder: (context) {
|
||||
final effect = chatThemeData.colorTheme.borderTop;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 26),
|
||||
if (icon != null) icon,
|
||||
const SizedBox(height: 26),
|
||||
Text(
|
||||
title,
|
||||
style: chatThemeData.textTheme.headlineBold,
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
if (question != null)
|
||||
Text(
|
||||
title,
|
||||
style: chatThemeData.textTheme.headlineBold,
|
||||
question,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
if (question != null)
|
||||
Text(
|
||||
question,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
Container(
|
||||
color: effect.color!.withOpacity(effect.alpha ?? 1),
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (cancelText != null)
|
||||
Flexible(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(
|
||||
cancelText,
|
||||
style: chatThemeData.textTheme.bodyBold.copyWith(
|
||||
color: chatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
Container(
|
||||
color: effect.color!.withOpacity(effect.alpha ?? 1),
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (cancelText != null)
|
||||
Flexible(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(
|
||||
okText,
|
||||
cancelText,
|
||||
style: chatThemeData.textTheme.bodyBold.copyWith(
|
||||
color: chatThemeData.colorTheme.accentError),
|
||||
color: chatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
Flexible(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(
|
||||
okText,
|
||||
style: chatThemeData.textTheme.bodyBold.copyWith(
|
||||
color: chatThemeData.colorTheme.accentError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows info dialog
|
||||
@@ -119,10 +123,11 @@ Future<bool?> showInfoDialog(
|
||||
theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg,
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
)),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -147,8 +152,8 @@ Future<bool?> showInfoDialog(
|
||||
height: 36,
|
||||
),
|
||||
Container(
|
||||
color: theme?.colorTheme.textHighEmphasis.withOpacity(.08) ??
|
||||
chatThemeData.colorTheme.textHighEmphasis.withOpacity(.08),
|
||||
color: theme?.colorTheme.textHighEmphasis.withOpacity(0.08) ??
|
||||
chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.08),
|
||||
height: 1,
|
||||
),
|
||||
Center(
|
||||
|
||||
@@ -6,11 +6,12 @@ import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
///
|
||||
class IVideoService {
|
||||
IVideoService._();
|
||||
// ignore: prefer-match-file-name
|
||||
class _IVideoService {
|
||||
_IVideoService._();
|
||||
|
||||
/// Singleton instance of [IVideoService]
|
||||
static final IVideoService instance = IVideoService._();
|
||||
/// Singleton instance of [_IVideoService]
|
||||
static final _IVideoService instance = _IVideoService._();
|
||||
final _lock = Lock();
|
||||
|
||||
/// compress video from [path]
|
||||
@@ -64,6 +65,6 @@ class IVideoService {
|
||||
);
|
||||
}
|
||||
|
||||
/// Get instance of [IVideoService]
|
||||
/// Get instance of [_IVideoService]
|
||||
// ignore: non_constant_identifier_names
|
||||
IVideoService get VideoService => IVideoService.instance;
|
||||
_IVideoService get VideoService => _IVideoService.instance;
|
||||
|
||||
@@ -55,10 +55,9 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
dev_dependencies:
|
||||
dart_code_metrics: ^4.4.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
golden_toolkit: ^0.10.0
|
||||
mocktail: ^0.1.2
|
||||
path: ^1.8.0
|
||||
pedantic: ^1.11.0
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
@@ -8,13 +9,21 @@ import 'mocks.dart';
|
||||
void main() {
|
||||
late StreamChatClient client;
|
||||
late Channel channel;
|
||||
late ChannelClientState channelClientState;
|
||||
late ClientState clientState;
|
||||
|
||||
setUp(() {
|
||||
client = MockClient();
|
||||
clientState = MockClientState();
|
||||
when(() => client.state).thenAnswer((_) => clientState);
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'testid'));
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(OwnUser(id: 'testid')));
|
||||
channel = MockChannel();
|
||||
when(() => channel.on(any(), any(), any(), any()))
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
final channelClientState = MockChannelState();
|
||||
channelClientState = MockChannelState();
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.state).thenReturn(channelClientState);
|
||||
when(() => channelClientState.threadsStream)
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
@@ -22,6 +31,17 @@ void main() {
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
when(() => channelClientState.messages).thenReturn([]);
|
||||
when(() => channelClientState.isUpToDate).thenReturn(true);
|
||||
when(() => channelClientState.isUpToDateStream)
|
||||
.thenAnswer((_) => Stream.value(true));
|
||||
when(() => channelClientState.unreadCountStream)
|
||||
.thenAnswer((_) => Stream.value(0));
|
||||
when(() => channelClientState.unreadCount).thenReturn(0);
|
||||
when(() => channelClientState.readStream)
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
when(() => channelClientState.read).thenReturn([]);
|
||||
when(() => channelClientState.membersStream)
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
when(() => channelClientState.members).thenReturn([]);
|
||||
});
|
||||
|
||||
// https://github.com/GetStream/stream-chat-flutter/issues/674
|
||||
@@ -46,4 +66,65 @@ void main() {
|
||||
expect(find.byType(MessageListView), findsOneWidget);
|
||||
expect(find.byKey(emptyWidgetKey), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders a non empty message list view with custom background',
|
||||
(tester) async {
|
||||
final message = Message(
|
||||
id: 'message1',
|
||||
text: 'Hello world!',
|
||||
user: User(
|
||||
id: 'user',
|
||||
name: 'Test User',
|
||||
),
|
||||
);
|
||||
|
||||
when(() => channelClientState.messagesStream).thenAnswer(
|
||||
(_) => Stream.value([message]),
|
||||
);
|
||||
when(() => channelClientState.messages).thenReturn([message]);
|
||||
|
||||
const nonEmptyWidgetKey = Key('non_empty_widget');
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DefaultAssetBundle(
|
||||
bundle: rootBundle,
|
||||
child: StreamChat(
|
||||
client: client,
|
||||
streamChatThemeData: StreamChatThemeData.light().copyWith(
|
||||
messageListViewTheme: const MessageListViewThemeData(
|
||||
backgroundColor: Colors.grey,
|
||||
backgroundImage: DecorationImage(
|
||||
image: AssetImage('images/placeholder.png'),
|
||||
fit: BoxFit.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: const MessageListView(
|
||||
key: nonEmptyWidgetKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
bool findBackground(Widget widget) =>
|
||||
widget is Container &&
|
||||
widget.decoration is BoxDecoration &&
|
||||
(widget.decoration! as BoxDecoration).image != null;
|
||||
|
||||
expect(find.byType(MessageListView), findsOneWidget);
|
||||
expect(find.byKey(nonEmptyWidgetKey), findsOneWidget);
|
||||
expect(
|
||||
find.byWidgetPredicate(
|
||||
findBackground,
|
||||
description: 'findBackground',
|
||||
),
|
||||
findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
class MockClient extends Mock implements StreamChatClient {
|
||||
MockClient() {
|
||||
when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected);
|
||||
when(() => wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.connected));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ final _channelPreviewThemeControl = ChannelPreviewThemeData(
|
||||
color: const Color(0xff7A7A7A),
|
||||
),
|
||||
lastMessageAtStyle: TextTheme.light().footnote.copyWith(
|
||||
color: ColorTheme.light().textHighEmphasis.withOpacity(.5),
|
||||
color: ColorTheme.light().textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
indicatorIconSize: 16,
|
||||
);
|
||||
@@ -83,7 +83,7 @@ final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData(
|
||||
fontSize: 12,
|
||||
),
|
||||
lastMessageAtStyle: TextTheme.light().footnote.copyWith(
|
||||
color: const Color(0x807f7f7f).withOpacity(.5),
|
||||
color: const Color(0x807f7f7f).withOpacity(0.5),
|
||||
),
|
||||
indicatorIconSize: 16,
|
||||
);
|
||||
@@ -102,7 +102,7 @@ final _channelPreviewThemeControlDark = ChannelPreviewThemeData(
|
||||
color: const Color(0xff7A7A7A),
|
||||
),
|
||||
lastMessageAtStyle: TextTheme.dark().footnote.copyWith(
|
||||
color: ColorTheme.dark().textHighEmphasis.withOpacity(.5),
|
||||
color: ColorTheme.dark().textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
indicatorIconSize: 16,
|
||||
);
|
||||
|
||||
@@ -109,6 +109,37 @@ void main() {
|
||||
expect(messageListViewTheme.backgroundColor,
|
||||
_messageListViewThemeDataControlDark.backgroundColor);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Pass backgroundImage to MessageListViewThemeData return backgroundImage',
|
||||
(WidgetTester tester) async {
|
||||
late BuildContext _context;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
builder: (context, child) => StreamChat(
|
||||
client: MockStreamChatClient(),
|
||||
streamChatThemeData: StreamChatThemeData.light()
|
||||
.copyWith(messageListViewTheme: _messageListViewThemeDataImage),
|
||||
child: child,
|
||||
),
|
||||
home: Builder(
|
||||
builder: (BuildContext context) {
|
||||
_context = context;
|
||||
return Scaffold(
|
||||
body: StreamChannel(
|
||||
channel: MockChannel(),
|
||||
child: const MessageListView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final messageListViewTheme = MessageListViewTheme.of(_context);
|
||||
expect(messageListViewTheme.backgroundImage,
|
||||
_messageListViewThemeDataImage.backgroundImage);
|
||||
});
|
||||
}
|
||||
|
||||
final _messageListViewThemeDataControl = MessageListViewThemeData(
|
||||
@@ -122,3 +153,10 @@ const _messageListViewThemeDataControlHalfLerp = MessageListViewThemeData(
|
||||
final _messageListViewThemeDataControlDark = MessageListViewThemeData(
|
||||
backgroundColor: ColorTheme.dark().barsBg,
|
||||
);
|
||||
|
||||
const _messageListViewThemeDataImage = MessageListViewThemeData(
|
||||
backgroundImage: DecorationImage(
|
||||
image: AssetImage('example/assets/background_doodle.png'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user