Merge branch 'develop' into feat/open-library-from-limited-access
This commit is contained in:
@@ -3,6 +3,14 @@
|
||||
✅ Added
|
||||
|
||||
- [[#1087]](https://github.com/GetStream/stream-chat-flutter/issues/1087): Handle limited access to camera on iOS.
|
||||
- `centerTitle` and `elevation` properties to `ChannelHeader`, `ThreadHeader` and `ChannelListHeader`.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#1067]](https://github.com/GetStream/stream-chat-flutter/issues/1067): Fix name text overflow in reaction card.
|
||||
- [[#842]](https://github.com/GetStream/stream-chat-flutter/issues/842): show date divider for first message.
|
||||
- Loosen up url check for attachment download.
|
||||
- Use `ogScrapeUrl` for LinkAttachments.
|
||||
|
||||
## 3.6.1
|
||||
|
||||
@@ -12,7 +20,7 @@
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
-[[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`.
|
||||
- [[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`.
|
||||
- Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput`
|
||||
- Removed dependency on `visibility_detector`
|
||||
|
||||
|
||||
@@ -18,14 +18,11 @@ class AttachmentTitle extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final normalizedTitleLink = attachment.titleLink?.replaceFirst(
|
||||
RegExp(r'https?://(www\.)?'),
|
||||
'',
|
||||
);
|
||||
final ogScrapeUrl = attachment.ogScrapeUrl;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final titleLink = attachment.titleLink;
|
||||
if (titleLink != null) launchURL(context, titleLink);
|
||||
final ogScrapeUrl = attachment.ogScrapeUrl;
|
||||
if (ogScrapeUrl != null) launchURL(context, ogScrapeUrl);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -42,8 +39,8 @@ class AttachmentTitle extends StatelessWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (normalizedTitleLink != null)
|
||||
Text(normalizedTitleLink, style: messageTheme.messageTextStyle),
|
||||
if (ogScrapeUrl != null)
|
||||
Text(ogScrapeUrl, style: messageTheme.messageTextStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -37,11 +37,11 @@ class UrlAttachment extends StatelessWidget {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final titleLink = urlAttachment.titleLink;
|
||||
if (titleLink != null) {
|
||||
final ogScrapeUrl = urlAttachment.ogScrapeUrl;
|
||||
if (ogScrapeUrl != null) {
|
||||
onLinkTap != null
|
||||
? onLinkTap!(titleLink)
|
||||
: launchURL(context, titleLink);
|
||||
? onLinkTap!(ogScrapeUrl)
|
||||
: launchURL(context, ogScrapeUrl);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
||||
@@ -61,9 +61,11 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.showConnectionStateTile = false,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.centerTitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.backgroundColor,
|
||||
this.elevation = 1,
|
||||
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@@ -92,6 +94,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// Subtitle widget
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Whether the title should be centered
|
||||
final bool? centerTitle;
|
||||
|
||||
/// Leading widget
|
||||
final Widget? leading;
|
||||
|
||||
@@ -102,8 +107,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// The background color for this [ChannelHeader].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The elevation for this [ChannelHeader].
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveCenterTitle = getEffectiveCenterTitle(
|
||||
Theme.of(context),
|
||||
actions: actions,
|
||||
centerTitle: centerTitle,
|
||||
);
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final channelHeaderTheme = ChannelHeaderTheme.of(context);
|
||||
|
||||
@@ -144,7 +157,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
systemOverlayStyle: theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
elevation: 1,
|
||||
elevation: elevation,
|
||||
leading: leadingWidget,
|
||||
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
|
||||
actions: actions ??
|
||||
@@ -162,14 +175,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
centerTitle: true,
|
||||
centerTitle: centerTitle,
|
||||
title: InkWell(
|
||||
onTap: onTitleTap,
|
||||
child: SizedBox(
|
||||
height: preferredSize.height,
|
||||
width: preferredSize.width,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: effectiveCenterTitle
|
||||
? CrossAxisAlignment.center
|
||||
: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
title ??
|
||||
ChannelName(
|
||||
|
||||
@@ -56,9 +56,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.showConnectionStateTile = false,
|
||||
this.preNavigationCallback,
|
||||
this.subtitle,
|
||||
this.centerTitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.backgroundColor,
|
||||
this.elevation = 1,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
|
||||
@@ -83,6 +85,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// Subtitle widget
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Whether the title should be centered
|
||||
final bool? centerTitle;
|
||||
|
||||
/// Leading widget
|
||||
/// By default it shows the logged in user avatar
|
||||
final Widget? leading;
|
||||
@@ -94,6 +99,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// The background color for this [ChannelListHeader].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The elevation for this [ChannelListHeader].
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _client = client ?? StreamChat.of(context).client;
|
||||
@@ -128,10 +136,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
systemOverlayStyle: theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
elevation: 1,
|
||||
elevation: elevation,
|
||||
backgroundColor:
|
||||
backgroundColor ?? channelListHeaderThemeData.color,
|
||||
centerTitle: true,
|
||||
centerTitle: centerTitle,
|
||||
leading: leading ??
|
||||
Center(
|
||||
child: user != null
|
||||
|
||||
@@ -225,3 +225,12 @@ extension UserListX on List<User> {
|
||||
return entries.map((e) => e.key).toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions on [Uri]
|
||||
extension UriX on Uri {
|
||||
/// Return the URI adding the http scheme if it is missing
|
||||
Uri get withScheme {
|
||||
if (hasScheme) return this;
|
||||
return Uri.parse('http://${toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1297,7 +1297,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildReplyToMessage() {
|
||||
if (!_hasQuotedMessage) return const Offstage();
|
||||
final containsUrl = widget.quotedMessage!.attachments
|
||||
.any((element) => element.titleLink != null);
|
||||
.any((element) => element.ogScrapeUrl != null);
|
||||
return QuotedMessageWidget(
|
||||
reverse: true,
|
||||
showBorder: !containsUrl,
|
||||
|
||||
@@ -528,6 +528,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return ((index + 2) * 2) - 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
|
||||
@@ -559,6 +560,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
if (widget.reverse
|
||||
? widget.headerBuilder == null
|
||||
: widget.footerBuilder == null) {
|
||||
if (messages.isNotEmpty) {
|
||||
return _buildDateDivider(messages.last);
|
||||
}
|
||||
if (_isThreadConversation) return const Offstage();
|
||||
return const SizedBox(height: 52);
|
||||
}
|
||||
@@ -583,21 +587,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
message = messages[i - 2];
|
||||
nextMessage = messages[i - 1];
|
||||
}
|
||||
|
||||
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
Units.DAY,
|
||||
)) {
|
||||
final divider = widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder!(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
);
|
||||
return divider;
|
||||
return _buildDateDivider(nextMessage);
|
||||
}
|
||||
final timeDiff =
|
||||
Jiffy(nextMessage.createdAt.toLocal()).diff(
|
||||
@@ -747,6 +742,20 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildDateDivider(Message message) {
|
||||
final divider = widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder!(
|
||||
message.createdAt.toLocal(),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: DateDivider(
|
||||
dateTime: message.createdAt.toLocal(),
|
||||
),
|
||||
);
|
||||
return divider;
|
||||
}
|
||||
|
||||
Widget _buildThreadSeparator() {
|
||||
if (widget.threadSeparatorBuilder != null) {
|
||||
return widget.threadSeparatorBuilder!.call(context);
|
||||
@@ -803,7 +812,11 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
index = _getBottomElementIndex(values);
|
||||
}
|
||||
|
||||
if (index == null) return const Offstage();
|
||||
if ((index == null) ||
|
||||
(!_isThreadConversation && index == itemCount - 2) ||
|
||||
(_isThreadConversation && index == itemCount - 1)) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
if (index <= 2 || index >= itemCount - 3) {
|
||||
if (widget.reverse) {
|
||||
@@ -1079,7 +1092,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
|
||||
final hasUrlAttachment =
|
||||
message.attachments.any((it) => it.titleLink != null);
|
||||
message.attachments.any((it) => it.ogScrapeUrl != null);
|
||||
|
||||
final borderSide =
|
||||
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
|
||||
|
||||
@@ -236,6 +236,8 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
reaction.user!.name.split(' ')[0],
|
||||
style: chatThemeData.textTheme.footnoteBold,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -574,11 +574,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
|
||||
|
||||
bool get hasNonUrlAttachments => widget.message.attachments
|
||||
.where((it) => it.titleLink == null || it.type == 'giphy')
|
||||
.where((it) => it.ogScrapeUrl == null || it.type == 'giphy')
|
||||
.isNotEmpty;
|
||||
|
||||
bool get hasUrlAttachments => widget.message.attachments
|
||||
.any((it) => it.titleLink != null && it.type != 'giphy');
|
||||
.any((it) => it.ogScrapeUrl != null && it.type != 'giphy');
|
||||
|
||||
bool get showBottomRow =>
|
||||
showThreadReplyIndicator ||
|
||||
@@ -999,9 +999,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
|
||||
Widget _buildUrlAttachment() {
|
||||
final urlAttachment = widget.message.attachments
|
||||
.firstWhere((element) => element.titleLink != null);
|
||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||
|
||||
final host = Uri.parse(urlAttachment.titleLink!).host;
|
||||
final host = Uri.parse(urlAttachment.ogScrapeUrl!).withScheme.host;
|
||||
final splitList = host.split('.');
|
||||
final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||
@@ -1173,7 +1173,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
|
||||
widget.message.attachments
|
||||
.where((element) =>
|
||||
(element.titleLink == null && element.type != null) ||
|
||||
(element.ogScrapeUrl == null && element.type != null) ||
|
||||
element.type == 'giphy')
|
||||
.forEach((e) {
|
||||
if (attachmentGroups[e.type] == null) {
|
||||
|
||||
@@ -97,7 +97,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
bool get _hasAttachments => message.attachments.isNotEmpty;
|
||||
|
||||
bool get _containsLinkAttachment =>
|
||||
message.attachments.any((element) => element.titleLink != null);
|
||||
message.attachments.any((element) => element.ogScrapeUrl != null);
|
||||
|
||||
bool get _containsText => message.text?.isNotEmpty == true;
|
||||
|
||||
@@ -201,7 +201,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
Attachment attachment;
|
||||
if (_containsLinkAttachment) {
|
||||
attachment = message.attachments.firstWhere(
|
||||
(element) => element.titleLink != null,
|
||||
(element) => element.ogScrapeUrl != null,
|
||||
);
|
||||
child = _buildUrlAttachment(attachment);
|
||||
} else {
|
||||
|
||||
@@ -65,11 +65,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.onBackPressed,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.centerTitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.onTitleTap,
|
||||
this.showTypingIndicator = true,
|
||||
this.backgroundColor,
|
||||
this.elevation = 1,
|
||||
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@@ -92,6 +94,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// Subtitle widget
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Whether the title should be centered
|
||||
final bool? centerTitle;
|
||||
|
||||
/// Leading widget
|
||||
final Widget? leading;
|
||||
|
||||
@@ -105,8 +110,17 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// The background color of this [ThreadHeader].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The elevation for this [ThreadHeader].
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveCenterTitle = getEffectiveCenterTitle(
|
||||
Theme.of(context),
|
||||
actions: actions,
|
||||
centerTitle: centerTitle,
|
||||
);
|
||||
|
||||
final channelHeaderTheme = ChannelHeaderTheme.of(context);
|
||||
|
||||
final defaultSubtitle = subtitle ??
|
||||
@@ -134,7 +148,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
systemOverlayStyle: theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
elevation: 1,
|
||||
elevation: elevation,
|
||||
leading: leading ??
|
||||
(showBackButton
|
||||
? StreamBackButton(
|
||||
@@ -144,7 +158,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
)
|
||||
: const SizedBox()),
|
||||
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
|
||||
centerTitle: true,
|
||||
centerTitle: centerTitle,
|
||||
actions: actions,
|
||||
title: InkWell(
|
||||
onTap: onTitleTap,
|
||||
@@ -153,6 +167,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
width: 250,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: effectiveCenterTitle
|
||||
? CrossAxisAlignment.center
|
||||
: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
title ??
|
||||
Text(
|
||||
|
||||
@@ -47,6 +47,12 @@ class TypingIndicator extends StatelessWidget {
|
||||
.where((element) => element.value.parentId == parentId)
|
||||
.map((e) => e.key)),
|
||||
builder: (context, data) => AnimatedSwitcher(
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: data.isNotEmpty
|
||||
? Padding(
|
||||
|
||||
@@ -8,15 +8,37 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// Launch URL
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
try {
|
||||
await launch(Uri.parse(url).withScheme.toString());
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.translations.launchUrlError)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get centerTitle considering a default and platform specific behaviour
|
||||
bool getEffectiveCenterTitle(
|
||||
ThemeData theme, {
|
||||
bool? centerTitle,
|
||||
List<Widget>? actions,
|
||||
}) {
|
||||
if (centerTitle != null) return centerTitle;
|
||||
if (theme.appBarTheme.centerTitle != null) {
|
||||
return theme.appBarTheme.centerTitle!;
|
||||
}
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return false;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return actions == null || actions.length < 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows confirmation dialog
|
||||
Future<bool?> showConfirmationDialog(
|
||||
BuildContext context, {
|
||||
|
||||
Reference in New Issue
Block a user