diff --git a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx index 9e2afb44..96d24a05 100644 --- a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx +++ b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx @@ -64,30 +64,34 @@ The second type looks like this: We can use a `Stack` for achieving this: ```dart -Scaffold( - body: Stack( - children: [ - // Add your video implementation here - ShaderMask( - shaderCallback: (rect) { - return LinearGradient( +Stack( + children: [ + // Add your video implementation here + ShaderMask( + shaderCallback: (rect) { + return const LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, - colors: [Colors.black, Colors.transparent], - stops: [0.4, 0.65] - ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); - }, - blendMode: BlendMode.dstIn, - child: Column( - children: [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ), - ], - ), - ) -``` + colors: [Colors.black, Colors.transparent], + stops: [0.4, 0.8]).createShader( + Rect.fromLTRB(0, 0, rect.width, rect.height), + ); + }, + blendMode: BlendMode.dstIn, + child: Column( + children: const [ + Expanded( + child: MessageListViewTheme( + data: MessageListViewThemeData( + backgroundColor: Colors.transparent, + ), + child: MessageListView(), + ), + ), + MessageInput(), + ], + ), + ), + ], +), +``` \ No newline at end of file diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx index 285c2205..917d9216 100644 --- a/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx +++ b/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx @@ -86,6 +86,14 @@ firebaseMessaging.onTokenRefresh.listen((token) { }); ``` +Push Notifications v2 also supports specifying a name to the push device tokens you register. By setting the optional `pushProviderName` param in the `addDevice` call you can support different configurations between the device and the `PushProvider`. + +```dart +firebaseMessaging.onTokenRefresh.listen((token) { + client.addDevice(token, PushProvider.firebase, pushProviderName: 'my-custom-config'); +}); +``` + ### Receiving Notifications Push notifications behave a bit differently depending on whether you are using iOS or Android. diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f0707cdb..0680109c 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- Added `push_provider_name` to `addDevice` API call + ## 3.6.1 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index f61560ef..69fd3180 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -64,7 +64,7 @@ class StreamChatClient { StreamChatClient( String apiKey, { this.logLevel = Level.WARNING, - LogHandlerFunction? logHandlerFunction, + this.logHandlerFunction = StreamChatClient.defaultLogHandler, RetryPolicy? retryPolicy, @Deprecated(''' Location is now deprecated in favor of the new edge server. Will be removed in v4.0.0. @@ -77,7 +77,6 @@ class StreamChatClient { WebSocket? ws, AttachmentFileUploader? attachmentFileUploader, }) { - this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler; logger.info('Initiating new StreamChatClient'); final options = StreamHttpClientOptions( @@ -134,7 +133,7 @@ class StreamChatClient { '${CurrentPlatform.name}-' '${PACKAGE_VERSION.split('+')[0]}'; - /// Additionals headers for all requests + /// Additional headers for all requests static Map additionalHeaders = {}; ChatPersistenceClient? _originalChatPersistenceClient; @@ -189,7 +188,7 @@ class StreamChatClient { /// final client = StreamChatClient("stream-chat-api-key", /// logHandlerFunction: myLogHandlerFunction); ///``` - late LogHandlerFunction logHandlerFunction; + final LogHandlerFunction logHandlerFunction; StreamSubscription? _connectionStatusSubscription; @@ -214,17 +213,18 @@ class StreamChatClient { Stream get wsConnectionStatusStream => _wsConnectionStatusController.stream.distinct(); - LogHandlerFunction get _defaultLogHandler => (LogRecord record) { - print( - '${record.time} ' - '${_levelEmojiMapper[record.level] ?? record.level.name} ' - '${record.loggerName} ${record.message} ', - ); - if (record.error != null) print(record.error); - if (record.stackTrace != null) print(record.stackTrace); - }; + /// Default log handler function for the [StreamChatClient] logger. + static void defaultLogHandler(LogRecord record) { + print( + '${record.time} ' + '${_levelEmojiMapper[record.level] ?? record.level.name} ' + '${record.loggerName} ${record.message} ', + ); + if (record.error != null) print(record.error); + if (record.stackTrace != null) print(record.stackTrace); + } - /// + /// Default logger for the [StreamChatClient]. Logger detachedLogger(String name) => Logger.detached(name) ..level = logLevel ..onRecord.listen(logHandlerFunction); @@ -818,8 +818,16 @@ class StreamChatClient { ); /// Add a device for Push Notifications. - Future addDevice(String id, PushProvider pushProvider) => - _chatApi.device.addDevice(id, pushProvider); + Future addDevice( + String id, + PushProvider pushProvider, { + String? pushProviderName, + }) => + _chatApi.device.addDevice( + id, + pushProvider, + pushProviderName: pushProviderName, + ); /// Gets a list of user devices. Future getDevices() => _chatApi.device.getDevices(); diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart index f4b7f0b1..df137cdc 100644 --- a/packages/stream_chat/lib/src/core/api/device_api.dart +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -29,13 +29,16 @@ class DeviceApi { /// Add a device for Push Notifications. Future addDevice( String deviceId, - PushProvider pushProvider, - ) async { + PushProvider pushProvider, { + String? pushProviderName, + }) async { final response = await _client.post( '/devices', data: { 'id': deviceId, 'push_provider': pushProvider.name, + if (pushProviderName != null && pushProviderName.isNotEmpty) + 'push_provider_name': pushProviderName, }, ); return EmptyResponse.fromJson(response.data); diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 571d78f5..794d7505 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -5,37 +5,36 @@ export 'package:dio/src/dio_error.dart'; export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/options.dart'; export 'package:dio/src/options.dart' show ProgressCallback; -export 'package:logging/logging.dart' show Logger, Level; +export 'package:logging/logging.dart' show Logger, Level, LogRecord; export 'package:rate_limiter/rate_limiter.dart'; -export './src/core/api/attachment_file_uploader.dart' - show AttachmentFileUploader; -export './src/core/api/requests.dart'; -export './src/core/api/requests.dart'; -export './src/core/api/responses.dart'; -export './src/core/api/stream_chat_api.dart' show PushProvider; -export './src/core/error/error.dart'; -export './src/core/models/action.dart'; -export './src/core/models/attachment.dart'; -export './src/core/models/attachment_file.dart'; -export './src/core/models/channel_config.dart'; -export './src/core/models/channel_model.dart'; -export './src/core/models/channel_state.dart'; -export './src/core/models/command.dart'; -export './src/core/models/device.dart'; -export './src/core/models/event.dart'; -export './src/core/models/filter.dart' show Filter; -export './src/core/models/member.dart'; -export './src/core/models/message.dart'; -export './src/core/models/mute.dart'; -export './src/core/models/own_user.dart'; -export './src/core/models/reaction.dart'; -export './src/core/models/read.dart'; -export './src/core/models/user.dart'; -export './src/core/util/extension.dart'; -export './src/db/chat_persistence_client.dart'; -export './src/event_type.dart'; -export './src/location.dart'; -export './src/ws/connection_status.dart'; export 'src/client/channel.dart'; export 'src/client/client.dart'; +export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; +export 'src/core/api/requests.dart'; +export 'src/core/api/requests.dart'; +export 'src/core/api/responses.dart'; +export 'src/core/api/stream_chat_api.dart' show PushProvider; +export 'src/core/error/error.dart'; +export 'src/core/models/action.dart'; +export 'src/core/models/attachment.dart'; +export 'src/core/models/attachment_file.dart'; +export 'src/core/models/channel_config.dart'; +export 'src/core/models/channel_model.dart'; +export 'src/core/models/channel_state.dart'; +export 'src/core/models/command.dart'; +export 'src/core/models/device.dart'; +export 'src/core/models/event.dart'; +export 'src/core/models/filter.dart' show Filter; +export 'src/core/models/member.dart'; +export 'src/core/models/message.dart'; +export 'src/core/models/mute.dart'; +export 'src/core/models/own_user.dart'; +export 'src/core/models/reaction.dart'; +export 'src/core/models/read.dart'; +export 'src/core/models/user.dart'; +export 'src/core/util/extension.dart'; +export 'src/db/chat_persistence_client.dart'; +export 'src/event_type.dart'; +export 'src/location.dart'; +export 'src/ws/connection_status.dart'; diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 5ec2bfeb..4947bc72 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -1171,7 +1171,7 @@ void main() { verifyNoMoreInteractions(api.channel); }); - test('`.addDevice`', () async { + test('`.addDevice should work`', () async { const id = 'test-device-id'; const provider = PushProvider.firebase; @@ -1185,6 +1185,34 @@ void main() { verifyNoMoreInteractions(api.device); }); + test('`.addDevice should work with pushProviderName`', () async { + const id = 'test-device-id'; + const provider = PushProvider.firebase; + const pushProviderName = 'my-custom-config'; + + when( + () => api.device.addDevice( + id, + provider, + pushProviderName: pushProviderName, + ), + ).thenAnswer((_) async => EmptyResponse()); + + final res = await client.addDevice( + id, + provider, + pushProviderName: pushProviderName, + ); + expect(res, isNotNull); + + verify(() => api.device.addDevice( + id, + provider, + pushProviderName: pushProviderName, + )).called(1); + verifyNoMoreInteractions(api.device); + }); + test('`.getDevices`', () async { final devices = List.generate( 3, diff --git a/packages/stream_chat/test/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart index 7d4f59fd..13cfe4dc 100644 --- a/packages/stream_chat/test/src/core/api/device_api_test.dart +++ b/packages/stream_chat/test/src/core/api/device_api_test.dart @@ -20,7 +20,7 @@ void main() { deviceApi = DeviceApi(client); }); - test('addDevice', () async { + test('addDevice should work', () async { const deviceId = 'test-device-id'; const pushProvider = PushProvider.firebase; @@ -44,6 +44,36 @@ void main() { verifyNoMoreInteractions(client); }); + test('addDevice should work with pushProviderName', () async { + const deviceId = 'test-device-id'; + const pushProvider = PushProvider.firebase; + const pushProviderName = 'my-custom-config'; + + const path = '/devices'; + + when(() => client.post( + path, + data: { + 'id': deviceId, + 'push_provider': pushProvider.name, + 'push_provider_name': pushProviderName, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await deviceApi.addDevice( + deviceId, + pushProvider, + pushProviderName: pushProviderName, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + test('getDevices', () async { const path = '/devices'; diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b7a90144..b7d57481 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -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` diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index f1369d49..4a85f2bb 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -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), ], ), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index df05ba5e..8d4dfbfb 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -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( diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 954aa758..2716ea00 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -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: [ title ?? ChannelName( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 6c54ad89..fbe71ff7 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -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 diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index f955ca99..cc41614c 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -225,3 +225,12 @@ extension UserListX on List { 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()}'); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 43528f73..19e9574b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1297,7 +1297,7 @@ class MessageInputState extends State { 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, diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 0b432caa..ed1ea56e 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -528,6 +528,7 @@ class _MessageListViewState extends State { 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 { 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 { 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 { 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 { 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 { 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) diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 98e64c25..2127a98d 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -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, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 3ffca84a..812a7595 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -574,11 +574,11 @@ class _MessageWidgetState extends State 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 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 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) { diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 131ca60b..8994eece 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -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 { diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index aae1d8f9..199b8a44 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -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( diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index d961a8ff..5bf7358a 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -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: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ), duration: const Duration(milliseconds: 300), child: data.isNotEmpty ? Padding( diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 2d661fbc..3d012194 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -8,15 +8,37 @@ import 'package:url_launcher/url_launcher.dart'; /// Launch URL Future 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? 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 showConfirmationDialog( BuildContext context, { diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 0c7ce68e..16838cf5 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -74,6 +74,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { return const StreamChatLocalizationsKo(); case 'pt': return const StreamChatLocalizationsPt(); + default: + return null; } } diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 2dc61f18..f8dfe9e5 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart' show LogRecord; import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart';