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 cc6e868a..9a2863b6 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,35 @@ 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: StreamMessageListView(), - ), - StreamMessageInput(), - ], - ), - ), - ], - ), - ) + 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: StreamMessageListViewTheme( + data: StreamMessageListViewThemeData( + backgroundColor: Colors.transparent, + ), + child: StreamMessageListView(), + ), + ), + StreamMessageInput(), + ], + ), + ), + ], +), ``` + 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 a390a823..0965cd2e 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 + ## 4.0.0-beta.2 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 6e9ecc33..ee805b56 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); @@ -820,8 +820,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/src/core/models/attachment_file.freezed.dart b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart index c524a742..634af64f 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart @@ -1,5 +1,6 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target part of 'attachment_file.dart'; @@ -324,13 +325,15 @@ class _$InProgress implements InProgress { return identical(this, other) || (other.runtimeType == runtimeType && other is InProgress && - (identical(other.uploaded, uploaded) || - other.uploaded == uploaded) && - (identical(other.total, total) || other.total == total)); + const DeepCollectionEquality().equals(other.uploaded, uploaded) && + const DeepCollectionEquality().equals(other.total, total)); } @override - int get hashCode => Object.hash(runtimeType, uploaded, total); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(uploaded), + const DeepCollectionEquality().hash(total)); @JsonKey(ignore: true) @override @@ -612,11 +615,12 @@ class _$Failed implements Failed { return identical(this, other) || (other.runtimeType == runtimeType && other is Failed && - (identical(other.error, error) || other.error == error)); + const DeepCollectionEquality().equals(other.error, error)); } @override - int get hashCode => Object.hash(runtimeType, error); + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(error)); @JsonKey(ignore: true) @override diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index cb2f81c2..c3e83435 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -13,7 +13,7 @@ class ChannelModel { String? id, String? type, String? cid, - this.ownCapabilities = const [], + this.ownCapabilities, ChannelConfig? config, this.createdBy, this.frozen = false, @@ -54,7 +54,7 @@ class ChannelModel { /// List of user permissions on this channel @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final List ownCapabilities; + final List? ownCapabilities; /// The channel configuration data @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) diff --git a/packages/stream_chat/lib/src/core/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index 974ef383..9bd8f062 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -11,9 +11,8 @@ ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( type: json['type'] as String?, cid: json['cid'] as String?, ownCapabilities: (json['own_capabilities'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], + ?.map((e) => e as String) + .toList(), config: json['config'] == null ? null : ChannelConfig.fromJson(json['config'] as Map), diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index c0ceab3a..0fce9d63 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -5,7 +5,7 @@ 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 'package:uuid/uuid.dart'; @@ -41,3 +41,31 @@ export './src/permission_type.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 ef980e29..cc865b2e 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,16 @@ +## Upcoming + +✅ 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. ## 4.0.0-beta.2 ✅ Added diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 209af7ea..376b0ba8 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -145,7 +145,7 @@ class ThreadPage extends StatelessWidget { ), ), StreamMessageInput( - messageInputController: MessageInputController( + messageInputController: StreamMessageInputController( message: Message(parentId: parent!.id), ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index a872fb6e..e9c0168f 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -184,7 +184,7 @@ class ThreadPage extends StatelessWidget { ), ), StreamMessageInput( - messageInputController: MessageInputController( + messageInputController: StreamMessageInputController( message: Message(parentId: parent!.id), ), ), 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 b1aba4a2..6852cdb6 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -24,14 +24,11 @@ class StreamAttachmentTitle 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), @@ -48,8 +45,8 @@ class StreamAttachmentTitle 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 25cfb5ba..00f29c02 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -43,11 +43,11 @@ class StreamUrlAttachment 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 9da8b4f8..aea899bd 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -68,9 +68,11 @@ class StreamChannelHeader extends StatelessWidget 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); @@ -99,6 +101,9 @@ class StreamChannelHeader extends StatelessWidget /// Subtitle widget final Widget? subtitle; + /// Whether the title should be centered + final bool? centerTitle; + /// Leading widget final Widget? leading; @@ -109,8 +114,16 @@ class StreamChannelHeader extends StatelessWidget /// The background color for this [StreamChannelHeader]. final Color? backgroundColor; + /// The elevation for this [StreamChannelHeader]. + 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 = StreamChannelHeaderTheme.of(context); @@ -151,7 +164,7 @@ class StreamChannelHeader extends StatelessWidget systemOverlayStyle: theme.brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, - elevation: 1, + elevation: elevation, leading: leadingWidget, backgroundColor: backgroundColor ?? channelHeaderTheme.color, actions: actions ?? @@ -170,14 +183,16 @@ class StreamChannelHeader extends StatelessWidget ), ), ], - 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 ?? StreamChannelName( 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 f814a08e..4835cad0 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -62,9 +62,11 @@ class StreamChannelListHeader extends StatelessWidget 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. @@ -89,6 +91,9 @@ class StreamChannelListHeader extends StatelessWidget /// 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; @@ -100,6 +105,9 @@ class StreamChannelListHeader extends StatelessWidget /// The background color for this [StreamChannelListHeader]. final Color? backgroundColor; + /// The elevation for this [StreamChannelListHeader]. + final double elevation; + @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; @@ -135,10 +143,10 @@ class StreamChannelListHeader extends StatelessWidget 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/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 819ed40f..f70badc1 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,3 +1,6 @@ +// ignore: lines_longer_than_80_chars +// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use + import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index a0c9d288..5ad30039 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -272,3 +272,12 @@ extension MessageX on Message { return copyWith(text: messageTextToSend); } } + +/// 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/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 37809b40..340392b7 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -5,7 +5,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; -import 'package:stream_chat_flutter/src/stream_attachment_package.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 83682c6d..3d12b73b 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -1,7 +1,6 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/connection_status_builder.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; -import 'package:stream_chat_flutter/src/message_search_list_view.dart'; import 'package:stream_chat_flutter/src/v4/message_input/stream_message_input.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' show User; @@ -321,6 +320,9 @@ abstract class Translations { /// The label for "Reply to message" String get replyToMessageLabel; + /// The label for "View library" + String get viewLibrary; + /// Label for "Attachment limit exceeded: /// it's not possible to add more than $limit attachments" String attachmentLimitExceedError(int limit); @@ -696,6 +698,9 @@ class DefaultTranslations implements Translations { @override String get slowModeOnLabel => 'Slow mode ON'; + @override + String get viewLibrary => 'View library'; + @override String attachmentLimitExceedError(int limit) => """ Attachment limit exceeded: it's not possible to add more than $limit attachments"""; diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart index c2404208..72378c1c 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@macro media_list_view} @@ -20,6 +21,7 @@ class StreamMediaListView extends StatefulWidget { Key? key, this.selectedIds = const [], this.onSelect, + this.controller, }) : super(key: key); /// Stores the media selected @@ -28,18 +30,28 @@ class StreamMediaListView extends StatefulWidget { /// Callback for on media selected final void Function(AssetEntity media)? onSelect; + /// Controller that handles MediaListView + final MediaListViewController? controller; + @override _StreamMediaListViewState createState() => _StreamMediaListViewState(); } class _StreamMediaListViewState extends State { - final _media = []; - final ScrollController _scrollController = ScrollController(); - int _currentPage = 0; + var _media = []; + var _currentPage = 0; + final _scrollController = ScrollController(); + + /// Controller necessary to verify limited access to photo gallery in iOS and + /// update the media list when listerners are emitted + late final controller = widget.controller ?? MediaListViewController(); @override Widget build(BuildContext context) => LazyLoadScrollView( - onEndOfPage: () async => _getMedia(), + onEndOfPage: () async { + await _getMedia(); + _updatePage(); + }, child: GridView.builder( itemCount: _media.length, controller: _scrollController, @@ -136,9 +148,29 @@ class _StreamMediaListViewState extends State { @override void initState() { super.initState(); + controller.addListener(_updateMediaList); _getMedia(); } + @override + void dispose() { + super.dispose(); + controller.removeListener(_updateMediaList); + if (widget.controller == null) { + controller.dispose(); + } + } + + void _updateMediaList() { + if (controller.shouldUpdateMedia) { + _getMedia(); + } + } + + void _updatePage() { + ++_currentPage; + } + Future _getMedia() async { final assetList = (await PhotoManager.getAssetPathList( filterOption: FilterOptionGroup( @@ -157,13 +189,11 @@ class _StreamMediaListViewState extends State { page: _currentPage, size: 50, ); - if (media?.isNotEmpty == true) { setState(() { - _media.addAll(media!); + _media = media!; }); } - ++_currentPage; } } diff --git a/packages/stream_chat_flutter/lib/src/media_list_view_controller.dart b/packages/stream_chat_flutter/lib/src/media_list_view_controller.dart new file mode 100644 index 00000000..9ec3f594 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/media_list_view_controller.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +/// Controller for MediaListView Widget +class MediaListViewController extends ChangeNotifier { + var _shouldUpdateMedia = false; + + /// Getter that knows if the media should be updated. + bool get shouldUpdateMedia => _shouldUpdateMedia; + + /// Method that update shouldUpdateMedia and notify all listeners + /// about this update. + void updateMedia({required bool newValue}) { + _shouldUpdateMedia = newValue; + notifyListeners(); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 056e84fc..ffd8f386 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -639,7 +639,7 @@ class _StreamMessageActionsModalState extends State { widget.editMessageInputBuilder!(context, widget.message) else StreamMessageInput( - messageInputController: MessageInputController( + messageInputController: StreamMessageInputController( message: widget.message, ), preMessageSending: (m) { diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 1dd27661..b830ab37 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:async'; import 'dart:math'; @@ -15,7 +17,7 @@ import 'package:stream_chat_flutter/src/emoji/emoji.dart'; 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/multi_overlay.dart'; +import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/user_mentions_overlay.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; @@ -333,6 +335,7 @@ class MessageInputState extends State { final List _mentionedUsers = []; final _imagePicker = ImagePicker(); + final _mediaListViewController = MediaListViewController(); late final _focusNode = widget.focusNode ?? FocusNode(); late final _isInternalFocusNode = widget.focusNode == null; bool _inputEnabled = true; @@ -1046,6 +1049,26 @@ class MessageInputState extends State { ); }, ), + const Spacer(), + FutureBuilder( + future: PhotoManager.requestPermissionExtend(), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data == PermissionState.limited) { + return TextButton( + child: Text(context.translations.viewLibrary), + onPressed: () async { + await PhotoManager.presentLimited(); + _mediaListViewController.updateMedia( + newValue: true, + ); + }, + ); + } + + return const SizedBox.shrink(); + }, + ), ], ), DecoratedBox( @@ -1078,6 +1101,7 @@ class MessageInputState extends State { borderRadius: BorderRadius.circular(8), ), child: _PickerWidget( + mediaListViewController: _mediaListViewController, filePickerIndex: _filePickerIndex, streamChatTheme: _streamChatTheme, containsFile: _attachmentContainsFile, @@ -1241,7 +1265,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 StreamQuotedMessageWidget( reverse: true, showBorder: !containsUrl, @@ -1909,6 +1933,7 @@ class _PickerWidget extends StatefulWidget { required this.onAddMoreFilesClick, required this.onMediaSelected, required this.streamChatTheme, + required this.mediaListViewController, }) : super(key: key); final int filePickerIndex; @@ -1917,6 +1942,7 @@ class _PickerWidget extends StatefulWidget { final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; final void Function(AssetEntity) onMediaSelected; final StreamChatThemeData streamChatTheme; + final MediaListViewController mediaListViewController; @override _PickerWidgetState createState() => _PickerWidgetState(); @@ -1964,7 +1990,9 @@ class _PickerWidgetState extends State<_PickerWidget> { ), ); } + return StreamMediaListView( + controller: widget.mediaListViewController, selectedIds: widget.selectedMedias, onSelect: widget.onMediaSelected, ); 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 e6bbba99..7d77b2ea 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -579,6 +579,9 @@ class _StreamMessageListViewState 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); } @@ -603,21 +606,12 @@ class _StreamMessageListViewState 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: StreamDateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ); - return divider; + return _buildDateDivider(nextMessage); } final timeDiff = Jiffy(nextMessage.createdAt.toLocal()).diff( @@ -769,6 +763,20 @@ class _StreamMessageListViewState 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: StreamDateDivider( + dateTime: message.createdAt.toLocal(), + ), + ); + return divider; + } + Widget _buildThreadSeparator() { if (widget.threadSeparatorBuilder != null) { return widget.threadSeparatorBuilder!.call(context); @@ -825,7 +833,11 @@ class _StreamMessageListViewState 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) { @@ -1109,7 +1121,7 @@ class _StreamMessageListViewState 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 eceba3f2..f862048b 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -246,6 +246,8 @@ class StreamMessageReactionsModal 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_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 2554b242..4cc436f4 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -1,3 +1,6 @@ +// ignore: lines_longer_than_80_chars +// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 3108803e..fd710c8e 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -581,11 +581,11 @@ class _StreamMessageWidgetState 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 || @@ -1006,9 +1006,9 @@ class _StreamMessageWidgetState 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() ?? @@ -1176,7 +1176,7 @@ class _StreamMessageWidgetState 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 81c8b892..1a7bb2d0 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -57,7 +57,7 @@ class StreamQuotedMessageWidget 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; @@ -161,7 +161,7 @@ class StreamQuotedMessageWidget 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/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 4ece0bf2..32b4830f 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -414,7 +414,7 @@ class StreamChatThemeData { /// Theme configuration for the [StreamUserListView] widget. final StreamUserListViewThemeData userListViewTheme; - /// Theme configuration for the [MessageSearchListView] widget. + /// Theme configuration for the [StreamMessageSearchListView] widget. final StreamMessageSearchListViewThemeData messageSearchListViewTheme; /// Creates a copy of [StreamChatThemeData] with specified attributes diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 052e1bcb..f6f77a32 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -72,11 +72,13 @@ class StreamThreadHeader extends StatelessWidget 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); @@ -99,6 +101,9 @@ class StreamThreadHeader extends StatelessWidget /// Subtitle widget final Widget? subtitle; + /// Whether the title should be centered + final bool? centerTitle; + /// Leading widget final Widget? leading; @@ -112,8 +117,17 @@ class StreamThreadHeader extends StatelessWidget /// The background color of this [StreamThreadHeader]. final Color? backgroundColor; + /// The elevation for this [StreamThreadHeader]. + final double elevation; + @override Widget build(BuildContext context) { + final effectiveCenterTitle = getEffectiveCenterTitle( + Theme.of(context), + actions: actions, + centerTitle: centerTitle, + ); + final channelHeaderTheme = StreamChannelHeaderTheme.of(context); final defaultSubtitle = subtitle ?? @@ -126,7 +140,8 @@ class StreamThreadHeader extends StatelessWidget style: channelHeaderTheme.subtitleStyle, ), Flexible( - child: ChannelName( + child: StreamChannelName( + channel: StreamChannel.of(context).channel, textStyle: channelHeaderTheme.subtitleStyle, ), ), @@ -141,7 +156,7 @@ class StreamThreadHeader extends StatelessWidget systemOverlayStyle: theme.brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, - elevation: 1, + elevation: elevation, leading: leading ?? (showBackButton ? StreamBackButton( @@ -151,7 +166,7 @@ class StreamThreadHeader extends StatelessWidget ) : const SizedBox()), backgroundColor: backgroundColor ?? channelHeaderTheme.color, - centerTitle: true, + centerTitle: centerTitle, actions: actions, title: InkWell( onTap: onTitleTap, @@ -160,6 +175,9 @@ class StreamThreadHeader extends StatelessWidget 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 1cb3d8eb..1ac99785 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -49,6 +49,12 @@ class StreamTypingIndicator extends StatelessWidget { .where((element) => element.value.parentId == parentId) .map((e) => e.key)), builder: (context, users) => AnimatedSwitcher( + layoutBuilder: (currentChild, previousChildren) => Stack( + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ), duration: const Duration(milliseconds: 300), child: users.isNotEmpty ? Padding( diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 6f1804e7..2b5b868e 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -1,3 +1,6 @@ +// ignore: lines_longer_than_80_chars +// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 74c721c9..6f68d287 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -2,22 +2,43 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_attachment_package.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; 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, { @@ -433,8 +454,8 @@ int levenshtein(String s, String t, {bool caseSensitive = true}) { /// An easy way to handle attachment related operations on a message extension AttachmentPackagesX on Message { - /// This extension will return a List of type [StreamAttachmentPackage] from the - /// existing attachments of the message + /// This extension will return a List of type [StreamAttachmentPackage] + /// from the existing attachments of the message List getAttachmentPackageList() { final _attachmentPackages = List.generate( attachments.length, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart deleted file mode 100644 index 69a6cf6e..00000000 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; - -/// A shimmering list item which shows a loading effect. -/// -/// This is used by [StreamChannelListView] to show a loading effect while -/// the list is being loaded. -class StreamChannelListLoadingTile extends StatelessWidget { - /// Creates a new instance of [StreamChannelListLoadingTile] widget. - const StreamChannelListLoadingTile({ - Key? key, - this.visualDensity = VisualDensity.standard, - this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), - }) : super(key: key); - - /// Defines how compact the list tile's layout will be. - /// - /// {@macro flutter.material.themedata.visualDensity} - /// - /// See also: - /// - /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all - /// widgets within a [Theme]. - final VisualDensity visualDensity; - - /// The tile's internal padding. - /// - /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], - /// and [trailing] widgets. - /// - /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. - final EdgeInsetsGeometry contentPadding; - - @override - Widget build(BuildContext context) { - final colorTheme = StreamChatTheme.of(context).colorTheme; - - final leading = Container( - height: 49, - width: 49, - decoration: BoxDecoration( - color: colorTheme.barsBg, - shape: BoxShape.circle, - ), - ); - - final title = Container( - height: 16, - width: 66, - decoration: BoxDecoration( - color: colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - ); - - final subtitle = Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Container( - height: 16, - decoration: BoxDecoration( - color: colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - const SizedBox(width: 8), - Container( - height: 16, - width: 50, - decoration: BoxDecoration( - color: colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - ), - ], - ); - - return Shimmer.fromColors( - baseColor: colorTheme.disabled, - highlightColor: colorTheme.inputBg, - child: ListTile( - leading: leading, - title: title, - subtitle: subtitle, - visualDensity: visualDensity, - contentPadding: contentPadding, - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart index cb3859be..c6a3581b 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:photo_manager/photo_manager.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/media_list_view_controller.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Callback for when a file has to be picked. @@ -47,8 +48,8 @@ class StreamAttachmentPicker extends StatefulWidget { /// The picker size in height. final double pickerSize; - /// The [MessageInputController] linked to this picker. - final MessageInputController messageInputController; + /// The [StreamMessageInputController] linked to this picker. + final StreamMessageInputController messageInputController; /// The limit of attachments that can be picked. final int attachmentLimit; @@ -77,7 +78,7 @@ class StreamAttachmentPicker extends StatefulWidget { /// properties. StreamAttachmentPicker copyWith({ Key? key, - MessageInputController? messageInputController, + StreamMessageInputController? messageInputController, FilePickerCallback? onFilePicked, bool? isOpen, double? pickerSize, @@ -113,6 +114,7 @@ class StreamAttachmentPicker extends StatefulWidget { class _StreamAttachmentPickerState extends State { int _filePickerIndex = 0; + final _mediaListViewController = MediaListViewController(); @override Widget build(BuildContext context) { @@ -281,6 +283,26 @@ class _StreamAttachmentPickerState extends State { ), ], ), + const Spacer(), + FutureBuilder( + future: PhotoManager.requestPermissionExtend(), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data == PermissionState.limited) { + return TextButton( + child: Text(context.translations.viewLibrary), + onPressed: () async { + await PhotoManager.presentLimited(); + _mediaListViewController.updateMedia( + newValue: true, + ); + }, + ); + } + + return const SizedBox.shrink(); + }, + ), DecoratedBox( decoration: BoxDecoration( color: _streamChatTheme.colorTheme.barsBg, @@ -315,6 +337,7 @@ class _StreamAttachmentPickerState extends State { borderRadius: BorderRadius.circular(8), ), child: _PickerWidget( + mediaListViewController: _mediaListViewController, filePickerIndex: _filePickerIndex, streamChatTheme: _streamChatTheme, containsFile: _attachmentContainsFile, @@ -410,6 +433,7 @@ class _PickerWidget extends StatefulWidget { required this.streamChatTheme, required this.allowedAttachmentTypes, required this.customAttachmentTypes, + required this.mediaListViewController, }) : super(key: key); final int filePickerIndex; @@ -420,6 +444,7 @@ class _PickerWidget extends StatefulWidget { final StreamChatThemeData streamChatTheme; final List allowedAttachmentTypes; final List customAttachmentTypes; + final MediaListViewController mediaListViewController; @override _PickerWidgetState createState() => _PickerWidgetState(); @@ -473,6 +498,7 @@ class _PickerWidgetState extends State<_PickerWidget> { return StreamMediaListView( selectedIds: widget.selectedMedias, onSelect: widget.onMediaSelected, + controller: widget.mediaListViewController, ); } diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart index 9c33d83c..24eec8fa 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart @@ -13,7 +13,6 @@ import 'package:stream_chat_flutter/src/commands_overlay.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/emoji_overlay.dart'; import 'package:stream_chat_flutter/src/extension.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/user_mentions_overlay.dart'; import 'package:stream_chat_flutter/src/v4/message_input/simple_safe_area.dart'; @@ -76,16 +75,16 @@ typedef ActionButtonBuilder = Widget Function( ); /// Widget builder for widgets that may require data from the -/// [MessageInputController]. +/// [StreamMessageInputController]. typedef MessageRelatedBuilder = Widget Function( BuildContext context, - MessageInputController messageInputController, + StreamMessageInputController messageInputController, ); /// Widget builder for a custom attachment picker. typedef AttachmentsPickerBuilder = Widget Function( BuildContext context, - MessageInputController messageInputController, + StreamMessageInputController messageInputController, StreamAttachmentPicker defaultPicker, ); @@ -246,7 +245,7 @@ class StreamMessageInput extends StatefulWidget { final bool hideSendAsDm; /// The text controller of the TextField. - final MessageInputController? messageInputController; + final StreamMessageInputController? messageInputController; /// List of action widgets. final List actions; @@ -375,14 +374,14 @@ class StreamMessageInputState extends State bool get _disableEmojiSuggestionsOverlay => widget.disableEmojiSuggestionsOverlay ?? false; - RestorableMessageInputController? _controller; + StreamRestorableMessageInputController? _controller; - MessageInputController get _effectiveController => + StreamMessageInputController get _effectiveController => widget.messageInputController ?? _controller!.value; void _createLocalController([Message? message]) { assert(_controller == null, ''); - _controller = RestorableMessageInputController(message: message); + _controller = StreamRestorableMessageInputController(message: message); } void _registerController() { @@ -502,7 +501,7 @@ class StreamMessageInputState extends State ), ); } - return MessageValueListenableBuilder( + return StreamMessageValueListenableBuilder( valueListenable: _effectiveController, builder: (context, value, _) { Widget child = DecoratedBox( diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart index 8e4a34eb..0aaccd0a 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart @@ -181,8 +181,8 @@ class StreamMessageTextField extends StatefulWidget { /// Controls the message being edited. /// - /// If null, this widget will create its own [MessageInputController]. - final MessageInputController? controller; + /// If null, this widget will create its own [StreamMessageInputController]. + final StreamMessageInputController? controller; /// Defines the keyboard focus for this widget. /// @@ -437,7 +437,7 @@ class StreamMessageTextField extends StatefulWidget { /// This setting is only honored on iOS devices. /// /// If unset, defaults to the brightness of - /// [ThemeData.primaryColorBrightness]. + /// [ThemeData.brightness]. final Brightness? keyboardAppearance; /// {@macro flutter.widgets.editableText.scrollPadding} @@ -656,9 +656,9 @@ class StreamMessageTextField extends StatefulWidget { class _StreamMessageTextFieldState extends State with RestorationMixin { - RestorableMessageInputController? _controller; + StreamRestorableMessageInputController? _controller; - MessageInputController get _effectiveController => + StreamMessageInputController get _effectiveController => widget.controller ?? _controller!.value; @override @@ -671,7 +671,7 @@ class _StreamMessageTextFieldState extends State void _createLocalController([Message? message]) { assert(_controller == null, ''); - _controller = RestorableMessageInputController(message: message); + _controller = StreamRestorableMessageInputController(message: message); } @override diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart new file mode 100644 index 00000000..b8e2cf24 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that displays a user. +/// +/// This widget is intended to be used as a Tile in +/// [StreamChannelGridView]. +/// +/// It shows the user's avatar and name. +/// +/// See also: +/// * [StreamChannelGridView] +/// * [StreamUserAvatar] +class StreamChannelGridTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelGridTile] widget. + const StreamChannelGridTile({ + Key? key, + required this.channel, + this.child, + this.footer, + this.onTap, + this.onLongPress, + }) : super(key: key); + + /// The channel to display. + final Channel channel; + + /// The widget to display in the body of the tile. + final Widget? child; + + /// The widget to display in the footer of the tile. + final Widget? footer; + + /// Called when the user taps this grid tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this grid tile. + final GestureLongPressCallback? onLongPress; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamChannelGridTile copyWith({ + Key? key, + Channel? channel, + Widget? child, + Widget? footer, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + }) => + StreamChannelGridTile( + key: key ?? this.key, + channel: channel ?? this.channel, + footer: footer ?? this.footer, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + child: child ?? this.child, + ); + + @override + Widget build(BuildContext context) { + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + + final child = this.child ?? + StreamChannelAvatar( + channel: channel, + borderRadius: BorderRadius.circular(32), + constraints: const BoxConstraints.tightFor( + height: 64, + width: 64, + ), + ); + + final footer = this.footer ?? + StreamChannelName( + channel: channel, + textStyle: channelPreviewTheme.titleStyle, + ); + + return InkWell( + onTap: onTap, + onLongPress: onLongPress, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + child, + footer, + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart new file mode 100644 index 00000000..d1969fa4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart @@ -0,0 +1,401 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_empty_widget.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Default grid delegate for [StreamChannelGridView]. +const defaultChannelGridViewDelegate = + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); + +/// Signature for the item builder that creates the children of the +/// [StreamChannelGridView]. +typedef StreamChannelGridViewIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// A [GridView] that shows a grid of [User]s, +/// it uses [StreamChannelGridTile] as a default item. +/// +/// Example: +/// +/// ```dart +/// StreamChannelGridView( +/// controller: controller, +/// onChannelTap: (channel) { +/// // Handle channel tap event +/// }, +/// onChannelLongPress: (channel) { +/// // Handle channel long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamChannelGridTile] +/// * [StreamChannelListController] +class StreamChannelGridView extends StatelessWidget { + /// Creates a new instance of [StreamChannelGridView]. + const StreamChannelGridView({ + Key? key, + required this.controller, + this.gridDelegate = defaultChannelGridViewDelegate, + this.itemBuilder, + this.emptyBuilder, + this.loadMoreErrorBuilder, + this.loadMoreIndicatorBuilder, + this.loadingBuilder, + this.errorBuilder, + this.onChannelTap, + this.onChannelLongPress, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [StreamUserListController] used to control the grid of users. + final StreamChannelListController controller; + + /// A delegate that controls the layout of the children within + /// the [PagedValueGridView]. + final SliverGridDelegate gridDelegate; + + /// A builder that is called to build items in the [PagedValueGridView]. + /// + /// The `value` parameter is the [Channel] at this position in the grid. + final StreamChannelGridViewIndexedWidgetBuilder? itemBuilder; + + /// A builder that is called to build the empty state of the grid. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the load more error state of the grid. + final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the grid. + final WidgetBuilder? loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the grid. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the grid. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the user taps this grid tile. + final void Function(Channel)? onChannelTap; + + /// Called when the user long-presses on this grid tile. + final void Function(Channel)? onChannelLongPress; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by + /// an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollDirection] is [Axis.vertical] and + /// [controller] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [CustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. If the + /// number is unknown or unbounded this should be left unset or set to null. + /// + /// See also: + /// + /// * [SemanticsConfiguration.scrollChildCount], + /// the corresponding semantics property. + final int? semanticChildCount; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return PagedValueGridView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + scrollController: scrollController, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + cacheExtent: cacheExtent, + semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + clipBehavior: clipBehavior, + gridDelegate: gridDelegate, + itemBuilder: (context, channels, index) { + final channel = channels[index]; + final onTap = onChannelTap; + final onLongPress = onChannelLongPress; + + final streamChannelGridTile = StreamChannelGridTile( + channel: channel, + onTap: onTap == null ? null : () => onTap(channel), + onLongPress: onLongPress == null ? null : () => onLongPress(channel), + ); + + return itemBuilder?.call( + context, + channels, + index, + streamChannelGridTile, + ) ?? + streamChannelGridTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.letsStartChattingLabel, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.grid( + onTap: controller.retry, + error: Text( + context.translations.loadingChannelsError, + textAlign: TextAlign.center, + ), + ), + loadMoreIndicatorBuilder: (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingChannelsError), + onRetryPressed: controller.refresh, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart similarity index 99% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart rename to packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart index 8c5acea6..8a180fca 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart @@ -1,7 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/v4/stream_message_preview_text.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that displays a channel preview. diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart similarity index 78% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart rename to packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart index 1e76a25b..2a511835 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart @@ -3,9 +3,14 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; -import 'package:stream_chat_flutter/src/v4/stream_list_view_indexed_widget_builder.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_empty_widget.dart'; + +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Default separator builder for [StreamChannelListView]. @@ -19,7 +24,7 @@ Widget defaultChannelListViewSeparatorBuilder( /// Signature for the item builder that creates the children of the /// [StreamChannelListView]. typedef StreamChannelListViewIndexedWidgetBuilder - = StreamListViewIndexedWidgetBuilder; + = StreamScrollViewIndexedWidgetBuilder; /// A [ListView] that shows a list of [Channel]s, /// it uses [StreamChannelListTile] as a default item. @@ -78,23 +83,15 @@ class StreamChannelListView extends StatelessWidget { final StreamChannelListController controller; /// A builder that is called to build items in the [ListView]. - /// - /// The `channel` parameter is the [Channel] at this position in the list - /// and the `defaultWidget` is the default widget used - /// i.e: [StreamChannelListTile]. final StreamChannelListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; /// A builder that is called to build the empty state of the list. - /// - /// If not provided, [StreamChannelListEmptyWidget] will be used. final WidgetBuilder? emptyBuilder; /// A builder that is called to build the loading state of the list. - /// - /// If not provided, [StreamChannelListLoadingTile] will be used. final WidgetBuilder? loadingBuilder; /// A builder that is called to build the error state of the list. @@ -328,96 +325,52 @@ class StreamChannelListView extends StatelessWidget { ) ?? streamChannelListTile; }, - emptyBuilder: (context) => - emptyBuilder?.call(context) ?? - const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: StreamChannelListEmptyWidget(), - ), - ), + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.letsStartChattingLabel, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, loadMoreErrorBuilder: (context, error) => - StreamChannelListLoadMoreError(onTap: controller.retry), + StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingChannelsError), + ), loadMoreIndicatorBuilder: (context) => const Center( child: Padding( padding: EdgeInsets.all(16), - child: StreamChannelListLoadMoreIndicator(), + child: StreamScrollViewLoadMoreIndicator(), ), ), loadingBuilder: (context) => loadingBuilder?.call(context) ?? - ListView.separated( - padding: padding, - physics: physics, - reverse: reverse, - itemCount: 25, - separatorBuilder: (_, __) => const StreamChannelListSeparator(), - itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + const Center( + child: StreamScrollViewLoadingWidget(), ), errorBuilder: (context, error) => errorBuilder?.call(context, error) ?? Center( - child: StreamChannelListErrorWidget( - onPressed: controller.refresh, + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingChannelsError), + onRetryPressed: controller.refresh, ), ), ); } -/// A [StreamChannelListTile] that can be used in a [ListView] to show a -/// loading tile while waiting for the [StreamChannelListController] to load -/// more channels. -class StreamChannelListLoadMoreIndicator extends StatelessWidget { - /// Creates a new instance of [StreamChannelListLoadMoreIndicator]. - const StreamChannelListLoadMoreIndicator({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) => const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator.adaptive(), - ); -} - -/// A [StreamChannelListTile] that is used to display the error indicator when -/// loading more channels fails. -class StreamChannelListLoadMoreError extends StatelessWidget { - /// Creates a new instance of [StreamChannelListLoadMoreError]. - const StreamChannelListLoadMoreError({ - Key? key, - this.onTap, - }) : super(key: key); - - /// The callback to invoke when the user taps on the error indicator. - final GestureTapCallback? onTap; - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Container( - color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.translations.loadingChannelsError, - style: theme.textTheme.body.copyWith( - color: Colors.white, - ), - ), - StreamSvgIcon.retry(color: Colors.white), - ], - ), - ), - ), - ); - } -} - /// A widget that is used to display a separator between /// [StreamChannelListTile] items. class StreamChannelListSeparator extends StatelessWidget { @@ -471,29 +424,3 @@ class StreamChannelListErrorWidget extends StatelessWidget { ], ); } - -/// A widget that is used to display an empty state when -/// [StreamChannelListController] loads zero channels. -class StreamChannelListEmptyWidget extends StatelessWidget { - /// Creates a new instance of [StreamChannelListEmptyWidget] widget. - const StreamChannelListEmptyWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon.message( - size: 148, - color: chatThemeData.colorTheme.disabled, - ), - const SizedBox(height: 28), - Text( - context.translations.letsStartChattingLabel, - style: chatThemeData.textTheme.headline, - ), - ], - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart new file mode 100644 index 00000000..488148b5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart @@ -0,0 +1,365 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; + +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Default grid delegate for [StreamMessageSearchGridView]. +const defaultMessageSearchGridViewDelegate = + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); + +/// Signature for the item builder that creates the children of the +/// [StreamMessageSearchGridView]. +typedef StreamMessageSearchGridViewIndexedWidgetBuilder + = PagedValueScrollViewIndexedWidgetBuilder; + +/// A [GridView] that shows a grid of [GetMessageResponse]s, +/// it uses [StreamMessageSearchGridTile] as a default item. +/// +/// Example: +/// +/// ```dart +/// StreamMessageSearchGridView( +/// controller: controller, +/// itemBuilder: (context, messageResponses, index) { +/// return GridTile(message: messageResponses[index]); +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamUserListTile] +/// * [StreamUserListController] +class StreamMessageSearchGridView extends StatelessWidget { + /// Creates a new instance of [StreamMessageSearchGridView]. + const StreamMessageSearchGridView({ + Key? key, + required this.controller, + required this.itemBuilder, + this.gridDelegate = defaultMessageSearchGridViewDelegate, + this.emptyBuilder, + this.loadMoreErrorBuilder, + this.loadMoreIndicatorBuilder, + this.loadingBuilder, + this.errorBuilder, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [StreamUserListController] used to control the grid of users. + final StreamMessageSearchListController controller; + + /// A delegate that controls the layout of the children within + /// the [PagedValueGridView]. + final SliverGridDelegate gridDelegate; + + /// A builder that is called to build items in the [PagedValueGridView]. + /// + /// The `value` parameter is the [GetMessageBuilder] + /// at this position in the grid. + final StreamMessageSearchGridViewIndexedWidgetBuilder itemBuilder; + + /// A builder that is called to build the empty state of the grid. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the load more error state of the grid. + final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the grid. + final WidgetBuilder? loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the grid. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the grid. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by + /// an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollDirection] is [Axis.vertical] and + /// [controller] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [CustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. If the + /// number is unknown or unbounded this should be left unset or set to null. + /// + /// See also: + /// + /// * [SemanticsConfiguration.scrollChildCount], + /// the corresponding semantics property. + final int? semanticChildCount; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return PagedValueGridView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + scrollController: scrollController, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + cacheExtent: cacheExtent, + semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + clipBehavior: clipBehavior, + gridDelegate: gridDelegate, + itemBuilder: itemBuilder, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.emptyMessagesText, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.grid( + onTap: controller.retry, + error: Text(context.translations.loadingMessagesError), + ), + loadMoreIndicatorBuilder: (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + onRetryPressed: controller.refresh, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart similarity index 95% rename from packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_tile.dart rename to packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart index f67597f0..c303add0 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/v4/stream_message_preview_text.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that displays a message search item. @@ -158,15 +157,19 @@ class StreamMessageSearchListTile extends StatelessWidget { } } +/// A widget that displays the title of a [StreamMessageSearchListTile]. class MessageSearchListTileTitle extends StatelessWidget { + /// Creates a new [MessageSearchListTileTitle] instance. const MessageSearchListTileTitle({ Key? key, required this.messageResponse, this.textStyle, }) : super(key: key); + /// The message response for the tile. final GetMessageResponse messageResponse; + /// The style to use for the title. final TextStyle? textStyle; @override @@ -200,6 +203,7 @@ class MessageSearchListTileTitle extends StatelessWidget { } } +/// A widget which shows formatted created date of the passed [message]. class MessageSearchTileMessageDate extends StatelessWidget { /// Creates a new instance of [MessageSearchTileMessageDate]. const MessageSearchTileMessageDate({ diff --git a/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart similarity index 71% rename from packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_view.dart rename to packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart index aef76b10..9c3f165d 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_search_list_view/stream_message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart @@ -1,6 +1,12 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default separator builder for [StreamMessageSearchListView]. @@ -14,7 +20,7 @@ Widget defaultMessageSearchListViewSeparatorBuilder( /// Signature for the item builder that creates the children of the /// [StreamMessageSearchListView]. typedef StreamMessageSearchListViewIndexedWidgetBuilder - = StreamListViewIndexedWidgetBuilder; /// A [ListView] that shows a list of [GetMessageResponse]s, @@ -75,10 +81,6 @@ class StreamMessageSearchListView extends StatelessWidget { final StreamMessageSearchListController controller; /// A builder that is called to build items in the [ListView]. - /// - /// The `messageResponse` parameter is the [GetMessageResponse] at this - /// position in the list and the `defaultWidget` is the default widget used - /// i.e: [StreamMessageSearchListTile]. final StreamMessageSearchListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. @@ -86,18 +88,12 @@ class StreamMessageSearchListView extends StatelessWidget { separatorBuilder; /// A builder that is called to build the empty state of the list. - /// - /// If not provided, [StreamMessageSearchListEmptyWidget] will be used. final WidgetBuilder? emptyBuilder; /// A builder that is called to build the loading state of the list. - /// - /// If not provided, [StreamMessageSearchListLoadingTile] will be used. final WidgetBuilder? loadingBuilder; /// A builder that is called to build the error state of the list. - /// - /// If not provided, [StreamMessageSearchListErrorWidget] will be used. final Widget Function(BuildContext, StreamChatError)? errorBuilder; /// Called when the user taps this list tile. @@ -312,7 +308,7 @@ class StreamMessageSearchListView extends StatelessWidget { final onTap = onMessageTap; final onLongPress = onMessageLongPress; - final streamUserListTile = StreamMessageSearchListTile( + final streamMessageSearchListTile = StreamMessageSearchListTile( messageResponse: messageResponse, onTap: onTap == null ? null : () => onTap(messageResponse), onLongPress: @@ -323,101 +319,56 @@ class StreamMessageSearchListView extends StatelessWidget { context, messageResponses, index, - streamUserListTile, + streamMessageSearchListTile, ) ?? - streamUserListTile; + streamMessageSearchListTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.emptyMessagesText, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); }, - emptyBuilder: (context) => - emptyBuilder?.call(context) ?? - const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: StreamMessageSearchListEmptyWidget(), - ), - ), loadMoreErrorBuilder: (context, error) => - StreamMessageSearchListLoadMoreError(onTap: controller.retry), + StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingMessagesError), + ), loadMoreIndicatorBuilder: (context) => const Center( child: Padding( padding: EdgeInsets.all(16), - child: StreamMessageSearchListLoadMoreIndicator(), + child: StreamScrollViewLoadMoreIndicator(), ), ), loadingBuilder: (context) => loadingBuilder?.call(context) ?? - ListView.separated( - padding: padding, - physics: physics, - reverse: reverse, - itemCount: 25, - separatorBuilder: (_, __) => - const StreamMessageSearchListSeparator(), - itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + const Center( + child: StreamScrollViewLoadingWidget(), ), errorBuilder: (context, error) => errorBuilder?.call(context, error) ?? Center( - child: StreamMessageSearchListErrorWidget( - onPressed: controller.refresh, + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingMessagesError), + onRetryPressed: controller.refresh, ), ), ); } -/// A [StreamMessageSearchListTile] that can be used in a [ListView] to show a -/// loading tile while waiting for the [StreamMessageSearchListController] to -/// load more messages. -class StreamMessageSearchListLoadMoreIndicator extends StatelessWidget { - /// Creates a new instance of [StreamMessageSearchListLoadMoreIndicator]. - const StreamMessageSearchListLoadMoreIndicator({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) => const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator.adaptive(), - ); -} - -/// A [StreamMessageSearchListTile] that is used to display the error indicator -/// when loading more messages fails. -class StreamMessageSearchListLoadMoreError extends StatelessWidget { - /// Creates a new instance of [StreamMessageSearchListLoadMoreError]. - const StreamMessageSearchListLoadMoreError({ - Key? key, - this.onTap, - }) : super(key: key); - - /// The callback to invoke when the user taps on the error indicator. - final GestureTapCallback? onTap; - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Container( - color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.translations.loadingChannelsError, - style: theme.textTheme.body.copyWith( - color: Colors.white, - ), - ), - StreamSvgIcon.retry(color: Colors.white), - ], - ), - ), - ), - ); - } -} - /// A widget that is used to display a separator between /// [StreamMessageSearchListTile] items. class StreamMessageSearchListSeparator extends StatelessWidget { @@ -433,67 +384,3 @@ class StreamMessageSearchListSeparator extends StatelessWidget { ); } } - -/// A widget that is used to display an error screen -/// when [StreamMessageSearchListController] fails to load initial messages. -class StreamMessageSearchListErrorWidget extends StatelessWidget { - /// Creates a new instance of [StreamMessageSearchListErrorWidget] widget. - const StreamMessageSearchListErrorWidget({ - Key? key, - this.onPressed, - }) : super(key: key); - - /// The callback to invoke when the user taps on the retry button. - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(right: 2), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: context.translations.loadingChannelsError), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - TextButton( - onPressed: onPressed, - child: Text(context.translations.retryLabel), - ), - ], - ); -} - -/// A widget that is used to display an empty state when -/// [StreamMessageSearchListController] loads zero messages. -class StreamMessageSearchListEmptyWidget extends StatelessWidget { - /// Creates a new instance of [StreamMessageSearchListEmptyWidget] widget. - const StreamMessageSearchListEmptyWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon.message( - size: 148, - color: chatThemeData.colorTheme.disabled, - ), - const SizedBox(height: 28), - Text( - context.translations.letsStartChattingLabel, - style: chatThemeData.textTheme.headline, - ), - ], - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_empty_widget.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_empty_widget.dart new file mode 100644 index 00000000..531eac79 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_empty_widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// A widget that shows an empty view when the [StreamScrollView] loads +/// empty data. +class StreamScrollViewEmptyWidget extends StatelessWidget { + /// Creates a new instance of the [StreamScrollViewEmptyWidget]. + const StreamScrollViewEmptyWidget({ + Key? key, + required this.emptyIcon, + required this.emptyTitle, + this.emptyTitleStyle, + this.mainAxisSize = MainAxisSize.max, + this.mainAxisAlignment = MainAxisAlignment.center, + this.crossAxisAlignment = CrossAxisAlignment.center, + }) : super(key: key); + + /// The title of the empty view. + final Widget emptyTitle; + + /// The style of the title. + final TextStyle? emptyTitleStyle; + + /// The icon of the empty view. + final Widget emptyIcon; + + /// The main axis size of the empty view. + final MainAxisSize mainAxisSize; + + /// The main axis alignment of the empty view. + final MainAxisAlignment mainAxisAlignment; + + /// The cross axis alignment of the empty view. + final CrossAxisAlignment crossAxisAlignment; + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + + final emptyIcon = AnimatedSwitcher( + duration: kThemeChangeDuration, + child: this.emptyIcon, + ); + + final emptyTitleText = AnimatedDefaultTextStyle( + style: emptyTitleStyle ?? chatThemeData.textTheme.headline, + duration: kThemeChangeDuration, + child: emptyTitle, + ); + + return Column( + mainAxisSize: mainAxisSize, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + children: [ + emptyIcon, + emptyTitleText, + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_error_widget.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_error_widget.dart new file mode 100644 index 00000000..9d6af29c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_error_widget.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// A widget that is displayed when a [StreamScrollView] encounters an error +/// while loading the initial items. +class StreamScrollViewErrorWidget extends StatelessWidget { + /// Creates a new instance of the [StreamScrollViewErrorWidget]. + const StreamScrollViewErrorWidget({ + Key? key, + this.errorTitle, + this.errorTitleStyle, + this.errorIcon, + this.retryButtonText, + this.retryButtonTextStyle, + required this.onRetryPressed, + this.mainAxisSize = MainAxisSize.max, + this.mainAxisAlignment = MainAxisAlignment.center, + this.crossAxisAlignment = CrossAxisAlignment.center, + }) : super(key: key); + + /// The title of the error. + final Widget? errorTitle; + + /// The style of the title. + final TextStyle? errorTitleStyle; + + /// The icon to display when the list shows error. + final Widget? errorIcon; + + /// The text to display in the retry button. + final Widget? retryButtonText; + + /// The style of the retryButtonText. + final TextStyle? retryButtonTextStyle; + + /// The callback to invoke when the user taps on the retry button. + final VoidCallback onRetryPressed; + + /// The main axis size of the error view. + final MainAxisSize mainAxisSize; + + /// The main axis alignment of the error view. + final MainAxisAlignment mainAxisAlignment; + + /// The cross axis alignment of the error view. + final CrossAxisAlignment crossAxisAlignment; + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + + final errorIcon = AnimatedSwitcher( + duration: kThemeChangeDuration, + child: this.errorIcon ?? + Icon( + Icons.error_outline_rounded, + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + ); + + final titleText = AnimatedDefaultTextStyle( + style: errorTitleStyle ?? chatThemeData.textTheme.headline, + duration: kThemeChangeDuration, + child: errorTitle ?? const SizedBox(), + ); + + final retryButtonText = AnimatedDefaultTextStyle( + style: errorTitleStyle ?? + chatThemeData.textTheme.headline.copyWith( + color: Colors.white, + ), + duration: kThemeChangeDuration, + child: this.retryButtonText ?? Text(context.translations.retryLabel), + ); + + return Column( + mainAxisSize: mainAxisSize, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + children: [ + errorIcon, + titleText, + ElevatedButton( + onPressed: onRetryPressed, + child: retryButtonText, + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart new file mode 100644 index 00000000..0305cd1f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +/// Signature for a function that creates a widget for a given index, e.g., in a +/// list, grid. +/// +/// Used by [StreamChannelListView], [StreamMessageSearchListView] +/// and [StreamUserListView]. +typedef StreamScrollViewIndexedWidgetBuilder + = Widget Function( + BuildContext context, + List items, + int index, + WidgetType defaultWidget, +); diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_error.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_error.dart new file mode 100644 index 00000000..03575b1f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_error.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; + +/// A tile that is used to display the error indicator when +/// loading more items fails. +class StreamScrollViewLoadMoreError extends StatelessWidget { + /// Creates a new instance of [StreamScrollViewLoadMoreError.list]. + const StreamScrollViewLoadMoreError.list({ + Key? key, + this.error, + this.errorStyle, + this.errorIcon, + this.backgroundColor, + required this.onTap, + this.padding = const EdgeInsets.all(16), + this.mainAxisSize = MainAxisSize.max, + this.mainAxisAlignment = MainAxisAlignment.spaceBetween, + this.crossAxisAlignment = CrossAxisAlignment.center, + }) : _isList = true, + super(key: key); + + /// Creates a new instance of [StreamScrollViewLoadMoreError.grid]. + const StreamScrollViewLoadMoreError.grid({ + Key? key, + this.error, + this.errorStyle, + this.errorIcon, + this.backgroundColor, + required this.onTap, + this.padding = const EdgeInsets.all(16), + this.mainAxisSize = MainAxisSize.max, + this.mainAxisAlignment = MainAxisAlignment.spaceEvenly, + this.crossAxisAlignment = CrossAxisAlignment.center, + }) : _isList = false, + super(key: key); + + /// The error message to display. + final Widget? error; + + /// The style of the error message. + final TextStyle? errorStyle; + + /// The icon to display next to the message. + final Widget? errorIcon; + + /// The background color of the error message. + final Color? backgroundColor; + + /// The callback to invoke when the user taps on the error indicator. + final GestureTapCallback onTap; + + /// The amount of space by which to inset the child. + final EdgeInsetsGeometry padding; + + /// The main axis size of the error view. + final MainAxisSize mainAxisSize; + + /// The main axis alignment of the error view. + final MainAxisAlignment mainAxisAlignment; + + /// The cross axis alignment of the error view. + final CrossAxisAlignment crossAxisAlignment; + + final bool _isList; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + + final errorText = AnimatedDefaultTextStyle( + style: errorStyle ?? theme.textTheme.body.copyWith(color: Colors.white), + duration: kThemeChangeDuration, + child: error ?? const SizedBox(), + ); + + final errorIcon = AnimatedSwitcher( + duration: kThemeChangeDuration, + child: this.errorIcon ?? StreamSvgIcon.retry(color: Colors.white), + ); + + final backgroundColor = this.backgroundColor ?? + theme.colorTheme.textLowEmphasis.withOpacity(0.9); + + final children = [errorText, errorIcon]; + + return InkWell( + onTap: onTap, + child: Container( + color: backgroundColor, + child: Padding( + padding: padding, + child: _isList + ? Row( + mainAxisSize: mainAxisSize, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + children: children, + ) + : Column( + mainAxisSize: mainAxisSize, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + children: children, + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart new file mode 100644 index 00000000..93bf8d06 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +/// A widget that shows a loading indicator when the user is near the bottom of +/// the list. +class StreamScrollViewLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [StreamScrollViewLoadMoreIndicator]. + const StreamScrollViewLoadMoreIndicator({ + Key? key, + this.height = 16, + this.width = 16, + }) : super(key: key); + + /// The height of the indicator. + final double height; + + /// The width of the indicator. + final double width; + + @override + Widget build(BuildContext context) => SizedBox( + height: height, + width: width, + child: const CircularProgressIndicator.adaptive(), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_loading_widget.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_loading_widget.dart new file mode 100644 index 00000000..ae47b152 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_loading_widget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +/// A widget that is displayed while the [StreamScrollView] is loading. +class StreamScrollViewLoadingWidget extends StatelessWidget { + /// Creates a new instance of [StreamScrollViewLoadingWidget]. + const StreamScrollViewLoadingWidget({ + Key? key, + this.height = 42, + this.width = 42, + }) : super(key: key); + + /// The height of the indicator. + final double height; + + /// The width of the indicator. + final double width; + + @override + Widget build(BuildContext context) => SizedBox( + height: height, + width: width, + child: const CircularProgressIndicator.adaptive(), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart new file mode 100644 index 00000000..6d904086 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that displays a user. +/// +/// This widget is intended to be used as a Tile in [StreamUserGridView] +/// +/// It shows the user's avatar and name. +/// +/// See also: +/// * [StreamUserGridView] +/// * [StreamUserAvatar] +class StreamUserGridTile extends StatelessWidget { + /// Creates a new instance of [StreamUserGridTile] widget. + const StreamUserGridTile({ + Key? key, + required this.user, + this.child, + this.footer, + this.onTap, + this.onLongPress, + }) : super(key: key); + + /// The user to display. + final User user; + + /// The widget to display in the body of the tile. + final Widget? child; + + /// The widget to display in the footer of the tile. + final Widget? footer; + + /// Called when the user taps this grid tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this grid tile. + final GestureLongPressCallback? onLongPress; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamUserGridTile copyWith({ + Key? key, + User? user, + Widget? child, + Widget? footer, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + }) => + StreamUserGridTile( + key: key ?? this.key, + user: user ?? this.user, + footer: footer ?? this.footer, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + child: child ?? this.child, + ); + + @override + Widget build(BuildContext context) { + final child = this.child ?? + StreamUserAvatar( + user: user, + borderRadius: BorderRadius.circular(32), + constraints: const BoxConstraints.tightFor( + height: 64, + width: 64, + ), + onlineIndicatorConstraints: const BoxConstraints.tightFor( + height: 12, + width: 12, + ), + ); + + final footer = this.footer ?? + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + user.name, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ); + + return InkWell( + onTap: onTap, + onLongPress: onLongPress, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + child, + const SizedBox(height: 4), + footer, + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart new file mode 100644 index 00000000..08cc4a50 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart @@ -0,0 +1,394 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Default grid delegate for [StreamUserGridView]. +const defaultUserGridViewDelegate = + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); + +/// Signature for the item builder that creates the children of the +/// [StreamUserGridView]. +typedef StreamUserGridViewIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// A [GridView] that shows a grid of [User]s, +/// it uses [StreamUserGridTile] as a default item. +/// +/// Example: +/// +/// ```dart +/// StreamUserGridView( +/// controller: controller, +/// onUserTap: (user) { +/// // Handle user tap event +/// }, +/// onUserLongPress: (user) { +/// // Handle user long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamUserListTile] +/// * [StreamUserListController] +class StreamUserGridView extends StatelessWidget { + /// Creates a new instance of [StreamUserGridView]. + const StreamUserGridView({ + Key? key, + required this.controller, + this.gridDelegate = defaultUserGridViewDelegate, + this.itemBuilder, + this.emptyBuilder, + this.loadMoreErrorBuilder, + this.loadMoreIndicatorBuilder, + this.loadingBuilder, + this.errorBuilder, + this.onUserTap, + this.onUserLongPress, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [StreamUserListController] used to control the grid of users. + final StreamUserListController controller; + + /// A delegate that controls the layout of the children within + /// the [PagedValueGridView]. + final SliverGridDelegate gridDelegate; + + /// A builder that is called to build items in the [PagedValueGridView]. + final StreamUserGridViewIndexedWidgetBuilder? itemBuilder; + + /// A builder that is called to build the empty state of the grid. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the load more error state of the grid. + final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the grid. + final WidgetBuilder? loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the grid. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the grid. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the user taps this grid tile. + final void Function(User)? onUserTap; + + /// Called when the user long-presses on this grid tile. + final void Function(User)? onUserLongPress; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by + /// an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollDirection] is [Axis.vertical] and + /// [controller] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [CustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. If the + /// number is unknown or unbounded this should be left unset or set to null. + /// + /// See also: + /// + /// * [SemanticsConfiguration.scrollChildCount], + /// the corresponding semantics property. + final int? semanticChildCount; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return PagedValueGridView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + scrollController: scrollController, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + cacheExtent: cacheExtent, + semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + clipBehavior: clipBehavior, + gridDelegate: gridDelegate, + itemBuilder: (context, users, index) { + final user = users[index]; + final onTap = onUserTap; + final onLongPress = onUserLongPress; + + final streamUserGridTile = StreamUserGridTile( + user: user, + onTap: onTap == null ? null : () => onTap(user), + onLongPress: onLongPress == null ? null : () => onLongPress(user), + ); + + return itemBuilder?.call( + context, + users, + index, + streamUserGridTile, + ) ?? + streamUserGridTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.user( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noUsersLabel, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.grid( + onTap: controller.retry, + error: Text( + context.translations.loadingUsersError, + textAlign: TextAlign.center, + ), + ), + loadMoreIndicatorBuilder: (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingUsersError), + onRetryPressed: controller.refresh, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart similarity index 97% rename from packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_tile.dart rename to packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart index 0d6c79ba..c88862fa 100644 --- a/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart @@ -48,10 +48,11 @@ class StreamUserListTile extends StatelessWidget { /// A widget to display at the end of tile. final Widget? selectedWidget; - /// If this tile is also [enabled] then icons and text are rendered with the same color. + /// If this tile is also [enabled] then icons + /// and text are rendered with the same color. /// - /// By default the selected color is the theme's primary color. The selected color - /// can be overridden with a [ListTileTheme]. + /// By default the selected color is the theme's primary color. + /// The selected color can be overridden with a [ListTileTheme]. /// /// {@tool dartpad} /// Here is an example of using a [StatefulWidget] to keep track of the diff --git a/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart similarity index 72% rename from packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_view.dart rename to packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart index 45fd37bb..d87f8ab9 100644 --- a/packages/stream_chat_flutter/lib/src/v4/user_list_view/stream_user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart @@ -1,6 +1,12 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default separator builder for [StreamUserListView]. @@ -14,7 +20,7 @@ Widget defaultUserListViewSeparatorBuilder( /// Signature for the item builder that creates the children of the /// [StreamUserListView]. typedef StreamUserListViewIndexedWidgetBuilder - = StreamListViewIndexedWidgetBuilder; + = StreamScrollViewIndexedWidgetBuilder; /// A [ListView] that shows a list of [User]s, /// it uses [StreamUserListTile] as a default item. @@ -73,28 +79,18 @@ class StreamUserListView extends StatelessWidget { final StreamUserListController controller; /// A builder that is called to build items in the [ListView]. - /// - /// The `user` parameter is the [User] at this position in the list - /// and the `defaultWidget` is the default widget used - /// i.e: [StreamUserListTile]. final StreamUserListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; /// A builder that is called to build the empty state of the list. - /// - /// If not provided, [StreamUserListEmptyWidget] will be used. final WidgetBuilder? emptyBuilder; /// A builder that is called to build the loading state of the list. - /// - /// If not provided, [StreamUserListLoadingTile] will be used. final WidgetBuilder? loadingBuilder; /// A builder that is called to build the error state of the list. - /// - /// If not provided, [StreamUserListErrorWidget] will be used. final Widget Function(BuildContext, StreamChatError)? errorBuilder; /// Called when the user taps this list tile. @@ -322,96 +318,52 @@ class StreamUserListView extends StatelessWidget { ) ?? streamUserListTile; }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.user( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noUsersLabel, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, loadMoreErrorBuilder: (context, error) => - StreamUserListLoadMoreError(onTap: controller.retry), + StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingUsersError), + ), loadMoreIndicatorBuilder: (context) => const Center( child: Padding( padding: EdgeInsets.all(16), - child: StreamUserListLoadMoreIndicator(), + child: StreamScrollViewLoadMoreIndicator(), ), ), - emptyBuilder: (context) => - emptyBuilder?.call(context) ?? - const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: StreamUserListEmptyWidget(), - ), - ), loadingBuilder: (context) => loadingBuilder?.call(context) ?? - ListView.separated( - padding: padding, - physics: physics, - reverse: reverse, - itemCount: 25, - separatorBuilder: (_, __) => const StreamUserListSeparator(), - itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + const Center( + child: StreamScrollViewLoadingWidget(), ), errorBuilder: (context, error) => errorBuilder?.call(context, error) ?? Center( - child: StreamUserListErrorWidget( - onPressed: controller.refresh, + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingUsersError), + onRetryPressed: controller.refresh, ), ), ); } -/// A [StreamUserListTile] that can be used in a [ListView] to show a -/// loading tile while waiting for the [StreamUserListController] to load -/// more channels. -class StreamUserListLoadMoreIndicator extends StatelessWidget { - /// Creates a new instance of [StreamUserListLoadMoreIndicator]. - const StreamUserListLoadMoreIndicator({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) => const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator.adaptive(), - ); -} - -/// A [StreamUserListTile] that is used to display the error indicator when -/// loading more users fails. -class StreamUserListLoadMoreError extends StatelessWidget { - /// Creates a new instance of [StreamUserListLoadMoreError]. - const StreamUserListLoadMoreError({ - Key? key, - this.onTap, - }) : super(key: key); - - /// The callback to invoke when the user taps on the error indicator. - final GestureTapCallback? onTap; - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Container( - color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.translations.loadingChannelsError, - style: theme.textTheme.body.copyWith( - color: Colors.white, - ), - ), - StreamSvgIcon.retry(color: Colors.white), - ], - ), - ), - ), - ); - } -} - /// A widget that is used to display a separator between /// [StreamUserListTile] items. class StreamUserListSeparator extends StatelessWidget { @@ -427,67 +379,3 @@ class StreamUserListSeparator extends StatelessWidget { ); } } - -/// A widget that is used to display an error screen -/// when [StreamUserListController] fails to load initial users. -class StreamUserListErrorWidget extends StatelessWidget { - /// Creates a new instance of [StreamUserListErrorWidget] widget. - const StreamUserListErrorWidget({ - Key? key, - this.onPressed, - }) : super(key: key); - - /// The callback to invoke when the user taps on the retry button. - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(right: 2), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: context.translations.loadingChannelsError), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - TextButton( - onPressed: onPressed, - child: Text(context.translations.retryLabel), - ), - ], - ); -} - -/// A widget that is used to display an empty state when -/// [StreamUserListController] loads zero users. -class StreamUserListEmptyWidget extends StatelessWidget { - /// Creates a new instance of [StreamUserListEmptyWidget] widget. - const StreamUserListEmptyWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon.message( - size: 148, - color: chatThemeData.colorTheme.disabled, - ), - const SizedBox(height: 28), - Text( - context.translations.letsStartChattingLabel, - style: chatThemeData.textTheme.headline, - ), - ], - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_list_view_indexed_widget_builder.dart b/packages/stream_chat_flutter/lib/src/v4/stream_list_view_indexed_widget_builder.dart deleted file mode 100644 index e235e36a..00000000 --- a/packages/stream_chat_flutter/lib/src/v4/stream_list_view_indexed_widget_builder.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; - -typedef StreamListViewIndexedWidgetBuilder - = Widget Function( - BuildContext context, - List items, - int index, - WidgetType defaultWidget, -); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 0c2fc44b..5052393a 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -22,6 +22,7 @@ export 'src/info_tile.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; export 'src/message_action.dart'; +// ignore: deprecated_member_use_from_same_package export 'src/message_input.dart' show MessageInput, MessageInputState; export 'src/message_list_view.dart'; export 'src/message_search_item.dart'; @@ -48,23 +49,29 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; - // v4 -export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; -export 'src/v4/channel_list_view/stream_channel_list_tile.dart'; -export 'src/v4/channel_list_view/stream_channel_list_view.dart'; export 'src/v4/message_input/countdown_button.dart'; export 'src/v4/message_input/stream_attachment_picker.dart'; export 'src/v4/message_input/stream_message_input.dart'; export 'src/v4/message_input/stream_message_send_button.dart'; export 'src/v4/message_input/stream_message_text_field.dart'; -export 'src/v4/message_search_list_view/stream_message_search_list_tile.dart'; -export 'src/v4/message_search_list_view/stream_message_search_list_view.dart'; +export 'src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart'; +export 'src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart'; +export 'src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart'; +export 'src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart'; +export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart'; +export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart'; +export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart'; +export 'src/v4/scroll_view/stream_scroll_view_empty_widget.dart'; +export 'src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; +export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart'; +export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart'; +export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart'; +export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart'; +export 'src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart'; +export 'src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart'; export 'src/v4/stream_channel_avatar.dart'; export 'src/v4/stream_channel_info_bottom_sheet.dart'; export 'src/v4/stream_channel_name.dart'; -export 'src/v4/stream_list_view_indexed_widget_builder.dart'; export 'src/v4/stream_message_preview_text.dart'; -export 'src/v4/user_list_view/stream_user_list_tile.dart'; -export 'src/v4/user_list_view/stream_user_list_view.dart'; export 'src/visible_footnote.dart'; diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 831809a7..6866eae3 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -280,7 +280,7 @@ void main() { expect(find.text('test'), findsNothing); expect(find.byType(StreamBackButton), findsNothing); - expect(find.byType(ChannelAvatar), findsNothing); + expect(find.byType(StreamChannelAvatar), findsNothing); expect(find.byType(StreamChannelInfo), findsNothing); expect(find.text('leading'), findsOneWidget); expect(find.text('title'), findsOneWidget); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index 3fc1e718..1ec1c166 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -31,8 +31,10 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( - body: ChannelAvatar(), + child: Scaffold( + body: StreamChannelAvatar( + channel: channel, + ), ), ), ), @@ -101,8 +103,10 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( - body: ChannelAvatar(), + child: Scaffold( + body: StreamChannelAvatar( + channel: channel, + ), ), ), ), @@ -162,8 +166,10 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( - body: ChannelAvatar(), + child: Scaffold( + body: StreamChannelAvatar( + channel: channel, + ), ), ), ), @@ -202,9 +208,10 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( - body: ChannelAvatar( + child: Scaffold( + body: StreamChannelAvatar( selected: true, + channel: channel, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/channel_name_test.dart b/packages/stream_chat_flutter/test/src/channel_name_test.dart index dcfda04f..8848207e 100644 --- a/packages/stream_chat_flutter/test/src/channel_name_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_name_test.dart @@ -57,8 +57,10 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( - body: ChannelName(), + child: Scaffold( + body: StreamChannelName( + channel: channel, + ), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 285a5373..a2ab02f1 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/stream_chat_flutter/test/src/media_list_view_controller_test.dart b/packages/stream_chat_flutter/test/src/media_list_view_controller_test.dart new file mode 100644 index 00000000..3500d30b --- /dev/null +++ b/packages/stream_chat_flutter/test/src/media_list_view_controller_test.dart @@ -0,0 +1,37 @@ +import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; +import 'package:test/test.dart'; + +void main() { + test('should update media', () { + final controller = MediaListViewController(); + + expect(controller.shouldUpdateMedia, false); + + controller.updateMedia(newValue: true); + expect(controller.shouldUpdateMedia, true); + + controller.dispose(); + }); + + test('should notify listeners on update media', () { + final controller = MediaListViewController(); + + var callCount = 0; + void updateCallsSpy() => callCount++; + + controller.addListener(updateCallsSpy); + + expect(callCount, 0); + controller.updateMedia(newValue: false); + expect(controller.shouldUpdateMedia, false); + expect(callCount, 1); + + controller.updateMedia(newValue: true); + expect(controller.shouldUpdateMedia, true); + expect(callCount, 2); + + controller + ..removeListener(updateCallsSpy) + ..dispose(); + }); +} diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart index 37de3140..71193da4 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index 556f8109..a12b52f4 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -1,3 +1,6 @@ +// ignore: lines_longer_than_80_chars +// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart index 27ef0ea7..7b3550c8 100644 --- a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart @@ -1,3 +1,6 @@ +// ignore: lines_longer_than_80_chars +// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index 6d1758fa..20ce03f0 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/stream_chat_flutter_core/example/analysis_options.yaml b/packages/stream_chat_flutter_core/example/analysis_options.yaml new file mode 100644 index 00000000..d56da71f --- /dev/null +++ b/packages/stream_chat_flutter_core/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_flutter_core/example/android/app/src/main/AndroidManifest.xml index d2eb734a..dfabc845 100644 --- a/packages/stream_chat_flutter_core/example/android/app/src/main/AndroidManifest.xml +++ b/packages/stream_chat_flutter_core/example/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/android/app/src/main/res/values-night/styles.xml b/packages/stream_chat_flutter_core/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..3db14bb5 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 8c081082..49f2be75 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -60,102 +60,114 @@ class StreamExample extends StatelessWidget { } /// Basic layout displaying a list of [Channel]s the user is a part of. -/// This is implemented using [ChannelListCore]. +/// This is implemented using a [StreamChannelListController]. /// -/// [ChannelListCore] is a `builder` with callbacks for constructing UIs based -/// on different scenarios. -class HomeScreen extends StatelessWidget { +/// [StreamChannelListController] is a controller that lets you manage a list of +/// channels. +class HomeScreen extends StatefulWidget { /// Builds a basic layout displaying a list of [Channel]s the user is a /// part of. - HomeScreen({Key? key}) : super(key: key); + const HomeScreen({Key? key}) : super(key: key); + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { /// Controller used for loading more data and controlling pagination in - /// [ChannelListCore]. - final channelListController = ChannelListController(); + /// [StreamChannelListController]. + late final channelListController = StreamChannelListController( + client: StreamChatCore.of(context).client, + filter: Filter.and([ + Filter.equal('type', 'messaging'), + Filter.in_( + 'members', + [ + StreamChatCore.of(context).currentUser!.id, + ], + ), + ]), + ); + + @override + void initState() { + channelListController.doInitialLoad(); + super.initState(); + } + + @override + void dispose() { + channelListController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar( title: const Text('Channels'), ), - body: ChannelsBloc( - child: ChannelListCore( - channelListController: channelListController, - filter: Filter.and([ - Filter.equal('type', 'messaging'), - Filter.in_( - 'members', - [ - StreamChatCore.of(context).currentUser!.id, - ], - ), - ]), - emptyBuilder: (BuildContext context) => const Center( - child: Text('Looks like you are not in any channels'), - ), - loadingBuilder: (BuildContext context) => const Center( - child: SizedBox( - height: 100, - width: 100, - child: CircularProgressIndicator(), - ), - ), - errorBuilder: ( - BuildContext context, - dynamic error, - ) => - Center( - child: Text( - 'Oh no, something went wrong. ' - 'Please check your config. $error', - ), - ), - listBuilder: ( - BuildContext context, - List channels, - ) => - LazyLoadScrollView( - onEndOfPage: () async { - channelListController.paginateData!(); - }, - child: ListView.builder( - itemCount: channels.length, - itemBuilder: (BuildContext context, int index) { - final _item = channels[index]; - return ListTile( - title: Text(_item.name ?? ''), - subtitle: StreamBuilder( - stream: _item.state!.lastMessageStream, - initialData: _item.state!.lastMessage, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text(snapshot.data!.text!); - } - - return const SizedBox(); - }, - ), - onTap: () { - /// Display a list of messages when the user taps on - /// an item. We can use [StreamChannel] to wrap our - /// [MessageScreen] screen with the selected channel. - /// - /// This allows us to use a built-in inherited widget - /// for accessing our `channel` later on. - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: _item, - child: const MessageScreen(), - ), - ), - ); - }, - ); + body: PagedValueListenableBuilder( + valueListenable: channelListController, + builder: (context, value, child) { + return value.when( + (channels, nextPageKey, error) => LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) { + channelListController.loadMore(nextPageKey); + } }, + child: ListView.builder( + itemCount: channels.length, + itemBuilder: (BuildContext context, int index) { + final _item = channels[index]; + return ListTile( + title: Text(_item.name ?? ''), + subtitle: StreamBuilder( + stream: _item.state!.lastMessageStream, + initialData: _item.state!.lastMessage, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data!.text!); + } + + return const SizedBox(); + }, + ), + onTap: () { + /// Display a list of messages when the user taps on + /// an item. We can use [StreamChannel] to wrap our + /// [MessageScreen] screen with the selected channel. + /// + /// This allows us to use a built-in inherited widget + /// for accessing our `channel` later on. + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: _item, + child: const MessageScreen(), + ), + ), + ); + }, + ); + }, + ), ), - ), - ), + loading: () => const Center( + child: SizedBox( + height: 100, + width: 100, + child: CircularProgressIndicator(), + ), + ), + error: (e) => Center( + child: Text( + 'Oh no, something went wrong. ' + 'Please check your config. $e', + ), + ), + ); + }, ), ); } diff --git a/packages/stream_chat_flutter_core/example/web/favicon.png b/packages/stream_chat_flutter_core/example/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/packages/stream_chat_flutter_core/example/web/favicon.png differ diff --git a/packages/stream_chat_flutter_core/example/web/icons/Icon-192.png b/packages/stream_chat_flutter_core/example/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/packages/stream_chat_flutter_core/example/web/icons/Icon-192.png differ diff --git a/packages/stream_chat_flutter_core/example/web/icons/Icon-512.png b/packages/stream_chat_flutter_core/example/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/packages/stream_chat_flutter_core/example/web/icons/Icon-512.png differ diff --git a/packages/stream_chat_flutter_core/example/web/icons/Icon-maskable-192.png b/packages/stream_chat_flutter_core/example/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/stream_chat_flutter_core/example/web/icons/Icon-maskable-512.png b/packages/stream_chat_flutter_core/example/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/packages/stream_chat_flutter_core/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/stream_chat_flutter_core/example/web/index.html b/packages/stream_chat_flutter_core/example/web/index.html new file mode 100644 index 00000000..b6b9dd23 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/web/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/web/manifest.json b/packages/stream_chat_flutter_core/example/web/manifest.json new file mode 100644 index 00000000..096edf8f --- /dev/null +++ b/packages/stream_chat_flutter_core/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/stream_chat_flutter_core/example/windows/.gitignore b/packages/stream_chat_flutter_core/example/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/stream_chat_flutter_core/example/windows/CMakeLists.txt b/packages/stream_chat_flutter_core/example/windows/CMakeLists.txt new file mode 100644 index 00000000..1633297a --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/stream_chat_flutter_core/example/windows/flutter/CMakeLists.txt b/packages/stream_chat_flutter_core/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..b2e4bd8d --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugin_registrant.cc b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..8083d749 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); +} diff --git a/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugin_registrant.h b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugins.cmake b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..ba4a2175 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus_windows +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/stream_chat_flutter_core/example/windows/runner/CMakeLists.txt b/packages/stream_chat_flutter_core/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..de2d8916 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/stream_chat_flutter_core/example/windows/runner/Runner.rc b/packages/stream_chat_flutter_core/example/windows/runner/Runner.rc new file mode 100644 index 00000000..79f0da9a --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/stream_chat_flutter_core/example/windows/runner/flutter_window.cpp b/packages/stream_chat_flutter_core/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/stream_chat_flutter_core/example/windows/runner/flutter_window.h b/packages/stream_chat_flutter_core/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/stream_chat_flutter_core/example/windows/runner/main.cpp b/packages/stream_chat_flutter_core/example/windows/runner/main.cpp new file mode 100644 index 00000000..bcb57b0e --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/stream_chat_flutter_core/example/windows/runner/resource.h b/packages/stream_chat_flutter_core/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/stream_chat_flutter_core/example/windows/runner/resources/app_icon.ico b/packages/stream_chat_flutter_core/example/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/packages/stream_chat_flutter_core/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/stream_chat_flutter_core/example/windows/runner/runner.exe.manifest b/packages/stream_chat_flutter_core/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..c977c4a4 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter_core/example/windows/runner/utils.cpp b/packages/stream_chat_flutter_core/example/windows/runner/utils.cpp new file mode 100644 index 00000000..d19bdbbc --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/stream_chat_flutter_core/example/windows/runner/utils.h b/packages/stream_chat_flutter_core/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/stream_chat_flutter_core/example/windows/runner/win32_window.cpp b/packages/stream_chat_flutter_core/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..c10f08dc --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/stream_chat_flutter_core/example/windows/runner/win32_window.h b/packages/stream_chat_flutter_core/example/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/packages/stream_chat_flutter_core/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart index d807091c..51be28e7 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:async'; import 'dart:convert'; @@ -54,6 +56,11 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// /// Make sure to have a [StreamChatCore] ancestor in order to provide the /// information about the channels. +@Deprecated(''' +ChannelListCore is deprecated and will be removed in the next +major version. Use StreamChannelListController instead to create your custom list. +More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_channel_list_controller +''') class ChannelListCore extends StatefulWidget { /// Instantiate a new ChannelListView const ChannelListCore({ diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 76bf3854..2f8838b5 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index 0e8d011d..0c302c72 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 565743f2..e86d020b 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:convert'; import 'package:flutter/foundation.dart'; @@ -31,6 +33,11 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// information about the messages. /// The widget uses a [ListView.separated] to render the list of messages. /// +@Deprecated(''' +MessageSearchListCore is deprecated and will be removed in the next +major version. Use StreamMessageSearchListController instead to create your custom list. +More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_message_search_list_controller +''') class MessageSearchListCore extends StatefulWidget { /// Instantiate a new [MessageSearchListView]. /// The following parameters must be supplied and not null: diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart index 56037075..00d96f56 100644 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart @@ -463,7 +463,8 @@ class PagedValueGridView extends StatefulWidget { /// only scroll the view if it has sufficient content. See [physics]. /// /// Also when true, the scroll view is used for default [ScrollAction]s. If a - /// ScrollAction is not handled by an otherwise focused part of the application, + /// ScrollAction is not handled by + /// an otherwise focused part of the application, /// the ScrollAction will be evaluated using this scroll view, for example, /// when executing [Shortcuts] key events like page up and down. /// @@ -593,7 +594,8 @@ class PagedValueGridView extends StatefulWidget { /// /// See also: /// - /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. + /// * [SemanticsConfiguration.scrollChildCount], the corresponding + /// semantics property. final int? semanticChildCount; /// {@macro flutter.widgets.scrollable.dragStartBehavior} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart index 8d548c67..92b5650b 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -49,22 +49,8 @@ class StreamChannelListEventHandler { /// This event is fired when a channel is updated. /// /// By default, this updates the channel received in the event. - void onChannelUpdated(Event event, StreamChannelListController controller) { - final eventChannel = event.channel; - if (eventChannel == null) return; - - final channels = [...controller.currentItems]; - final channelIndex = channels.indexWhere( - (it) => it.cid == (event.cid ?? eventChannel.cid), - ); - - if (channelIndex >= 0) { - final channelState = ChannelState(channel: eventChannel); - channels[channelIndex].state?.updateChannelState(channelState); - } - - controller.channels = channels; - } + // ignore: no-empty-block + void onChannelUpdated(Event event, StreamChannelListController controller) {} /// Function which gets called for the event /// [EventType.channelVisible]. diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart similarity index 85% rename from packages/stream_chat_flutter_core/lib/src/message_input_controller.dart rename to packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart index 65c6d8d8..b8f9107d 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart @@ -8,46 +8,46 @@ import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart' /// A value listenable builder related to a [Message]. /// -/// Pass in a [MessageInputController] as the `valueListenable`. -typedef MessageValueListenableBuilder = ValueListenableBuilder; +/// Pass in a [StreamMessageInputController] as the `valueListenable`. +typedef StreamMessageValueListenableBuilder = ValueListenableBuilder; /// Controller for storing and mutating a [Message] value. -class MessageInputController extends ValueNotifier { +class StreamMessageInputController extends ValueNotifier { /// Creates a controller for an editable text field. /// /// This constructor treats a null [message] argument as if it were the empty /// message. - factory MessageInputController({ + factory StreamMessageInputController({ Message? message, Map? textPatternStyle, }) => - MessageInputController._( + StreamMessageInputController._( initialMessage: message ?? Message(), textPatternStyle: textPatternStyle, ); /// Creates a controller for an editable text field from an initial [text]. - factory MessageInputController.fromText( + factory StreamMessageInputController.fromText( String? text, { Map? textPatternStyle, }) => - MessageInputController._( + StreamMessageInputController._( initialMessage: Message(text: text), textPatternStyle: textPatternStyle, ); /// Creates a controller for an editable text field from initial /// [attachments]. - factory MessageInputController.fromAttachments( + factory StreamMessageInputController.fromAttachments( List attachments, { Map? textPatternStyle, }) => - MessageInputController._( + StreamMessageInputController._( initialMessage: Message(attachments: attachments), textPatternStyle: textPatternStyle, ); - MessageInputController._({ + StreamMessageInputController._({ required Message initialMessage, Map? textPatternStyle, }) : _textEditingController = MessageTextFieldController.fromValue( @@ -245,7 +245,7 @@ class MessageInputController extends ValueNotifier { /// will all be empty. /// /// Calling this will notify all the listeners of this - /// [MessageInputController] that they need to update + /// [StreamMessageInputController] that they need to update /// (calls [notifyListeners]). For this reason, /// this method should only be called between frames, e.g. in response to user /// actions, not during the build, layout, or paint phases. @@ -272,36 +272,37 @@ class MessageInputController extends ValueNotifier { } /// A [RestorableProperty] that knows how to store and restore a -/// [MessageInputController]. +/// [StreamMessageInputController]. /// -/// The [MessageInputController] is accessible via the [value] getter. During -/// state restoration, the property will restore [MessageInputController.value] +/// The [StreamMessageInputController] is accessible via the [value] getter. +/// During state restoration, +/// the property will restore [StreamMessageInputController.value] /// to the value it had when the restoration data it is getting restored from /// was collected. -class RestorableMessageInputController - extends RestorableChangeNotifier { - /// Creates a [RestorableMessageInputController]. +class StreamRestorableMessageInputController + extends RestorableChangeNotifier { + /// Creates a [StreamRestorableMessageInputController]. /// /// This constructor creates a default [Message] when no `message` argument /// is supplied. - RestorableMessageInputController({Message? message}) + StreamRestorableMessageInputController({Message? message}) : _initialValue = message ?? Message(); - /// Creates a [RestorableMessageInputController] from an initial + /// Creates a [StreamRestorableMessageInputController] from an initial /// [text] value. - factory RestorableMessageInputController.fromText(String? text) => - RestorableMessageInputController(message: Message(text: text)); + factory StreamRestorableMessageInputController.fromText(String? text) => + StreamRestorableMessageInputController(message: Message(text: text)); final Message _initialValue; @override - MessageInputController createDefaultValue() => - MessageInputController(message: _initialValue); + StreamMessageInputController createDefaultValue() => + StreamMessageInputController(message: _initialValue); @override - MessageInputController fromPrimitives(Object? data) { + StreamMessageInputController fromPrimitives(Object? data) { final message = Message.fromJson(json.decode(data! as String)); - return MessageInputController(message: message); + return StreamMessageInputController(message: message); } @override diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 61fbb860..03159afb 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:convert'; import 'package:flutter/foundation.dart'; @@ -53,6 +55,11 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and /// [errorBuilder] must all be supplied and not null. +@Deprecated(''' +UserListCore is deprecated and will be removed in the next +major version. Use StreamUserListController instead to create your custom list. +More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_user_list_controller +''') class UserListCore extends StatefulWidget { /// Instantiate a new [UserListCore] const UserListCore({ diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index 63b88974..c01242ec 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index cddae515..a5cd79df 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -7,17 +7,18 @@ export 'src/better_stream_builder.dart'; export 'src/channel_list_core.dart' hide ChannelListCoreState; export 'src/channels_bloc.dart'; export 'src/lazy_load_scroll_view.dart'; -export 'src/message_input_controller.dart'; export 'src/message_list_core.dart' hide MessageListCoreState; export 'src/message_search_bloc.dart'; export 'src/message_search_list_core.dart' hide MessageSearchListCoreState; export 'src/message_text_field_controller.dart'; -export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder; +export 'src/paged_value_notifier.dart' + show PagedValueListenableBuilder, PagedValue; export 'src/paged_value_scroll_view.dart'; export 'src/stream_channel.dart'; export 'src/stream_channel_list_controller.dart'; export 'src/stream_channel_list_event_handler.dart'; export 'src/stream_chat_core.dart'; +export 'src/stream_message_input_controller.dart'; export 'src/stream_message_search_list_controller.dart'; export 'src/stream_user_list_controller.dart'; export 'src/typedef.dart'; diff --git a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart index 5f7c0e03..f94ab3c6 100644 --- a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:async'; import 'package:flutter/widgets.dart'; diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index e6365868..630ef881 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:async'; import 'package:flutter/widgets.dart'; diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart index 764e629b..c1465731 100644 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart index d699faa0..c56e377b 100644 --- a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index 27f4cbe8..86ae601d 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart index 549f9291..ce28c2f8 100644 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/users_bloc_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 2a8addab..e318cc18 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +* Added translations for viewLibrary. + ## 3.0.0-beta.1 * Updated `stream_chat_flutter` dependency to [`4.0.0-beta.1`](https://pub.dev/packages/stream_chat_flutter/changelog). diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index d111ead0..d0eca8e8 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -403,6 +403,9 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get linkDisabledError => 'Links are disabled'; + + @override + String get viewLibrary => 'View library'; } void main() async { @@ -500,7 +503,8 @@ class MyApp extends StatelessWidget { /// A list of messages sent in the current channel. /// -/// This is implemented using [MessageListView], a widget that provides query +/// This is implemented using [StreamMessageListView], +/// a widget that provides query /// functionalities fetching the messages from the api and showing them in a /// listView. class ChannelPage extends StatelessWidget { @@ -511,13 +515,13 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 82d7e92c..55354e50 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -94,7 +94,8 @@ class MyApp extends StatelessWidget { /// A list of messages sent in the current channel. /// -/// This is implemented using [MessageListView], a widget that provides query +/// This is implemented using [StreamMessageListView], +/// a widget that provides query /// functionalities fetching the messages from the api and showing them in a /// listView. class ChannelPage extends StatelessWidget { @@ -105,13 +106,13 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index 6f311544..33ebaa99 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -121,7 +121,8 @@ class MyApp extends StatelessWidget { /// A list of messages sent in the current channel. /// -/// This is implemented using [MessageListView], a widget that provides query +/// This is implemented using [StreamMessageListView], +/// a widget that provides query /// functionalities fetching the messages from the api and showing them in a /// listView. class ChannelPage extends StatelessWidget { @@ -132,13 +133,13 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); 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 6ea2eda4..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,8 +74,9 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { return const StreamChatLocalizationsKo(); case 'pt': return const StreamChatLocalizationsPt(); + default: + return null; } - return null; } /// Implementation of localized strings for the stream chat widgets diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 08dbb02a..1d1d82b3 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -379,4 +379,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get linkDisabledError => 'Links are disabled'; + + @override + String get viewLibrary => 'View library'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 50c748cb..b906843f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -376,6 +376,9 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { No es posible añadir más de $limit archivos adjuntos '''; + @override + String get viewLibrary => 'Ver Librería'; + @override String get slowModeOnLabel => 'Modo lento activado'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 0671f5af..b5a44a51 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -375,6 +375,9 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $limit pièces jointes '''; + @override + String get viewLibrary => 'Voir la bibliothèque'; + @override String get slowModeOnLabel => 'Mode lent activé'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 1244e2ea..e07566bd 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -369,6 +369,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { अटैचमेंट लिमिट: $limit अटैचमेंट से अधिक जोड़ना संभव नहीं है '''; + @override + String get viewLibrary => 'पुस्तकालय देखिये'; + @override String get slowModeOnLabel => 'स्लो मोड चालू'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 1f7fd048..ffbdde94 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -372,6 +372,9 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; Attenzione: il limite massimo di $limit file è stato superato. '''; + @override + String get viewLibrary => 'Vedi la biblioteca'; + @override String get slowModeOnLabel => 'Slowmode attiva'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index e1eea3a4..7729a6ca 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -353,6 +353,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'スローモードオン'; + @override + String get viewLibrary => 'ライブラリを表示'; + @override String attachmentLimitExceedError(int limit) => ''' 添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 820d58e4..c7c858f1 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -354,6 +354,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => '슬로모드 켜짐'; + @override + @override + String get viewLibrary => '라이브러리 보기'; + @override String attachmentLimitExceedError(int limit) => '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 4c1d2a58..3a1a7696 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -382,4 +382,7 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get sendMessagePermissionError => 'Você não tem permissão para enviar mensagens'; + + @override + String get viewLibrary => 'Ver biblioteca'; } 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 5b180e30..6eb29139 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';