diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 33298866a..5f92c714d 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -35,7 +35,7 @@ jobs: run: | flutter pub global activate melos ${{ env.melos_version }} - name: "Bootstrap Workspace" - run: melos bootstrap + run: melos bootstrap --verbose - name: "Dart Analyze" run: | melos run analyze diff --git a/README.md b/README.md index 061a193d4..5d78e7bf4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ This repository contains code for our [Dart](https://dart.dev/) and [Flutter](ht Stream allows developers to rapidly deploy scalable feeds and chat messaging with an industry leading 99.999% uptime SLA guarantee. +**V4 Migration Guide** + +For upgrading from V3 to V4, please refer to the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/) + ## Sample apps and demos Our team maintains a dedicated repository for fully-fledged sample applications and demos. Consider checking out [GetStream/flutter-samples](https://github.com/GetStream/flutter-samples) to learn more or get started by looking at our latest [Stream Chat demo](https://github.com/GetStream/flutter-samples/tree/main/packages/stream_chat_v1). diff --git a/analysis_options.yaml b/analysis_options.yaml index 03a4220e0..336d222ef 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -91,7 +91,6 @@ linter: - prefer_constructors_over_static_methods - prefer_contains - prefer_equal_for_default_values - - prefer_expression_function_bodies - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals diff --git a/docusaurus/docs/Flutter/assets/slidable_demo.jpg b/docusaurus/docs/Flutter/assets/slidable_demo.jpg new file mode 100644 index 000000000..4cbda5d73 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/slidable_demo.jpg differ 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 96d24a055..9a2863b60 100644 --- a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx +++ b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx @@ -40,9 +40,9 @@ Scaffold( child: Column( children: [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ), @@ -81,17 +81,18 @@ Stack( child: Column( children: const [ Expanded( - child: MessageListViewTheme( - data: MessageListViewThemeData( + child: StreamMessageListViewTheme( + data: StreamMessageListViewThemeData( backgroundColor: Colors.transparent, ), - child: MessageListView(), + child: StreamMessageListView(), ), ), - MessageInput(), + StreamMessageInput(), ], ), ), ], ), -``` \ No newline at end of file +``` + diff --git a/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx b/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx index 9fe7f6aad..510e8c22a 100644 --- a/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx +++ b/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx @@ -13,7 +13,7 @@ own types of attachments through the SDK such as location, audio, etc. This involves doing three things: -1) Rendering the attachment thumbnail in the `MessageInput` +1) Rendering the attachment thumbnail in the `StreamMessageInput` 2) Sending a message with the custom attachment @@ -27,7 +27,7 @@ Let's build an example of location sharing option in the app: ![](../assets/location_sharing_example.jpg) -* Show a "Share Location" button next to MessageInput Textfield. +* Show a "Share Location" button next to StreamMessageInput Textfield. * When the user presses this button, it should fetch the current location coordinates of the user, and send a message on the channel as follows: @@ -53,14 +53,14 @@ Please check their [setup instructions](https://pub.dev/packages/geolocator) on NOTE: If you are testing on iOS simulator, you will need to set some dummy coordinates, as mentioned [here](https://stackoverflow.com/a/31238119/7489541). Also don't forget to enable "location update" capability in background mode, from XCode. -On the receiver end, `location` type attachment should be rendered in map view, in the `MessageListView`. +On the receiver end, `location` type attachment should be rendered in map view, in the `StreamMessageListView`. We are going to use [Google Static Maps API](https://developers.google.com/maps/documentation/maps-static/overview) to render the map in the message. You can use other libraries as well such as [google_maps_flutter](https://pub.dev/packages/google_maps_flutter). First, we add a button which when clicked fetches and shares location into the `MessageInput`: ```dart -MessageInput( +StreamMessageInput( actions: [ InkWell( child: Icon( @@ -146,10 +146,10 @@ Next, we build the Static Maps URL (Add your API key before using the code snipp } ``` -And then modify the `MessageListView` and tell it how to build a location attachment, using the `messageBuilder` property and copying the default message implementation overriding the `customAttachmentBuilders` property: +And then modify the `StreamMessageListView` and tell it how to build a location attachment, using the `messageBuilder` property and copying the default message implementation overriding the `customAttachmentBuilders` property: ```dart -MessageListView( +StreamMessageListView( messageBuilder: (context, details, messages, defaultMessage) { return defaultMessage.copyWith( customAttachmentBuilders: { @@ -179,15 +179,15 @@ To do this, we will: 1) Add an attachment instead of sending a message -2) Customize the `MessageInput` +2) Customize the `StreamMessageInput` First, we add the attachment when the location button is clicked: ```dart - GlobalKey _messageInputKey = GlobalKey(); + StreamMessageInputController _messageInputController = StreamMessageInputController(); - MessageInput( - key: _messageInputKey, + StreamMessageInput( + messageInputController: _messageInputController, actions: [ InkWell( child: Icon( @@ -197,7 +197,7 @@ First, we add the attachment when the location button is clicked: ), onTap: () { _determinePosition().then((value) { - _messageInputKey.currentState.addAttachment( + _messageInputController.addAttachment( Attachment( uploadState: UploadState.success(), type: 'location', @@ -219,8 +219,8 @@ First, we add the attachment when the location button is clicked: After this, we can build the thumbnail: ```dart -MessageInput( - key: _messageInputKey, +StreamMessageInput( + messageInputController: _messageInputController, actions: [ InkWell( child: Icon( @@ -230,7 +230,7 @@ MessageInput( ), onTap: () { _determinePosition().then((value) { - _messageInputKey.currentState.addAttachment( + _messageInputController.addAttachment( Attachment( uploadState: UploadState.success(), type: 'location', @@ -259,6 +259,6 @@ MessageInput( ), ``` -And we can see the thumbnails in the MessageInput: +And we can see the thumbnails in the StreamMessageInput: ![](../assets/location_sharing_example_message_thumbnail.jpg) diff --git a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx index c3dc2abaa..12cc77157 100644 --- a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx +++ b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx @@ -38,10 +38,10 @@ Additionally, pinning a message requires you to add the roles which are allowed ### Partially remove some message actions For example, if you only want to keep "copy message" and "delete message", -here is how to do it using the `messageBuilder` with our `MessageWidget`. +here is how to do it using the `messageBuilder` with our `StreamMessageWidget`. ```dart -MessageListView( +StreamMessageListView( messageBuilder: (context, details, messages, defaultMessage) { return defaultMessage.copyWith( showFlagButton: false, @@ -61,14 +61,14 @@ The SDK also allows you to add new actions into the dialog. For example, let's suppose you want to introduce a new message action - "Demo Action": -We use the `customActions` parameter of the `MessageWidget` to add extra actions. +We use the `customActions` parameter of the `StreamMessageWidget` to add extra actions. ```dart -MessageListView( +StreamMessageListView( messageBuilder: (context, details, messages, defaultMessage) { return defaultMessage.copyWith( customActions: [ - MessageAction( + StreamMessageAction( leading: Icon(Icons.add), title: Text('Demo Action'), onTap: (message) { diff --git a/docusaurus/docs/Flutter/guides/customize_message_widget.mdx b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx index af7014fb2..600f05d18 100644 --- a/docusaurus/docs/Flutter/guides/customize_message_widget.mdx +++ b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx @@ -1,7 +1,7 @@ --- id: customize_message_widget sidebar_position: 11 -title: Customizing The MessageWidget +title: Customizing The StreamMessageWidget --- Customizing Text Messages @@ -11,16 +11,16 @@ Customizing Text Messages Every application provides a unique look and feel to their own messaging interface including and not limited to fonts, colors, and shapes. -This guide details how to customize the `MessageWidget` in the Stream Chat Flutter UI SDK. +This guide details how to customize the `StreamMessageWidget` in the Stream Chat Flutter UI SDK. ### Building Custom Messages -This guide goes into detail about the ability to customize the `MessageWidget`. However, if you want -to customize the default `MessageWidget` in the `MessageListView` provided, you can use the `.copyWith()` method -provided inside the `messageBuilder` parameter of the `MessageListView` like this: +This guide goes into detail about the ability to customize the `StreamMessageWidget`. However, if you want +to customize the default `StreamMessageWidget` in the `StreamMessageListView` provided, you can use the `.copyWith()` method +provided inside the `messageBuilder` parameter of the `StreamMessageListView` like this: ```dart -MessageListView( +StreamMessageListView( messageBuilder: (context, details, messageList, defaultImpl) { // Your implementation of the message here // E.g: return Text(details.message.text ?? ''); @@ -30,8 +30,8 @@ MessageListView( ### Theming -You can customize the `MessageWidget` using the `StreamChatTheme` class, so that you can change the -message theme at the top instead of creating your own `MessageWidget` at the lower implementation level. +You can customize the `StreamMessageWidget` using the `StreamChatTheme` class, so that you can change the +message theme at the top instead of creating your own `StreamMessageWidget` at the lower implementation level. There are several things you can change in the theme including text styles and colors of various elements. @@ -49,13 +49,13 @@ Here is an example: StreamChatThemeData( /// Sets theme for user's messages - ownMessageTheme: MessageThemeData( + ownMessageTheme: StreamMessageThemeData( messageBackgroundColor: colorTheme.textHighEmphasis, ), /// Sets theme for received messages - otherMessageTheme: MessageThemeData( - avatarTheme: AvatarThemeData( + otherMessageTheme: StreamMessageThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), @@ -67,11 +67,11 @@ StreamChatThemeData( #### Change message text style -The `MessageWidget` has multiple `Text` widgets that you can manipulate the styles of. The three main +The `StreamMessageWidget` has multiple `Text` widgets that you can manipulate the styles of. The three main are the actual message text, user name, message links, and the message timestamp. ```dart -MessageThemeData( +StreamMessageThemeData( messageTextStyle: TextStyle(...), createdAtStyle: TextStyle(...), messageAuthorStyle: TextStyle(...), @@ -86,8 +86,8 @@ MessageThemeData( You can change the attributes of the avatar (if displayed) using the `avatarTheme` property. ```dart -MessageThemeData( - avatarTheme: AvatarThemeData( +StreamMessageThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(8), ), ) @@ -100,7 +100,7 @@ MessageThemeData( You also customize the reactions attached to every message using the theme. ```dart -MessageThemeData( +StreamMessageThemeData( reactionsBackgroundColor: Colors.red, reactionsBorderColor: Colors.redAccent, reactionsMaskColor: Colors.pink, @@ -111,12 +111,12 @@ MessageThemeData( ### Changing Message Actions -When a message is long pressed, the `MessageActionsModal` is shown. +When a message is long pressed, the `StreamMessageActionsModal` is shown. -The `MessageWidget` allows showing or hiding some options if you so choose. +The `StreamMessageWidget` allows showing or hiding some options if you so choose. ```dart -MessageWidget( +StreamMessageWidget( ... showUsername = true, showTimestamp = true, @@ -137,11 +137,11 @@ MessageWidget( ### Building attachments -The `customAttachmentBuilder` property allows you to build any kind of attachment (inbuilt or custom) +The `customAttachmentBuilders` property allows you to build any kind of attachment (inbuilt or custom) in your own way. While a separate guide is written for this, it is included here because of relevance. ```dart -MessageListView( +StreamMessageListView( messageBuilder: (context, details, messages, defaultMessage) { return defaultMessage.copyWith( customAttachmentBuilders: { @@ -163,7 +163,7 @@ MessageListView( ### Widget Builders -Some parameters allow you to construct your own widget in place of some elements in the `MessageWidget`. +Some parameters allow you to construct your own widget in place of some elements in the `StreamMessageWidget`. These are: * `userAvatarBuilder` : Allows user to substitute their own widget in place of the user avatar. @@ -173,7 +173,7 @@ These are: * `deletedBottomRowBuilder` : Allows user to substitute their own widget in the bottom of the message when deleted. ```dart -MessageWidget( +StreamMessageWidget( ... textBuilder: (context, message) { // Add your own text implementation here. diff --git a/docusaurus/docs/Flutter/guides/customize_text_messages.mdx b/docusaurus/docs/Flutter/guides/customize_text_messages.mdx index a2d373330..57b3301de 100644 --- a/docusaurus/docs/Flutter/guides/customize_text_messages.mdx +++ b/docusaurus/docs/Flutter/guides/customize_text_messages.mdx @@ -11,38 +11,38 @@ Customizing Text Messages Every application provides a unique look and feel to their own messaging interface including and not limited to fonts, colors, and shapes. -This guide details how to customize message text in the `MessageListView` / `MessageWidget` in the +This guide details how to customize message text in the `StreamMessageListView` / `StreamMessageWidget` in the Stream Chat Flutter UI SDK. :::note -This guide is specifically for the `MessageListView` but if you intend to display a `MessageWidget` +This guide is specifically for the `StreamMessageListView` but if you intend to display a `StreamMessageWidget` separately, follow the same process without the `.copyWith` and use the default constructor instead. ::: -### Basics of customizing a `MessageWidget` +### Basics of customizing a `StreamMessageWidget` -First, add a `MessageListView` in the appropriate place where you intend to display messages from a +First, add a `StreamMessageListView` in the appropriate place where you intend to display messages from a channel. ```dart -MessageListView( +StreamMessageListView( ... ) ``` Now, we use the `messageBuilder` parameter to build a custom message. The builder function also provides -the default implementation of the `MessageWidget` so that we can change certain aspects of the widget +the default implementation of the `StreamMessageWidget` so that we can change certain aspects of the widget without redoing all of the default parameters. :::note -In earlier versions of the SDK, some `MessageWidget` parameters were exposed directly through the `MessageListView`, +In earlier versions of the SDK, some `StreamMessageWidget` parameters were exposed directly through the `StreamMessageListView`, however, this quickly becomes hard to maintain as more parameters and customizations are added to the -`MessageWidget`. Newer version utilise a cleaner interface to change the parameters by supplying a +`StreamMessageWidget`. Newer version utilise a cleaner interface to change the parameters by supplying a default message implementation as aforementioned. ::: ```dart -MessageListView( +StreamMessageListView( ... messageBuilder: (context, messageDetails, messageList, defaultWidget) { return defaultWidget; @@ -53,7 +53,7 @@ MessageListView( We use `.copyWith()` to customize the widget: ```dart -MessageListView( +StreamMessageListView( ... messageBuilder: (context, messageDetails, messageList, defaultWidget) { return defaultWidget.copyWith( @@ -66,28 +66,28 @@ MessageListView( ### Customizing text If you intend to simply change the theme for the text, you need not recreate the whole widget. The -`MessageWidget` has a `messageTheme` parameter that allows you to pass the theme for most aspects +`StreamMessageWidget` has a `messageTheme` parameter that allows you to pass the theme for most aspects of the message. ```dart -MessageListView( +StreamMessageListView( ... messageBuilder: (context, messageDetails, messageList, defaultWidget) { return defaultWidget.copyWith( - messageTheme: MessageTheme( + messageTheme: StreamMessageThemeData( ... - messageText: TextStyle(), + messageTextStyle: TextStyle(), ), ); }, ) ``` -If you want to replace the entire text widget in the `MessageWidget`, you can use the `textBuilder` +If you want to replace the entire text widget in the `StreamMessageWidget`, you can use the `textBuilder` parameter which provides a builder for creating a widget to substitute the default text.parameter ```dart -MessageListView( +StreamMessageListView( ... messageBuilder: (context, messageDetails, messageList, defaultWidget) { return defaultWidget.copyWith( @@ -101,10 +101,10 @@ MessageListView( ### Adding Hashtags -To add elements like hashtags, we can override the `textBuilder` in the MessageWidget: +To add elements like hashtags, we can override the `textBuilder` in the StreamMessageWidget: ```dart -MessageListView( +StreamMessageListView( ... messageBuilder: (context, messageDetails, messageList, defaultWidget) { return defaultWidget.copyWith( diff --git a/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx b/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx index 8a85c6a50..8a463af49 100644 --- a/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx +++ b/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx @@ -190,7 +190,7 @@ await client.connectUser( Now you will use the `encryptMessage()` function created in the previous steps to encrypt the message. -To do that, you need to make some minor changes to the **MessageInput** widget. +To do that, you need to make some minor changes to the **StreamMessageInput** widget. ```dart final receiverJwk = receiver.extraData['publicKey']; @@ -200,7 +200,7 @@ final derivedKey = await deriveKey(keyPair.privateKey, receiverJwk); ``` ```dart -MessageInput( +StreamMessageInput( ... @@ -223,10 +223,10 @@ Here, you have used it to encrypt the message before sending it to Stream’s ba Now, it’s time to decrypt the message and present it in a human-readable format to the receiver. -You can customize the **MessageListView** widget to have a custom `messagebuilder`, that can decrypt the message. +You can customize the **StreamMessageListView** widget to have a custom `messagebuilder`, that can decrypt the message. ```dart -MessageListView( +StreamMessageListView( ... messageBuilder: (context, messageDetails, currentMessages, defaultWidget) { // Retrieving the message from details diff --git a/docusaurus/docs/Flutter/guides/migration_guide_4_0.mdx b/docusaurus/docs/Flutter/guides/migration_guide_4_0.mdx new file mode 100644 index 000000000..28a43ca19 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/migration_guide_4_0.mdx @@ -0,0 +1,753 @@ +--- +id: migration_guide_4_0 +sidebar_position: 14 +title: Migration Guide For The Stream Chat Flutter SDK v4.0 +--- + +**Version 4.0.0** of the Stream Chat Flutter SDK carries significant architectural changes to improve the developer experience by giving you more control and flexibility in how you use our core components and UI widgets. + +This guide is intended to enumerate and better explain the changes in the SDK. + +If you find any bugs or have any questions, please file an [issue on our GitHub repository](https://github.com/GetStream/stream-chat-flutter/issues). We want to support you as much as we can with this migration. + +Code examples: + +- See our [Stream Chat Flutter tutorial](https://getstream.io/chat/flutter/tutorial/) for an up-to-date guide using the latest Stream Chat version. +- See the [Stream Flutter Samples repository](https://github.com/GetStream/flutter-samples) with our fully-fledged messaging [sample application](https://github.com/GetStream/flutter-samples/tree/main/packages/stream_chat_v1). + +All of our documentation has also been updated to support v4, so all of the guides and examples will have updated code. + +### Dependencies + +To migrate to v4.0.0, update your `pubspec.yaml` with the correct Stream chat package you're using: + +```yaml +dependencies: + stream_chat_flutter: ^4.0.0 # full UI, core and client packages + stream_chat_flutter_core: ^4.0.0 # core and client packages + stream_chat: ^4.0.0 # client package +``` + +--- + +## Name Changes + +The majority of the Stream Chat widgets and classes have now been renamed to have a “Stream” prefix associated with them. This increases Stream widgets' discoverability and avoids name conflicts when importing. + +For example, `MessageListView` is now called `StreamMessageListView`, and `UserAvatar` is renamed to `StreamUserAvatar`. + +**The old class names are deprecated and will be removed in the next major release (v5.0.0).** + +See the sections below on “deprecated classes” for a complete list of changes. Some of these classes/widgets have undergone functional changes as well, that will be explore in the following sections. + +## Removed Functionality/Widgets + +This section highlights functionality removed. + +## Removed methods and classes + +In version 4 we removed the following deprecated methods and classes: + +* `Channel.banUser` + +* `Channel.unbanUser` + +* `ClientState.user` + +* `ClientState.userStream` + +* `MessageWidget.allRead` + +* `MessageWidget.readList` + +* `StreamChat.user` + +* `StreamChat.userStream` + +* `StreamChatCore.user` + +* `StreamChatCore.userStream` + +These were marked as deprecated in v3. + +### Video Compression + +The automatic video compression when uploading a video has been removed. You can integrate this yourself by manipulating attachments using a [custom attachment uploader](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart). + +### Slidable Channel List Item + +The default slidable channel preview behavior has been removed. We have created a [guide](slidable_channel_list_preview.mdx) showing you how you can easily add this functionality yourself. + +![Slidable demo](../assets/slidable_demo.jpg) + +### Pin Permission + +`pinPermissions` is no longer needed in the **MessageListView** widget. The permissions are automatically fetched for each Stream project. To enable users to pin the message, make sure the pin permissions are granted for different types of users on your [Stream application dashboard](https://dashboard.getstream.io/). + +--- + +## Deprecated Classes + +This section covers all the deprecated classes and widgets in the Stream chat packages. Some of these have also undergone functional changes, for example, **MessageInput** and **ChannelsBloc**. These are discussed in more detail below. + +The majority of the Stream widgets and classes have now been renamed to have a **"Stream"** prefix associated with them. + +Changes: + +- `AttachmentTitle` in favor of `StreamAttachmentTitle` +- `AttachmentUploadStateBuilder` in favor of `StreamAttachmentsUploadStateBuilder` +- `AttachmentWidget` in favor of `StreamAttachmentWidget` +- `AvatarThemeData` in favor of `StreamAvatarThemeData` +- `ChannelAvatar` in favor of `StreamChannelAvatar` +- `ChannelBottomSheet` in favor of `StreamChannelInfoBottomSheet` +- `ChannelHeader` in favor of `StreamChannelHeader` +- `ChannelHeaderTheme` in favor of `StreamChannelHeaderTheme` +- `ChannelHeaderThemeData` in favor of `StreamChannelHeaderThemeData` +- `ChannelInfo` in favor of `StreamChannelInfo` +- `ChannelListHeader` in favor of `StreamChannelListHeader` +- `ChannelListHeaderTheme` in favor of `StreamChannelListHeaderTheme` +- `ChannelListHeaderThemeData` in favor of `StreamChannelListHeaderThemeData` +- `ChannelListView` in favor of `StreamChannelListView` +- `ChannelListViewTheme` in favor of `StreamChannelListViewTheme` +- `ChannelListViewThemeData` in favor of `StreamChannelListViewThemeData` +- `ChannelListHeader` in favor of `StreamChannelListHeader` +- `ChannelListView` in favor of `StreamChannelListView` +- `ChannelName` in favor of `StreamChannelName` +- `ChannelPreview` in favor of `StreamChannelListTile` +- `ChannelPreviewTheme` in favor of `StreamChannelPreviewTheme` +- `ChannelPreviewThemeData` in favor of `StreamChannelPreviewThemeData` +- `ChannelName` in favor of `StreamChannelName` +- `ColorTheme` in favor of `StreamColorTheme` +- `CommandsOverlay` in favor of `StreamCommandsOverlay` +- `ConnectionStatusBuilder` in favor of `StreamConnectionStatusBuilder` +- `DateDivider` in favor of `StreamDateDivider` +- `DeletedMessage` in favor of `StreamDeletedMessage` +- `EmojiOverlay` in favor of `StreamEmojiOverlay` +- `FileAttachment` in favor of `StreamFileAttachment` +- `FullScreenMedia` in favor of `StreamFullScreenMedia` +- `GalleryFooter` in favor of `StreamGalleryFooter` +- `GalleryFooterThemeData` in favor of `StreamGalleryFooterThemeData` +- `GalleryHeader` in favor of `StreamGalleryHeader` +- `GalleryHeaderTheme` in favor of `StreamGalleryHeaderTheme` +- `GalleryHeaderThemeData` in favor of `StreamGalleryHeaderThemeData` +- `GiphyAttachment` in favor of `StreamGiphyAttachment` +- `GradientAvatar` in favor of `StreamGradientAvatar` +- `GroupAvatar` in favor of `StreamGroupAvatar` +- `ImageAttachment` in favor of `StreamImageAttachment` +- `ImageGroup` in favor of `StreamImageGroup` +- `InfoTile` in favor of `StreamInfoTile` +- `MediaListView` in favor of `StreamMediaListView` +- `MessageAction` in favor of `StreamMessageAction` +- `MessageActionsModal` in favor `StreamMessageActionsModal` +- `MessageInput` in favor of `StreamMessageInput` +- `MessageInputTheme` in favor of `StreamMessageInputTheme` +- `MessageInputThemeData` in favor of `StreamMessageInputThemeData` +- `MessageInputState` in favor of `StreamMessageInput` +- `MessageListView` in favor of `StreamMessageListView` +- `MessageListViewTheme` in favor of `StreamMessageListViewTheme` +- `MessageListViewThemeData` in favor of `StreamMessageListViewThemeData` +- `MessageSearchListView` in favor of `StreamMessageSearchListView` +- `MessageSearchListViewTheme` in favor of `StreamMessageSearchListViewTheme` +- `MessageSearchListViewThemeData` in favor of `StreamMessageSearchListViewThemeData` +- `MessageReactionsModal` in favor of `StreamMessageReactionsModal` +- `MessageSearchItem` in favor of `StreamMessageSearchItem` +- `MessageSearchListView` in favor of `StreamMessageSearchListView` +- `MessageText` in favor of `StreamMessageText` +- `MessageWidget` in favor of `StreamMessageWidget` +- `MessageThemeData` in favor of `StreamMessageThemeData` +- `MultiOverlay` in favor of `StreamMultiOverlay` +- `OptionListTile` in favor of `StreamOptionListTile` +- `QuotedMessageWidget` in favor of `StreamQuotedMessageWidget` +- `ReactionBubble` in favor of `StreamReactionBubble` +- `ReactionIcon` in favor of `StreamReactionIcon` +- `ReactionPicker` in favor of `StreamReactionPicker` +- `SendingIndicator` in favor of `StreamSendingIndicator` +- `SystemMessage` in favor of `StreamSystemMessage` +- `TextTheme` in favor of `StreamTextTheme` +- `ThreadHeader` in favor of `StreamThreadHeader` +- `TypingIndicator` in favor of `StreamTypingIndicator` +- `UnreadIndicator` in favor of `SteamUnreadIndicator` +- `UploadProgressIndicator` in favor of `StreamUploadProgressIndicator` +- `UrlAttachment` in favor of `StreamUrlAttachment` +- `UserAvatar` in favor of `StreamUserAvatar` +- `UserItem` in favor of `StreamUserItem` +- `UserListView` in favor of `StreamUserListView` +- `UserListViewTheme` in favor of `StreamUserListViewTheme` +- `UserListViewThemeData` in favor of `StreamUserListViewThemeData` +- `UserMentionTile` in favor of `StreamUserMentionTile` +- `UserMentionsOverlay` in favor of `StreamUserMentionsOverlay` +- `VideoAttachment` in favor of `StreamVideoAttachment` +- `VideoService` in favor of `StreamVideoService` +- `VideoThumbnailImage` in favor of `StreamVideoThumbnailImage` +- `VisibleFootnote` in favor of `StreamVisibleFootnote` + +## ChannelListView to StreamChannelListView + +The `ChannelListView` widget has been deprecated, and it is now recommended to use `StreamChannelListView`. + +Version 4 of the Stream Chat Flutter packages introduces a new controller called, `StreamChannelListController`. This controller manages the content for a channel list; it lets you perform tasks such as: + +- Load initial data. +- Use channel events handlers. +- Load more data using `loadMore`. +- Replace the previously loaded channels. +- Return/Create a new channel and start watching it. +- Pause and Resume all subscriptions added to this composite. + +For more information see the [`StreamChannelListView` documentation](../stream_chat_flutter/stream_channel_list_view.mdx). + +### ChannelsBloc to StreamChannelListController + +The `ChannelsBloc` widget should be replaced with `StreamChannelListController`. This controller provides all the functionality needed to query and manipulate channel data previously accessible through `ChannelsBloc`. + +For more information see the [`StreamChannelListController` documentation](../stream_chat_flutter_core/stream_channel_list_controller.mdx). + +### StreamChannelListView Examples + +Let's explore some examples of the functional differences when using the new `StreamChannelListView`. + +The **StreamChannelListController** provides various methods, such as: + +- **deleteChannel** +- **loadMore** +- **muteChannel** +- **deleteChannel** + +For a complete list with additional information, see the code documentation. + +#### Basic Use + +The following code demonstrates the old way of creating a **ChannelListPage**, that displays a list of channels: + +```dart +class ChannelListPage extends StatelessWidget { + const ChannelListPage({ + Key? key, + }) : super(key: key); + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + return Scaffold( + body: ChannelsBloc( + child: ChannelListView( + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + channelWidget: const ChannelPage(), + ), + ), + ); + } +} +``` + +In **v4** this can now be achieved with the following: + +```dart +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), + ), + ), + ); +} +``` + +As you can see, the **ChannelsBloc** has been replaced with a **StreamChannelListController**, where the **filter**, **limit**, and **sort** arguments can be set. The above code also demonstrates how to refresh the channel list by calling `_controller.refresh()`. + +## MessageSearchListView to StreamMessageSearchListView + +The `MessageSearchListView` widget has been deprecated, and it is now recommended to use `StreamMessageSearchListView`. + +Version 4 of the Stream Chat Flutter packages introduces a new controller called, `StreamMessageSearchListController`. This controller manages the content when searching for a message; it lets you perform tasks such as: + +- Load initial data. +- Set filters and search terms. +- Load more data using `loadMore`. +- Refresh data. + +For more information see the [`StreamMessageSearchListView` documentation](../stream_chat_flutter/stream_message_search_list_view.mdx). + +### MessageSearchBloc to StreamMessageSearchListController + +The `MessageSearchBloc` widget should be replaced with a `StreamMessageSearchListController`. This controller provides all the functionality needed to query and manipulate message search data previously accessible through `MessageSearchBloc`. + +For more information see the [`StreamMessageSearchListController` documentation](../stream_chat_flutter_core/stream_message_search_list_controller.mdx). + +### StreamMessageSearchListView Example + +The following code demonstrates the old way of searching for messages: + +```dart +class SearchExample extends StatelessWidget { + const SearchExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return MessageSearchBloc( + child: MessageSearchListView( + showErrorTile: true, + messageQuery: 'message query', + filters: Filter.in_('members', const ['user-id']), + sortOptions: const [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + pullToRefresh: false, + limit: 30, + emptyBuilder: (context) => const Text('Nothing to show'), + itemBuilder: (context, messageResponse) { + /// Return widget + } + onItemTap: (messageResponse) { + /// Handle on tap + } + ), + ); + } +} +``` + +In **v4**, this can now be achieved with the following: + +```dart +class SearchExample extends StatefulWidget { + const SearchExample({ + Key? key, + }) : super(key: key); + + @override + State createState() => _SearchExampleState(); +} + +class _SearchExampleState extends State { + late final StreamMessageSearchListController _messageSearchListController = + StreamMessageSearchListController( + client: StreamChat.of(context).client, + filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]), + limit: 5, + searchQuery: '', + sort: [ + const SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + ); + + search() { + _messageSearchListController.searchQuery = 'search-value'; + _messageSearchListController.doInitialLoad(); + } + + @override + dispose() { + _messageSearchListController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return StreamMessageSearchListView( + controller: _messageSearchListController, + emptyBuilder: (context) => const Text('Nothing to show'), + itemBuilder: ( + context, + messageResponses, + index, + defaultWidget, + ) { + return defaultWidget.copyWith(); // modify default widget + }); + } +} +``` + +## UserListView to StreamUserListView + +The `UserListView` widget has been deprecated, and it is now recommended to use `StreamUserListView`. + +Version 4 of the Stream Chat Flutter packages introduces a new controller called, `StreamUserListController`. This controller manages the content when retrieving Stream users; it let's you perform tasks, such as: + +- Load data. +- Set filters. +- Refresh data. + +For more information see the [`StreamUserListView` documentation](../stream_chat_flutter/stream_user_list_view.mdx). + +### UsersBloc to StreamUserListController + +The `UsersBloc` widget should be replaced with a `StreamUserListController`. This controller provides all the functionality needed to query and manipulate user data previously accessible through `UsersBloc`. + +For more information see the [`StreamUserListController` documentation](../stream_chat_flutter_core/stream_user_list_controller.mdx). + +### StreamUserListView Example + +The following code demonstrates the old way of displaying all users: + +```dart +class UsersExample extends StatelessWidget { + const UsersExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return UsersBloc( + child: UserListView( + groupAlphabetically: true, + onUserTap: (user, _) { + /// Handle on tap + }, + limit: 25, + filter: Filter.and([ + Filter.autoComplete('name', 'some-name'), + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]), + sort: const [ + SortOption( + 'name', + direction: 1, + ), + ], + ), + ); + } +} +``` + +In **v4**, this can now be achieved with the following: + +```dart +class UsersExample extends StatefulWidget { + const UsersExample({ + Key? key, + }) : super(key: key); + + @override + State createState() => _UsersExampleState(); +} + +class _UsersExampleState extends State { + late final userListController = StreamUserListController( + client: StreamChat.of(context).client, + limit: 25, + filter: Filter.and([ + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]), + sort: [ + const SortOption( + 'name', + direction: 1, + ), + ], + ); + + void _load() { + userListController.filter = Filter.and([ + Filter.autoComplete('name', 'some-name'), + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]); + userListController.doInitialLoad(); + } + + @override + dispose() { + userListController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return StreamUserListView( + controller: userListController, + onUserTap: (user) { + /// Handle on tap + }, + emptyBuilder: (context) => const Text('Nothing to show'), + itemBuilder: ( + context, + users, + index, + defaultWidget, + ) { + return defaultWidget.copyWith(); // modify default widget + }, + ); + } +} +``` + +## MessageInput to StreamMessageInput + +The `MessageInput` widget has been deprecated, and it is now recommended to use `StreamMessageInput`. + +Version 4 of the Stream Chat Flutter packages introduces a new controller called, `MessageInputController`. This controller maintains the state of the message input and exposes various methods to allow you to customize and manipulate the underlying **Message** value. + +Creating a separate controller allows easier control over the message input content by moving logic out of the deprecated `MessageInput` and into the controller. This controller can then be created, managed, and exposed in whatever way you like. + +The widget is also separated into smaller components: `StreamCountDownButton`, `StreamAttachmentPicker`, etc. + +> ❗The `MessageInputController` is exposed by the **stream_chat_flutter_core** package. This allows you to use the controller even if you're not using the UI components. + +As a result of this extra control, it is no longer needed for the new `StreamMessageInput` widget to expose these `MessageInput` arguments: + +- `parentMessage`: parent message in case of a thread +- `editMessage`: message to edit +- `initialMessage`: message to start with +- `quotedMessage`: message to quote/reply +- `onQuotedMessageCleared`: callback for clearing quoted message +- `textEditingController`: the text controller of the text field + +The following arguments are newly introduced to the `StreamMessageInput`, and are not available on the old `MessageInput`: + +- `messageInputController`: the controller for the message input +- `attachmentsPickerBuilder`: builder for bottom sheet when attachment picker is opened +- `sendButtonBuilder`: builder for creating send button +- `validator`: a callback function that validates the message +- `restorationId`: restoration ID to save and restore the state of the MessageInput +- `enableSafeArea`: wraps the **StreamMessageInput** widget with a **SafeArea** widget +- `elevation`: elevation of the **StreamMessageInput** widget +- `shadow`: **Shadow** for the **StreamMessageInput** widget + +For more information see the [`StreamMessageInput` documentation](../stream_chat_flutter_core/stream_message_input_controller.mdx). + +### StreamMessageInput Examples + +Let's explore some examples of the functional differences when using the new `StreamMessageInput`. + +#### Basic Use + +Unless you want to programmatically manipulate the value of the message input, then there is no difference in how you would use the message input widget. + +The following code demonstrates the old way of creating a **ChannelPage** widget that displays a chat screen: + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); + } +} +``` + +In **v4** this is the same, the only difference being that all the Stream widgets are now prefixed with **Stream**. For example, **MessageListView** becomes **StreamMessageListView**, and so forth. + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); +} +``` + +However, you can optionally pass in a **MessageInputController** in the **StreamMessageInput**, which gives extra control over the message input value. + +#### Thread Page + +The following code demonstrates the old way of creating a thread page: + +```dart +class ThreadPage extends StatelessWidget { + const ThreadPage({ + Key? key, + this.parent, + }) : super(key: key); + + final Message? parent; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ThreadHeader( + parent: parent!, + ), + body: Column( + children: [ + Expanded( + child: MessageListView( + parentMessage: parent, + ), + ), + MessageInput( + parentMessage: parent, + ), + ], + ), + ); + } +} +``` + +In **v4** the only difference is the **Stream** prefix and the way that the parent message is passed to the message input: + +```dart +class ThreadPage extends StatelessWidget { + const ThreadPage({ + Key? key, + this.parent, + }) : super(key: key); + + final Message? parent; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: StreamThreadHeader( + parent: parent!, + ), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + parentMessage: parent, + ), + ), + StreamMessageInput( + messageInputController: MessageInputController( + message: Message(parentId: parent!.id), + ), + ), + ], + ), + ); + } +} +``` + +To send a thread message, you need to specify the message's parent ID for which you're creating a thread. + +#### Reply/Quote Message + +The following code demonstrates the old way of replying to a message: + +```dart +... + +void _reply(Message message) { + setState(() => _quotedMessage = message); +} + +... + +MessageInput + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + }, +), +``` + +To reply to a message in **v4**: + +```dart +... + +void _reply(Message message) { + _messageInputController.quotedMessage = message; +} + +... + +StreamMessageInput( + messageInputController: _messageInputController, +), +``` + +The controller makes it much simpler to dynamically modify the message input. + +## Stream Chat Flutter Core + +Various changes have been made to the Core package, most notably, the indroduction of all of the controllers mentioned above: + +These controllers replace the business logic implementations (Bloc). Please note that this is not related to the well-known Flutter Bloc package, but instead refers to the naming we used for our business logic components. + +In this version we're introducing controllers in place of their bloc counterparts: +**StreamChannelListController** in favor of **ChannelsBloc** +**StreamMessageSearchListController** in favor of **MessageSearchBloc** +**StreamUserListController** in favor of **UsersBloc** + +The Bloc components are deprecated in v4.0.0 but can still be used. They will be removed in the next major release (v5.0.0). + +Additionally, we also now have the **StreamMessageInputController**, as discussed above. This can be used outside of our UI package as well. + +Finally, the following Core builders are also deprecated as their functionality can be replaced using their controller counterparts: + +- ChannelListCore +- MessageSearchListCore +- UserListCore diff --git a/docusaurus/docs/Flutter/guides/slidable_channel_list_preview.mdx b/docusaurus/docs/Flutter/guides/slidable_channel_list_preview.mdx new file mode 100644 index 000000000..9d57e7092 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/slidable_channel_list_preview.mdx @@ -0,0 +1,212 @@ +--- +id: slidable_channel_list_preview +sidebar_position: 14 +title: Slidable Channel List Preview +--- + +Slidable Channel List Preview + +### Introduction + +The default slidable behavior within the channel list has been removed in v4 of the Stream Chat Flutter SDK. +This guide will show you how you can easily add this functionality yourself. + +Please see our [full v4 migration guide](migration_guide_4_0.mdx) if you're migrating from an earlier version of the Stream Chat Flutter SDK. + +![Slidable demo](../assets/slidable_demo.jpg) + +### Prerequisites + +This guide assumes you are familiar with the Stream Chat SDK. +If you're new to Stream Chat Flutter, we recommend looking at our [getting started tutorial](https://getstream.io/chat/flutter/tutorial/). + +**Dependencies:** + +```dart +dependencies: + flutter: + sdk: flutter + stream_chat_flutter: ^4.0.0 + flutter_slidable: ^1.2.0 +``` + +⚠️ Note: The examples shown in this guide use the above packages and versions. + +### Example Code - Custom Stream Channel Item Builder + +In this example, you are doing a few important things in the ChannelListPage widget. You're: + +- Using the **flutter_slidable** package to easily add slide functionality. +- Passing in the `itemBuilder` argument for the **StreamChannelListView** widget. This gives access to the current **BuildContext**, **Channel**, and **StreamChannelListTile**, and allows you to create, or customize, the stream channel list tiles. +- Returning a Slidable widget with two CustomSlidableAction widgets - to delete a channel and show more options. These widgets come from the flutter_slidable package. +- Adding `onPressed` behaviour to call `showConfirmationDialog` and `showChannelInfoModalBottomSheet`. These methods come from the **stream_chat_flutter** package. They have a few different on-tap callbacks you can supply, for example, `onViewInfoTap`. Alternatively, you can create custom dialogs from scratch. +- Using the **StreamChannelListController** to perform actions, such as, `deleteChannel`. + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() async { + final client = StreamChatClient( + 's2dxdhpxd94g', + ); + + await client.connectUser( + User(id: 'super-band-9'), + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', + ); + + runApp( + MyApp( + client: client, + ), + ); +} + +class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) => StreamChat( + client: client, + child: child, + ), + home: ChannelListPage( + client: client, + ), + ); + } +} + +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: SlidableAutoCloseBehavior( + child: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + itemBuilder: (context, channel, tile) { + final chatTheme = StreamChatTheme.of(context); + final backgroundColor = chatTheme.colorTheme.inputBg; + final canDeleteChannel = channel.ownCapabilities + .contains(PermissionType.deleteChannel); + return Slidable( + groupTag: 'channels-actions', + endActionPane: ActionPane( + extentRatio: canDeleteChannel ? 0.40 : 0.20, + motion: const BehindMotion(), + children: [ + CustomSlidableAction( + onPressed: (_) { + showChannelInfoModalBottomSheet( + context: context, + channel: channel, + onViewInfoTap: () { + Navigator.pop(context); + // Navigate to info screen + }, + ); + }, + backgroundColor: backgroundColor, + child: const Icon(Icons.more_horiz), + ), + if (canDeleteChannel) + CustomSlidableAction( + backgroundColor: backgroundColor, + child: StreamSvgIcon.delete( + color: chatTheme.colorTheme.accentError, + ), + onPressed: (_) async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + question: + 'Are you sure you want to delete this conversation?', + okText: 'Delete', + cancelText: 'Cancel', + icon: StreamSvgIcon.delete( + color: chatTheme.colorTheme.accentError, + ), + ); + if (res == true) { + await _controller.deleteChannel(channel); + } + }, + ), + ], + ), + child: tile, + ); + }, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), + ), + ), + ), + ); +} + +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); +} +``` + +The above is the complete sample, and all you need for a basic implementation. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_grid_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_grid_view.mdx new file mode 100644 index 000000000..16bad7dc1 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_grid_view.mdx @@ -0,0 +1,74 @@ +--- +id: stream_channel_grid_view +sidebar_position: 4 +title: StreamChannelGridView +--- + +A Widget For Displaying A List Of Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChannelGridView-class.html) + +### Background + +The `StreamChannelGridView` widget allows displaying a list of channels to a user in a `GridView`. + +:::note +Make sure to check the [StreamChannelListView](./stream_channel_list_view.mdx) documentation to know how to show results in a `ListView`. +::: + +### Basic Example + +Here is a basic example of the `StreamChannelGridView` widget. It consists of the main widget itself, a `StreamChannelListController` to control the list of channels and a callback to handle the tap of a channel. + +```dart +class ChannelGridPage extends StatefulWidget { + const ChannelGridPage({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + State createState() => _ChannelGridPageState(); +} + +class _ChannelGridPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelGridView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), + ), + ), + ); +} +``` + +This example by default displays the channels that a user is a part of. Now let's look at customizing +the widget. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_header.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_header.mdx new file mode 100644 index 000000000..bc3c8aaf7 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_header.mdx @@ -0,0 +1,84 @@ +--- +id: stream_channel_header +sidebar_position: 10 +title: StreamChannelHeader +--- + +A Widget To Display Common Channel Details + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChannelHeader-class.html) + +![](../assets/channel_header.png) + +### Background + +When a user opens a channel, it is helpful to provide context of which channel they are in. This may +be in the form of a channel name or the users in the channel. Along with that, there also needs to be +a way for the user to look at more details of the channel (media, pinned messages, actions, etc.) and +preferably also a way to navigate back to where they came from. + +To encapsulate all of this functionality into one widget, the Flutter SDK contains a `StreamChannelHeader` +widget which provides these out of the box. + +### Basic Example + +Let's just add a `StreamChannelHeader` to a page with a `StreamMessageListView` and a `StreamMessageInput` to display +and send messages. + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: StreaChannelHeader(), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, + ), + ), + StreamMessageInput(), + ], + ), + ); + } +} +``` + +### Customizing Parts Of The Header + +The header works like a `ListTile` widget. + +Use the `title`, `subtitle`, `leading`, or `actions` parameters to substitute the widgets for your own. + +```dart +//... +StreamChannelHeader( + title: Text('My Custom Name'), +), +``` + +![](../assets/channel_header_custom_title.png) + +### Showing Connection State + +The `StreamChannelHeader` can also display connection state below the tile which shows the user if they +are connected or offline, etc. on connection events. + +To enable this, use the `showConnectionStateTile` property. + +```dart +//... +StreamChannelHeader( + showConnectionStateTile: true, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_list_header.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_list_header.mdx new file mode 100644 index 000000000..4c8fbc632 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_list_header.mdx @@ -0,0 +1,118 @@ +--- +id: stream_channel_list_header +sidebar_position: 9 +title: StreamChannelListHeader +--- + +A Header Widget For A List Of Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChannelListHeader-class.html) + +![](../assets/channel_list_header.png) + +### Background + +A common pattern for most messaging apps is to show a list of Channels (chats) on the first screen +and navigate to an individual one on being clicked. On this first page where the list of channels are +displayed, it is usual to have functionality such as adding a new chat, display the user logged in, etc. + +To encapsulate all of this functionality into one widget, the Flutter SDK contains a `StreamChannelListHeader` +widget which provides these out of the box. + +### Basic Example + +This is a basic example of a page which has a `StreamChannelListView` and a `StreamChannelListHeader` to recreate a +common Channels Page. + +```dart +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: StreamChannelListHeader(), + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), + ), + ), + ); +} +``` + +### Customizing Parts Of The Header + +The header works like a `ListTile` widget. + +Use the `titleBuilder`, `subtitle`, `leading`, or `actions` parameters to substitute the widgets for your own. + +```dart +//... +StreamChannelListHeader( + subtitle: Text('My Custom Subtitle'), +), +``` + +![](../assets/channel_list_header_custom_subtitle.png) + +The `titleBuilder` param helps you build different titles depending on the connection state: + +```dart +//... +StreamChannelListHeader( + titleBuilder: (context, status, client) { + switch(status) { + /// Return your title widget + } + }, +), +``` + +### Showing Connection State + +The `StreamChannelListHeader` can also display connection state below the tile which shows the user if they +are connected or offline, etc. on connection events. + +To enable this, use the `showConnectionStateTile` property. + +```dart +//... +StreamChannelListHeader( + showConnectionStateTile: true, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_list_view.mdx new file mode 100644 index 000000000..dbb3f2667 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_channel_list_view.mdx @@ -0,0 +1,107 @@ +--- +id: stream_channel_list_view +sidebar_position: 4 +title: StreamChannelListView +--- + +A Widget For Displaying A List Of Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChannelListView-class.html) + +![](../assets/channel_list_view.png) + +### Background + +Channels are fundamental elements of Stream Chat and constitute shared spaces which allow users to +message each other. + +1:1 conversations and groups are both examples of channels, albeit with some (distinct/non-distinct) +differences. Displaying the list of channels that a user is a part of is a pattern present in most messaging apps. + +The `StreamChannelListView` widget allows displaying a list of channels to a user. By default, this is NOT +ONLY the channels that the user is a part of. This section goes into setting up and using a `StreamChannelListView` +widget. + +:::note +Make sure to check the [StreamChannelListController](./stream_channel_list_controller.mdx) documentation for more information on how to use the controller to manipulate the `StreamChannelListView`. +::: + +### Basic Example + +Here is a basic example of the `StreamChannelListView` widget. It consists of the main widget itself, a `StreamChannelListController` to control the list of channels and a callback to handle the tap of a channel. + +```dart +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), + ), + ), + ); +} +``` + +This example by default displays the channels that a user is a part of. Now let's look at customizing +the widget. + +### Customizing the Channel Preview + +A common aspect of the widget needed to be tweaked according to each app is the Channel Preview (the +Channel tile in the list). To do this, we use the `itemBuilder` parameter like this: + +```dart +StreamChannelListView( + ... + itemBuilder: (context, channels, index, defaultTile) { + return ListTile( + tileColor: Colors.amberAccent, + title: Center( + child: StreamChannelName(channel: channels[index]), + ), + ); + }, +), +``` + +Which gives you a new Channel preview in the list: + +![](../assets/channel_preview.png) diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_input.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_input.mdx new file mode 100644 index 000000000..b60696aea --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_input.mdx @@ -0,0 +1,113 @@ +--- +id: stream_message_input +sidebar_position: 6 +title: StreamMessageInput +--- + +A Widget Dealing With Everything Related To Sending A Message + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamMessageInput-class.html) + +![](../assets/message_input.png) + +### Background + +In Stream Chat, we can send messages in a channel. However, sending a message isn't as simple as adding +a `TextField` and logic for sending a message. It involves additional processes like addition of media, +quoting a message, adding a custom command like a GIF board, and much more. Moreover, most apps also +need to customize the input to match their theme, overall color and structure pattern, etc. + +To do this, we created a `StreamMessageInput` widget which abstracts all expected functionality a modern input +needs - and allows you to use it out of the box. + +### Basic Example + +A `StreamChannel` is required above the widget tree in which the `StreamMessageInput` is rendered since the channel is +where the messages sent actually go. Let's look at a common example of how we could use the `StreamMessageInput`: + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: StreaChannelHeader(), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, + ), + ), + StreamMessageInput(), + ], + ), + ); + } +} +``` + +It is common to put this widget in the same page of a `StreamMessageListView` as the bottom widget. + +:::note +Make sure to check the [StreamMessageInputController](./stream_message_input_controller.mdx) documentation for more information on how to use the controller to manipulate the `StreamMessageInput`. +::: + +### Adding Custom Actions + +By default, the `StreamMessageInput` has two actions: one for attachments and one for commands like Giphy. +To add your own action, we use the `actions` parameter like this: + +```dart +StreamMessageInput( + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + // Do something here + }, + ), + ], +), +``` + +This will add on your action to the existing ones. + +### Disable Attachments + +To disable attachments being added to the message, set the `disableAttachments` parameter to true. + +```dart +StreamMessageInput( + disableAttachments: true, +), +``` + +### Changing Position Of MessageInput Components + +You can also change the position of the TextField, actions and 'send' button relative to each other. + +To do this, use the `actionsLocation` or `sendButtonLocation` parameters which help you decide the location +of the buttons in the input. + +For example, if we want the actions on the right and the send button inside the TextField, we can do: + +```dart +StreamMessageInput( + sendButtonLocation: SendButtonLocation.inside, + actionsLocation: ActionsLocation.right, +), +``` + +![](../assets/message_input_change_position.png) diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_list_view.mdx new file mode 100644 index 000000000..19a041a01 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_list_view.mdx @@ -0,0 +1,92 @@ +--- +id: stream_message_list_view +sidebar_position: 5 +title: StreamMessageListView +--- + +A Widget For Displaying A List Of Messages + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamMessageListView-class.html) + +![](../assets/message_list_view.png) + +### Background + +Every channel can contain a list of messages sent by users inside it. The `StreamMessageListView` widget +displays the list of messages inside a particular channel along with possible attachments and +other message attributes (if the message is pinned for example). This sets it apart from the `StreamMessageSearchListView` +which may not contain messages only from a single channel and is used to search for messages across +many. + +### Basic Example + +The `StreamMessageListView` shows the list of messages of the current channel. It has inbuilt support for +common messaging functionality: displaying and editing messages, adding / modifying reactions, support +for quoting messages, pinning messages, and more. + +An example of how you can use the `StreamMessageListView` is: + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: StreamChannelHeader(), + body: Column( + children: [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); + } +} +``` + +### Enable Threads + +Threads are made of a parent message and replies linked to it. To enable threading, the SDK requires you +to supply a `threadBuilder` which will supply the page when the thread is clicked. + +```dart +StreamMessageListView( + threadBuilder: (_, parentMessage) { + return ThreadPage( + parent: parentMessage, + ); + }, +), +``` + +![](../assets/message_list_view_threads.png) + +The `StreamMessageListView` itself can render the thread by supplying the `parentMessage` parameter. + +```dart +StreamMessageListView( + parentMessage: parent, +), +``` + +### Building Custom Messages + +You can also supply your own implementation for displaying messages using the `messageBuilder` parameter. + +:::note +To customize the existing implementation, look at the `StreamMessageWidget` documentation instead. +::: + +```dart +StreamMessageListView( + messageBuilder: (context, details, messageList, defaultImpl) { + // Your implementation of the message here + // E.g: return Text(details.message.text ?? ''); + }, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_search_grid_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_search_grid_view.mdx new file mode 100644 index 000000000..d20a62fc7 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_search_grid_view.mdx @@ -0,0 +1,55 @@ +--- +id: stream_message_search_grid_view +sidebar_position: 8 +title: StreamMessageSearchGridView +--- + +A Widget To Search For Messages Across Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamMessageSearchGridView-class.html) + +### Background + +The `StreamMessageSearchGridView` widget allows displaying a list of searched messages in a `GridView`. + +:::note +Make sure to check the [StreamMessageSearchListView](./stream_message_search_list_view.mdx) documentation to know how to show results in a `ListView`. +::: + +### Basic Example + +```dart +class StreamMessageSearchPage extends StatefulWidget { + const StreamMessageSearchPage({ + Key? key, + required this.client, + }) : super(key: key);` + + final StreamChatClient client; + + @override + State createState() => _StreamMessageSearchState(); +} + +class _StreamMessageSearchState extends State { + late final _controller = StreamMessageSearchListController( + client: widget.client, + limit: 20, + filters: Filter.in_('members', [StreamChat.of(context).user!.id],), + searchQuery: 'your query here', + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamMessageSearchGridView( + controller: _controller, + ), + ); +} +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_search_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_search_list_view.mdx new file mode 100644 index 000000000..cf27facfd --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_search_list_view.mdx @@ -0,0 +1,74 @@ +--- +id: stream_message_search_list_view +sidebar_position: 8 +title: StreamMessageSearchListView +--- + +A Widget To Search For Messages Across Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamMessageSearchListView-class.html) + +![](../assets/message_search_list_view.png) + +### Background + +Users in Stream Chat can have several channels and it can get hard to remember which channel has the +message they are searching for. As such, there needs to be a way to search for a message across multiple +channels. This is where `StreamMessageSearchListView` comes in. + +:::note +Make sure to check the [StreamMessageSearchListController](./stream_message_search_list_controller.mdx) documentation for more information on how to use the controller to manipulate the `StreamMessageSearchListView`. +::: + +### Basic Example + +While the `StreamMessageListView` is tied to a certain `StreamChannel`, a `StreamMessageSearchListView` is not. + +```dart +class StreamMessageSearchPage extends StatefulWidget { + const StreamMessageSearchPage({ + Key? key, + required this.client, + }) : super(key: key);` + + final StreamChatClient client; + + @override + State createState() => _StreamMessageSearchState(); +} + +class _StreamMessageSearchState extends State { + late final _controller = StreamMessageSearchListController( + client: widget.client, + limit: 20, + filters: Filter.in_('members', [StreamChat.of(context).user!.id],), + searchQuery: 'your query here', + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamMessageSearchListView( + controller: _controller, + ), + ); +} +``` + +### Customize The Result Tiles + +You can use your own widget for the result items using the `itemBuilder` parameter. + +```dart +StreamMessageSearchListView( + // ... + itemBuilder: (context, responses, index, defaultWidget) { + return Text(responses[index].message.text); + }, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_widget.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_widget.mdx new file mode 100644 index 000000000..c7c084ab9 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_message_widget.mdx @@ -0,0 +1,97 @@ +--- +id: stream_message_widget +sidebar_position: 11 +title: StreamMessageWidget +--- + +A Widget For Displaying Messages And Attachments + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamMessageWidget-class.html) + +### Background + +There are several things that need to be displayed with text in a message in a modern messaging app: +attachments, highlights if the message is pinned, user avatars of the sender, etc. + +To encapsulate all of this functionality into one widget, the Flutter SDK contains a `StreamMessageWidget` +widget which provides these out of the box. + +### Basic Example (Modifying `StreamMessageWidget` in `StreamMessageListView`) + +Primarily, the `StreamMessageWidget` is used in the `StreamMessageListView`. To customize only a few properties +of the `StreamMessageWidget` without supplying all other properties, the `messageBuilder` builder supplies +a default implementation of the widget for us to modify. + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: StreamMessageListView( + messageBuilder: (context, details, messageList, defaultMessageWidget) { + return defaultMessageWidget.copyWith( + showThreadReplyIndicator: false, + ); + }, + ), + ); + } +} +``` + +### Building A Custom Attachment + +When a custom attachment type (location, audio, etc.) is sent, the MessageWidget also needs to know +how to build it. For this purpose, we can use the `customAttachmentBuilders` parameter. + +As an example, if a message has a attachment type 'location', we do: + +```dart +StreamMessageWidget( + //... + customAttachmentBuilders: { + 'location': (context, message, attachments) { + var attachmentWidget = Image.network( + _buildMapAttachment( + attachments[0].extraData['latitude'], + attachments[0].extraData['longitude'], + ), + ); + + return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); + } + }, +) +``` + +You can also override the builder for existing attachment types like `image` and `video`. + +### Show User Avatar For Messages + +You can decide to show, hide, or remove user avatars of the sender of the message. To do this, set +the `showUserAvatar` property like this: + +```dart +StreamMessageWidget( + //... + showUserAvatar = DisplayWidget.show, +) +``` + +### Reverse the message + +In most cases, `StreamMessageWidget` needs to be a different orientation depending upon if the sender is the +user or someone else. + +For this, we use the `reverse` parameter to change the orientation of the message: + +```dart +StreamMessageWidget( + //... + reverse = true, +) +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_user_grid_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_user_grid_view.mdx new file mode 100644 index 000000000..d2fb953d5 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_user_grid_view.mdx @@ -0,0 +1,74 @@ +--- +id: stream_user_grid_view +sidebar_position: 7 +title: StreamUserGridView +--- + +A Widget For Displaying And Selecting Users + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamUserGridView-class.html) + +### Background + +The `StreamUserGridView` widget allows displaying a list of users in a `GridView`. + +:::note +Make sure to check the [StreamUserListView](./stream_user_list_view.mdx) documentation to know how to show results in a `ListView`. +::: + +### Basic Example + +```dart +class UserGridPage extends StatefulWidget { + const UserGridPage({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + State createState() => _UserGridPageState(); +} + +class _UserGridPageState extends State { + late final _controller = StreamUserListController( + client: widget.client, + limit: 25, + filter: Filter.and([ + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]), + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamUserGridView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), + ), + ), + ); +} +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/stream_user_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/stream_user_list_view.mdx new file mode 100644 index 000000000..1a8cbbf8a --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter/stream_user_list_view.mdx @@ -0,0 +1,91 @@ +--- +id: stream_user_list_view +sidebar_position: 7 +title: StreamUserListView +--- + +A Widget For Displaying And Selecting Users + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamUserListView-class.html) + +![](../assets/user_list_view.png) + +### Background + +A list of users is required for many different purposes: showing a list of users in a Channel, +selecting users to add in a channel, etc. The `StreamUserListView` displays a list +of users. + +:::note +Make sure to check the [StreamUserListController](./stream_user_list_controller.mdx) documentation for more information on how to use the controller to manipulate the `StreamUserListView`. +::: + +### Basic Example + +```dart +class UserListPage extends StatefulWidget { + const UserListPage({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + State createState() => _UserListPageState(); +} + +class _UserListPageState extends State { + late final _controller = StreamUserListController( + client: widget.client, + limit: 25, + filter: Filter.and([ + Filter.notEqual('id', StreamChat.of(context).currentUser!.id), + ]), + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), + ), + ), + ); +} +``` + +### Customize The User Items + +You can use your own widget for the user items using the `itemBuilder` parameter. + +```dart +StreamUsersListView( + // ... + itemBuilder: (context, users, index, defaultWidget) { + return Text(user[index].name); + }, +), +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/introduction.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/introduction.mdx index 3c6a70f2b..5cb8f0db1 100644 --- a/docusaurus/docs/Flutter/stream_chat_flutter_core/introduction.mdx +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/introduction.mdx @@ -24,57 +24,14 @@ Also, it has very few dependencies. We will now explore the components of this intermediary package and understand how it helps you build the experience you want your users to have. -The package primarily contains three types of classes: - -* Business Logic Components -* Core Components -* Core Controllers - -### Business Logic Components - -These components allow you to have the maximum and lower-level control of the queries being executed. - -In BLoCs, the basic functionalities - such as queries for messages, channels or queries - are bundled up -and passed along down the tree. Using a BLoC allows you to either create your own way to fetch and -build UIs or use an inbuilt Core widget to do the work such as queries, pagination, etc for you. - -The BLoCs we provide are: - -* ChannelsBloc -* MessageSearchBloc -* UsersBloc - -### Core Components - -Core components usually are an easy way to fetch data associated with Stream Chat. -Core components use functions exposed by the respective BLoCs (for example the ChannelListCore uses the ChannelsBloc) -and use the respective controllers for various operations. Unlike heavier components from the UI -package, core components are decoupled from UI and they expose builders instead to help you build -a fully custom interface. - -Data fetching can be controlled with the controllers of the respective core components. - -* ChannelListCore (Fetch a list of channels) -* MessageListCore (Fetch a list of messages from a channel) -* MessageSearchListCore (Fetch a list of search messages) -* UserListCore (Fetch a list of users) -* StreamChatCore (This is different from the other core components - it is a version of StreamChat decoupled from theme and initialisations.) - -### Core Controllers - -Core Controllers are supplied to respective CoreList widgets which allows reloading and pagination of data whenever needed. - -Unlike the UI package, the Core package allows a fully custom user interface built with the data. This -in turn provides a few challenges: we do not know implicitly when to paginate your list or reload your data. - -While this is handled out of the box in the UI package since the List implementation is inbuilt, a controller -needs to be used in the core package notifying the core components to reload or paginate the data existing -currently. For this, each core component has a respective controller which you can use to call the -specific function (reload / paginate) whenever such an event is triggered through / needed in your UI. - -* ChannelListController -* MessageListController -* MessageSearchListController -* ChannelListController +The package primarily contains a bunch of controller classes. +Controllers are used to handle the business logic of the chat. You can use them together with our UI widgets, or you can even use them to build your own UI. + +* StreamChannelListController +* StreamUserListController +* StreamMessageSearchListController +* StreamMessageInputController +* LazyLoadScrollView +* PagedValueListenableBuilder This section goes into the individual core package widgets and their functional use. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/lazy_load_scroll_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/lazy_load_scroll_view.mdx new file mode 100644 index 000000000..457f15c71 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/lazy_load_scroll_view.mdx @@ -0,0 +1,41 @@ +--- +id: lazy_load_scroll_view +sidebar_position: 8 +title: LazyLoadScrollView +--- + +A Widget For Building A Paginated List + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/LazyLoadScrollView-class.html) + +### Background + +The `LazyLoadScrollView` is a widget that helps you build a paginated list. +It provides callbacks to notify you when the list has been scrolled to the bottom and when the list has been scrolled to the top and other necessary callbacks. + +#### Callbacks + +* onStartOfPage: called when the list has been scrolled to the top of the page. + +* onEndOfPage: called when the list has been scrolled to the bottom of the page. + +* onPageScrollStart: called when the scroll of the list starts. + +* onPageScrollEnd: called when the scroll of the list ends. + +* onInBetweenOfPage: called when the list is not either at the top nor at the bottom of the page. + +### Basic Example + +Building a paginated list is a very common task. Here is an example of how to use the `LazyLoadScrollView` to build a simple list with pagination. + +```dart +LazyLoadScrollView( + onEndOfPage: _paginateData, + /// The child could be any widget which dispatches [ScrollNotification]s. + /// For example [ListView], [GridView] or [CustomScrollView]. + child: ListView.builder( + itemBuilder: ((context, index) => _buildListTile), + ), +) +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_list_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_list_core.mdx index 5fd5877ad..57f06b2a9 100644 --- a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_list_core.mdx +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_list_core.mdx @@ -6,6 +6,8 @@ title: MessageListCore A Widget For Building A List Of Messages +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/MessageListCore-class.html) + ### Background The UI SDK of Stream Chat supplies a `MessageListView` class that builds a list of channels fetching diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/paged_value_listenable_builder.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/paged_value_listenable_builder.mdx new file mode 100644 index 000000000..b527f7a01 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/paged_value_listenable_builder.mdx @@ -0,0 +1,88 @@ +--- +id: paged_value_listenable_builder +sidebar_position: 9 +title: PagedValueListenableBuilder +--- + +A Widget Whose Content Stays Synced With A `ValueNotifier` Of Type `PagedValue`. + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/PagedValueListenableBuilder-class.html) + +### Background + +Given a `PagedValueNotifier` implementation and a [builder] which builds widgets from +concrete values of `PagedValue`, this class will automatically register itself as a +listener of the [PagedValueNotifier] and call the [builder] with updated values +when the value changes. + +### Basic Example + +```dart +class UserNameValueNotifier extends PagedValueNotifier { + UserNameValueNotifier() : super(const PagedValue.loading()); + + @override + Future doInitialLoad() async { + // Imitating network delay + await Future.delayed(const Duration(seconds: 1)); + value = const PagedValue( + items: ['Sahil', 'Salvatore', 'Reuben'], + + /// Passing the key to load the next page + nextPageKey: nextPageKey, + ); + } + + @override + Future loadMore(int nextPageKey) async { + // Imitating network delay + await Future.delayed(const Duration(seconds: 1)); + final previousItems = value.asSuccess.items; + final newItems = previousItems + ['Deven', 'Sacha', 'Gordon']; + value = PagedValue( + items: newItems, + // Passing nextPageKey as null to indicate + // that there are no more items. + nextPageKey: null, + ); + } +} + +class _MyHomePageState extends State { + final pagedValueNotifier = UserNameValueNotifier(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: PagedValueListenableBuilder( + builder: (context, value, child) { + // This builder will only get called when the _counter + // is updated. + return value.when( + (userNames, nextPageKey, error) => Column( + children: [ + const Text('Usernames:'), + Expanded( + child: ListView( + children: userNames.map((it) => Text(it)).toList(), + ), + ), + if (nextPageKey != null) + TextButton( + child: const Text('Load more'), + onPressed: () => pagedValueNotifier.loadMore(nextPageKey), + ), + ], + ), + loading: () => CircularProgressIndicator(), + error: (e) => Text('Error: $e'), + ); + }, + valueListenable: pagedValueNotifier, + ), + ), + ); + } +} +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_channel_list_controller.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_channel_list_controller.mdx new file mode 100644 index 000000000..c37eeedd4 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_channel_list_controller.mdx @@ -0,0 +1,151 @@ +--- +id: stream_channel_list_controller +sidebar_position: 4 +title: StreamChannelListController +--- + +A Widget For Controlling A List Of Channels + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamChannelListController-class.html) + +### Background + +The `StreamChannelListController` is a controller class that allows you to control a list of channels. +`StreamChannelListController` is a required parameter of the `StreamChannelListView` widget. +Check the [`StreamChannelListView` documentation](../stream_chat_flutter/stream_channel_list_view.mdx) to read more about that. + +The `StreamChannelListController` also listens for various events and manipulates the current list of channels accordingly. +Passing a `StreamChannelListEventHandler` to the `StreamChannelListController` will allow you to customize this behaviour. + +### Basic Example + +Building a custom channel list is a very common task. Here is an example of how to use the `StreamChannelListController` to build a simple list with pagination. + +First of all we should create an instance of the `StreamChannelListController` and provide it with the `StreamChatClient` instance. +You can also add a `Filter`, a list of `SortOption`s and other pagination-related parameters. + +```dart +class ChannelListPageState extends State { + /// Controller used for loading more data and controlling pagination in + /// [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, + ], + ), + ]), + ); +``` + +Make sure you call `channelListController.doInitialLoad()` to load the initial data and `channelListController.dispose()` when the controller is no longer required. + +```dart +@override +void initState() { + channelListController.doInitialLoad(); + super.initState(); +} + +@override +void dispose() { + channelListController.dispose(); + super.dispose(); +} +``` + +The `StreamChannelListController` is basically a [`PagedValueNotifier`](./paged_value_notifier.mdx) that notifies you when the list of channels has changed. +You can use a [`PagedValueListenableBuilder`](./paged_value_listenable_builder.mdx) to build your UI depending on the latest channels. + +```dart +@override +Widget build(BuildContext context) => Scaffold( + 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( + /// We're using the channels length when there are no more + /// pages to load and there are no errors with pagination. + /// In case we need to show a loading indicator or and error + /// tile we're increasing the count by 1. + itemCount: (nextPageKey != null || error != null) + ? channels.length + 1 + : channels.length, + itemBuilder: (BuildContext context, int index) { + if (index == channels.length) { + if (error != null) { + return TextButton( + onPressed: () { + channelListController.retry(); + }, + child: Text(error.message), + ); + } + return CircularProgressIndicator(); + } + + 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', + ), + ), + ); + }, + ), + ); +``` + +In this case we're using the [`LazyLoadScrollView`](./lazy_load_scroll_view.mdx) widget to load more data when the user scrolls to the bottom of the list. \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_channel_list_event_handler.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_channel_list_event_handler.mdx new file mode 100644 index 000000000..043d41a18 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_channel_list_event_handler.mdx @@ -0,0 +1,64 @@ +--- +id: stream_channel_list_event_handler +sidebar_position: 10 +title: StreamChannelListEventHandler +--- + +A Class To Customize The Event Handler For The StreamChannelListController. + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamChannelListEventHandler-class.html) + +### Background + +A `StreamChannelListEventHandler` is a class that handles the events that are related to the channel list loaded by `StreamChannelListController`. +The `StreamChannelListController` automatically creates a `StreamChannelListEventHandler` internally and handles the events. In order to provide a custom +implementation of `StreamChannelListEventHandler`, you need to create a class that extends the `StreamChannelListEventHandler` class. + +### Basic Example + +There are 2 ways to provide a custom implementation of `StreamChannelListEventHandler`: + +* Create a class that extends the `StreamChannelListEventHandler` and pass it down to the controller. + +```dart +class MyCustomEventHandler extends StreamChannelListEventHandler { + @override + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + // Write your own custom implementation here + } +} +``` + +Pass it down to the controller: + +```dart + late final listController = StreamChannelListController( + client: StreamChat.of(context).client, + eventHandler: MyCustomEventHandler(), + ); +``` + +* Mix the `StreamChannelListEventHandler` into your widget state. + +```dart +class _ChannelListPageState extends State + with StreamChannelListEventHandler { + + @override + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + // Write your own custom implementation here + } + + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + eventHandler: this, + ); +} +``` + diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_chat_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_chat_core.mdx index 2a43ae522..e3434e7f7 100644 --- a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_chat_core.mdx +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_chat_core.mdx @@ -7,6 +7,8 @@ title: StreamChatCore `StreamChatCore` is a version of `StreamChat` found in `stream_chat_flutter` that is decoupled from theme and initialisations. +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamChatCore-class.html) + `StreamChatCore` is used to provide information about the chat client to the widget tree. This Widget is used to react to life cycle changes and system updates. When the app goes into the background, the websocket connection is automatically closed and when it goes back to foreground the connection is opened again. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_message_input_controller.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_message_input_controller.mdx new file mode 100644 index 000000000..7139d15e6 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_message_input_controller.mdx @@ -0,0 +1,89 @@ +--- +id: stream_message_input_controller +sidebar_position: 4 +title: StreamMessageInputController +--- + +A Widget For Controlling A Message Input + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamMessageInputController-class.html) + +### Background + +The `StreamMessageInputController` is a controller class that embed the business logic to compose a message. +`StreamMessageInputController` is a parameter of the `StreamMessageInput` widget. +Check the [`StreamMessageInput` documentation](../stream_chat_flutter/stream_message_input.mdx) to read more about that. + +### Basic Example + +Building a custom message input is a common task. Here is an example of how to use the `StreamMessageInputController` to build a simple custom message input widget. + +First of all we should create an instance of the `StreamMessageInputController`. + +```dart +class MessageScreenState extends State { + final StreamMessageInputController messageInputController = StreamMessageInputController(); +``` + +Make sure you call `messageInputController.dispose()` when the controller is no longer required. + +```dart +@override +void dispose() { + messageInputController.dispose(); + super.dispose(); +} +``` + +The `StreamMessageInputController` is basically a `ValueNotifier` that notifies you when the message being composed has changed. +You can use a `ValueListenableBuilder` to build your UI depending on the latest message. +For a very simple message input you could even pass the `messageInputController.textEditingController` to your `TextField` and set the `onChanged` callback. + +```dart +... +Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: messageInputController.textEditingController, + onChanged: (s) => messageInputController.text = s, + decoration: const InputDecoration( + hintText: 'Enter your message', + ), + ), + ), + Material( + type: MaterialType.circle, + color: Colors.blue, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + if (messageInputController.message.text?.isNotEmpty == + true) { + await channel.sendMessage( + messageInputController.message, + ); + messageInputController.clear(); + if (mounted) { + _updateList(); + } + } + }, + child: const Padding( + padding: EdgeInsets.all(8), + child: Center( + child: Icon( + Icons.send, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), +), +... +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_message_search_list_controller.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_message_search_list_controller.mdx new file mode 100644 index 000000000..f45463316 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_message_search_list_controller.mdx @@ -0,0 +1,128 @@ +--- +id: stream_message_search_list_controller +sidebar_position: 6 +title: StreamMessageSearchListController +--- + +A Widget For Controlling A List Of Searched Messages + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamMessageSearchListController-class.html) + +### Background + +The `StreamMessageSearchListController` is a controller class that allows you to control a list of searched messages. +`StreamMessageSearchListController` is a required parameter of the `StreamMessageSearchListView` widget. +Check the [`StreamMessageSearchListView` documentation](../stream_chat_flutter/stream_message_search_list_view.mdx) to read more about that. + +### Basic Example + +Building a custom message search feature is a common task. Here is an example of how to use the `StreamMessageSearchListController` to build a simple search list with pagination. + +First of all we should create an instance of the `StreamMessageSearchListController` and provide it with the `StreamChatClient` instance. +You can also add a `Filter`, a list of `SortOption`s and other pagination-related parameters. + +```dart +class SearchListPageState extends State { + /// Controller used for loading more data and controlling pagination in + /// [StreamMessageSearchListController]. + late final messageSearchListController = StreamMessageSearchListController( + client: StreamChatCore.of(context).client, + ); +``` + +Make sure you call `messageSearchListController.doInitialLoad()` to load the initial data and `messageSearchListController.dispose()` when the controller is no longer required. + +```dart +@override +void initState() { + messageSearchListController.doInitialLoad(); + super.initState(); +} + +@override +void dispose() { + messageSearchListController.dispose(); + super.dispose(); +} +``` + +The `StreamMessageSearchListController` is basically a [`PagedValueNotifier`](./paged_value_notifier.mdx) that notifies you when the list of responses has changed. +You can use a [`PagedValueListenableBuilder`](./paged_value_listenable_builder.mdx) to build your UI depending on the latest responses. + +```dart +@override +Widget build(BuildContext context) => Scaffold( + body: Column( + children: [ + TextField( + /// This is just a sample implementation of a search field. + /// In a real-world app you should throttle the search requests. + /// You can use our library [rate_limiter](https://pub.dev/packages/rate_limiter). + onChanged: (s) { + messageSearchListController.searchQuery = s; + messageSearchListController.doInitialLoad(); + }, + ), + Expanded( + child: PagedValueListenableBuilder( + valueListenable: messageSearchListController, + builder: (context, value, child) { + return value.when( + (responses, nextPageKey, error) => LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) { + messageSearchListController.loadMore(nextPageKey); + } + }, + child: ListView.builder( + /// We're using the responses length when there are no more + /// pages to load and there are no errors with pagination. + /// In case we need to show a loading indicator or and error + /// tile we're increasing the count by 1. + itemCount: (nextPageKey != null || error != null) + ? responses.length + 1 + : responses.length, + itemBuilder: (BuildContext context, int index) { + if (index == responses.length) { + if (error != null) { + return TextButton( + onPressed: () { + messageSearchListController.retry(); + }, + child: Text(error.message), + ); + } + return CircularProgressIndicator(); + } + + final _item = responses[index]; + return ListTile( + title: Text(_item.channel?.name ?? ''), + subtitle: Text(_item.message.text ?? ''), + ); + }, + ), + ), + 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', + ), + ), + ); + }, + ), + ), + ], + ), + ); +``` + +In this case we're using the [`LazyLoadScrollView`](./lazy_load_scroll_view.mdx) widget to load more data when the user scrolls to the bottom of the list. \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx new file mode 100644 index 000000000..0bee29ed3 --- /dev/null +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx @@ -0,0 +1,112 @@ +--- +id: stream_user_list_controller +sidebar_position: 5 +title: StreamUserListController +--- + +A Widget For Controlling A List Of Users + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter_core/latest/stream_chat_flutter_core/StreamUserListController-class.html) + +### Background + +The `StreamUserListController` is a controller class that allows you to control a list of users. +`StreamUserListController` is a required parameter of the `StreamUserListView` widget. +Check the [`StreamUserListView` documentation](../stream_chat_flutter/stream_user_list_view.mdx) to read more about that. + +### Basic Example + +Building a custom user list is a very common task. Here is an example of how to use the `StreamUserListController` to build a simple list with pagination. + +First of all we should create an instance of the `StreamUserListController` and provide it with the `StreamChatClient` instance. +You can also add a `Filter`, a list of `SortOption`s and other pagination-related parameters. + +```dart +class UserListPageState extends State { + /// Controller used for loading more data and controlling pagination in + /// [StreamUserListController]. + late final userListController = StreamUserListController( + client: StreamChatCore.of(context).client, + ); +``` + +Make sure you call `userListController.doInitialLoad()` to load the initial data and `userListController.dispose()` when the controller is no longer required. + +```dart +@override +void initState() { + userListController.doInitialLoad(); + super.initState(); +} + +@override +void dispose() { + userListController.dispose(); + super.dispose(); +} +``` + +The `StreamUserListController` is basically a [`PagedValueNotifier`](./paged_value_notifier.mdx) that notifies you when the list of users has changed. +You can use a [`PagedValueListenableBuilder`](./paged_value_listenable_builder.mdx) to build your UI depending on the latest users. + +```dart +@override +Widget build(BuildContext context) => Scaffold( + body: PagedValueListenableBuilder( + valueListenable: userListController, + builder: (context, value, child) { + return value.when( + (users, nextPageKey, error) => LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) { + userListController.loadMore(nextPageKey); + } + }, + child: ListView.builder( + /// We're using the users length when there are no more + /// pages to load and there are no errors with pagination. + /// In case we need to show a loading indicator or and error + /// tile we're increasing the count by 1. + itemCount: (nextPageKey != null || error != null) + ? users.length + 1 + : users.length, + itemBuilder: (BuildContext context, int index) { + if (index == users.length) { + if (error != null) { + return TextButton( + onPressed: () { + userListController.retry(); + }, + child: Text(error.message), + ); + } + return CircularProgressIndicator(); + } + + final _item = users[index]; + return ListTile( + title: Text(_item.name ?? ''), + ); + }, + ), + ), + 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', + ), + ), + ); + }, + ), + ); +``` + +In this case we're using the [`LazyLoadScrollView`](./lazy_load_scroll_view.mdx) widget to load more data when the user scrolls to the bottom of the list. \ No newline at end of file diff --git a/docusaurus/flutter-docusaurus-dontent-docs.plugin.js b/docusaurus/flutter-docusaurus-dontent-docs.plugin.js new file mode 100644 index 000000000..7a09ae26d --- /dev/null +++ b/docusaurus/flutter-docusaurus-dontent-docs.plugin.js @@ -0,0 +1,19 @@ +module.exports = { + plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + lastVersion: 'current', + versions: { + current: { + label: 'v4' + }, + '3.x.x': { + label: 'v3', + path: 'v3' + } + } + } + ] + ] +} diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/authentication_demo_app.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/authentication_demo_app.jpg new file mode 100644 index 000000000..eed57c337 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/authentication_demo_app.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_header.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_header.png new file mode 100644 index 000000000..98f41c614 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_header.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_header_custom_title.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_header_custom_title.png new file mode 100644 index 000000000..65a9ee75a Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_header_custom_title.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_header.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_header.png new file mode 100644 index 000000000..6f112db9b Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_header.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_header_custom_subtitle.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_header_custom_subtitle.png new file mode 100644 index 000000000..3c7798570 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_header_custom_subtitle.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_view.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_view.png new file mode 100644 index 000000000..a4390c2ea Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_list_view.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_preview.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_preview.png new file mode 100644 index 000000000..39f5629bb Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/channel_preview.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/chat_basics.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/chat_basics.png new file mode 100644 index 000000000..82e27cfa4 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/chat_basics.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/chat_overview_page-2fbd5bbfb70c5623bd37ff7d6c41bf4d.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/chat_overview_page-2fbd5bbfb70c5623bd37ff7d6c41bf4d.png new file mode 100644 index 000000000..9e92437db Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/chat_overview_page-2fbd5bbfb70c5623bd37ff7d6c41bf4d.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_firebase_enable.jpeg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_firebase_enable.jpeg new file mode 100644 index 000000000..e4696ec88 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_firebase_enable.jpeg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_firebase_key.jpeg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_firebase_key.jpeg new file mode 100644 index 000000000..87243e91f Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_firebase_key.jpeg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_save_changes.jpeg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_save_changes.jpeg new file mode 100644 index 000000000..1c58a93e6 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/dashboard_save_changes.jpeg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/end_to_end_encryption.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/end_to_end_encryption.png new file mode 100644 index 000000000..b41e11ced Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/end_to_end_encryption.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_authentication_dashboard.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_authentication_dashboard.jpg new file mode 100644 index 000000000..c30405576 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_authentication_dashboard.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_notifications_toggle-5aeabfcbdc24cb8f1fea7d41d0e845fc.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_notifications_toggle-5aeabfcbdc24cb8f1fea7d41d0e845fc.png new file mode 100644 index 000000000..c5670d0dc Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_notifications_toggle-5aeabfcbdc24cb8f1fea7d41d0e845fc.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_project_settings.jpeg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_project_settings.jpeg new file mode 100644 index 000000000..4fbc6521c Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/firebase_project_settings.jpeg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/hashtag_example.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/hashtag_example.jpg new file mode 100644 index 000000000..a2bb3ba4c Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/hashtag_example.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/live_stream_1.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/live_stream_1.jpg new file mode 100644 index 000000000..bef68c6ed Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/live_stream_1.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/live_stream_2.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/live_stream_2.jpg new file mode 100644 index 000000000..02a351329 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/live_stream_2.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/localization_support.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/localization_support.jpg new file mode 100644 index 000000000..50d8b761e Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/localization_support.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example.jpg new file mode 100644 index 000000000..7f220379c Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example_message.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example_message.jpg new file mode 100644 index 000000000..707f13887 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example_message.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example_message_thumbnail.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example_message_thumbnail.jpg new file mode 100644 index 000000000..e2b7d624f Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/location_sharing_example_message_thumbnail.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_actions.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_actions.jpg new file mode 100644 index 000000000..dcfba64ec Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_actions.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input.png new file mode 100644 index 000000000..d63cfbb3e Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input_change_position.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input_change_position.png new file mode 100644 index 000000000..859107d25 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input_change_position.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input_quoted_message.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input_quoted_message.png new file mode 100644 index 000000000..c1fda2237 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_input_quoted_message.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view.png new file mode 100644 index 000000000..cc27be3c9 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view_pin.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view_pin.png new file mode 100644 index 000000000..0b6f1c391 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view_pin.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view_threads.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view_threads.png new file mode 100644 index 000000000..2c9387663 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_list_view_threads.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_reaction_theming.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_reaction_theming.png new file mode 100644 index 000000000..8cddd6ba2 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_reaction_theming.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_rounded_avatar.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_rounded_avatar.png new file mode 100644 index 000000000..ec3733443 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_rounded_avatar.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_search_list_view.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_search_list_view.png new file mode 100644 index 000000000..b13f5bdc9 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_search_list_view.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_styles.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_styles.png new file mode 100644 index 000000000..db04865c0 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_styles.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_theming.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_theming.png new file mode 100644 index 000000000..b5d28dde0 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_theming.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_widget_actions.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_widget_actions.png new file mode 100644 index 000000000..e835a4ed0 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/message_widget_actions.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/sdk_title.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/sdk_title.png new file mode 100644 index 000000000..4d92e8914 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/sdk_title.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/server_key.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/server_key.png new file mode 100644 index 000000000..f11d6fb56 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/server_key.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/stream_chat_user_database.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/stream_chat_user_database.jpg new file mode 100644 index 000000000..fcfe73c82 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/stream_chat_user_database.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/swipe_channel.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/swipe_channel.png new file mode 100644 index 000000000..44f13ca59 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/swipe_channel.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/user_list_view.png b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/user_list_view.png new file mode 100644 index 000000000..9aede8243 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/user_list_view.png differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/using_theme.jpg b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/using_theme.jpg new file mode 100644 index 000000000..b835d9316 Binary files /dev/null and b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/assets/using_theme.jpg differ diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/_category_.json b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/_category_.json new file mode 100644 index 000000000..76d335692 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Introduction", + "position": 1 +} \ No newline at end of file diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/choose_package.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/choose_package.mdx new file mode 100644 index 000000000..8be73f010 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/choose_package.mdx @@ -0,0 +1,55 @@ +--- +id: choose_package +sidebar_position: 2 +title: Choosing The Right Flutter Package +--- + +### Why the SDK is split into different packages + +Different applications need different levels of customization and integration with the Stream Chat SDK. +To do this, the Flutter SDK is split into three different packages which build upon the last and give +varying levels of control to the developer. The higher level packages offer better compatibility out of the +box while the lower level SDKs offer fine grained control. There is also a separate package for persistence +which allows you persist data locally which works with all packages. + +### How do I choose? + +#### The case for stream_chat_flutter + +For the quickest way to integrate Stream Chat with your app, the UI SDK (`stream_chat_flutter`) is the +way to go. `stream_chat_flutter` contains prebuilt components that manage most operations like data +fetching, pagination, sending a message, and more. This ensures you have a nearly out-of-the-box +experience adding chat to your applications. It is also possible to use this in conjunction with +lower level operations of the SDK to get the best of both worlds. + +:::note +The package allows customization of components to a large extent making it easy to tweak the theme +to match your app colors and such. If you require any additional feature or customization, feel free +to request this through our support channels. +::: + +Summary: + +For the quickest and easiest way to add Chat to your app with prebuilt UI components, use stream_chat_flutter + + +#### The case for stream_chat_flutter_core + +If your application involves UI that does not fit in with the stream_chat_flutter components, stream_chat_flutter_core +strips away the UI associated with the components and provides the data fetching and manipulation +capabilities while supplying builders for UI. This allows you to implement your own UI and themes +completely independently while not worrying about writing functions for data and pagination. + +Summary: + +For implementing your own custom UI while not having to worry about lower level API calls, use stream_chat_flutter_core. + +#### The case for stream_chat + +The stream_chat package is the Low-level Client (LLC) of Stream Chat in Flutter. This package wraps +the underlying functionality of Stream Chat and allows the most customization in terms of UI, data, +and architecture. + +Summary: + +For the most control over the SDK and dealing with low level calls to the API, use stream_chat. diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/introduction.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/introduction.mdx new file mode 100644 index 000000000..99170774e --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/introduction.mdx @@ -0,0 +1,77 @@ +--- +slug: / +id: introduction +sidebar_position: 1 +title: About The Flutter SDK +--- +Exploring The Basics Of Stream Chat + +![](../assets/sdk_title.png) + +Stream Chat is a service that helps you easily build a full chat experience in your Flutter apps. +We also support a variety of other SDKs. + +This section of the documentation focuses on our Flutter SDK which helps you easily +ship high quality messaging experiences in apps and programs built with the [Flutter toolkit +made by Google](https://flutter.dev). + +The Stream Chat Flutter SDK comprises five different packages to choose from, ranging from ones +giving you complete control to ones that give you a rich out-of-the-box chat experience. + +The packages that make up the Stream Chat SDK are: + +1. Low Level Client (stream_chat): a pure Dart package that can be used on any Dart project. +It provides a low-level client to access the Stream Chat service. +2. Core (stream_chat_flutter_core): provides business logic to fetch common things required +for integrating Stream Chat into your application. +The core package allows more customisation and hence provides business logic but no UI components. +3. UI (stream_chat_flutter): this library includes both a low-level chat SDK and a set of +reusable and customisable UI components. +4. Persistence (stream_chat_persistence): provides a persistence client for fetching and +saving chat data locally. +5. Localizations (stream_chat_localizations): provides a set of localizations for the SDK. + +We recommend building prototypes using the full UI package, [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter), +since it contains UI widgets already integrated with Stream's API. It is the fastest way to get up +and running using Stream chat in your app. + +The Flutter SDK enables you to build any type of chat or messaging experience for Android, iOS, Web +and Desktop. + +If you're building a very custom UI and would prefer a more lean package, +[stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) will be suited to this +use case. Core allows you to build custom, expressive UIs while retaining the benefits of our full +Flutter SDK. APIs for accessing and controlling users, sending messages, and so forth are seamlessly integrated +into this package and accessible via providers and builders. + +Before going into the docs, let's take a small detour to look at how the elements of Stream Chat are structured. + +### Basic Structure + +There are two core elements in chat, Users and Channels. +Channels are groups of one or more users that can message each other. +In an app, you need to have a user connected to query channels. + +There is no specific distinction between a chat with only two people and a group chat, +but there is a way to create a unique chat between a certain number of people by creating a distinct channel. + +![](../assets/chat_basics.png) + +In essence, a normal two-person chat would be a distinct channel created with two members (you cannot add or delete members in this channel), whereas a group created with two people would simply be a non distinct channel (possible to add or remove members). + +:::note +It is also possible to add more than two people in a distinct channel which retains the same add/removal properties and resembles the Slack DMs where you can DM one or more people as well. +::: + +In summary, if you were creating a Whatsapp-like app, the first screen would be a list of channels - which on opening would show a list of messages that were sent by the users in the Channel. + +While this is a simplistic overview of the service, the Flutter SDK handles the UI and more time consuming things (media upload, offline storage, theming, etc.) for you. + +Before reading the docs, consider trying our [online API tour](https://getstream.io/chat/get_started/), +it is a nice way to learn how the API works. +It's in-browser so you'll need to use Javascript but the core conceps are pretty much the same as Dart. + +You may also like to look at the [Flutter tutorial](https://getstream.io/chat/flutter/tutorial/) +which focuses on using the UI package to get Stream Chat integrated into a Flutter app. + +Further sections break down each individual packages and explain several common operations. \ No newline at end of file diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/versioning_policy.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/versioning_policy.mdx new file mode 100644 index 000000000..48f25d66b --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/basics/versioning_policy.mdx @@ -0,0 +1,20 @@ +--- +id: versioning_policy +sidebar_position: 3 +title: Versioning Policy +--- + +All of the Stream Chat packages follow [semantic versioning (semver)](https://semver.org/). + +That means that with a version number x.y.z (major.minor.patch): +- When releasing bug fixes (backwards compatible), we make a patch release by changing the z number (ex: 3.6.2 to 3.6.3). A bug fix is defined as an internal change that fixes incorrect behavior. +- When releasing new features or non-critical fixes, we make a minor release by changing the y number (ex: 3.6.2 to 3.7.0). +- When releasing breaking changes (backward incompatible), we make a major release by changing the x number (ex: 3.6.2 to 4.0.0). + +See the [semantic versioning](https://dart.dev/tools/pub/versioning#semantic-versions) section from the Dart docs for more information. + +This versioning policy does not apply to prerelease packages (below major version of 1). See this +[StackOverflow thread](https://stackoverflow.com/questions/66201337/how-do-dart-package-versions-work-how-should-i-version-my-flutter-plugins) +for more information on Dart package versioning. + +Whenever possible, we will add deprecation warnings in preparation for future breaking changes. diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/_category_.json b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/_category_.json new file mode 100644 index 000000000..cb58ac0dc --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guides", + "position": 2 +} \ No newline at end of file diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_chat_to_video_livestreams.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_chat_to_video_livestreams.mdx new file mode 100644 index 000000000..9e2afb447 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_chat_to_video_livestreams.mdx @@ -0,0 +1,93 @@ +--- +id: adding_chat_to_video_livestreams +sidebar_position: 7 +title: Adding Chat To Video Livestreams +--- + +Adding Chat To Video Livestreams + +### Introduction + +Video livestreams are usually complemented with a chat section to make the livestream more interactive +and encourage retention. There are several ways to show the chat interface on the screen and requires +some design choices. + +This guide details multiple ways of adding chat functionality to your video livestream. + +### Implementing Chat + +There are two common scenarios in live-streaming applications depending how well integrated the two +components (video + chat) are allowed to be on the screen. Two common types are split-screen and a +chat overlay that fades in. + +Let's explore creating both types: + +### Split-screen + +In the split-screen implementation, we have a visual split between the video and the message list. +This allows the content to be unobstructed by chat and have a clear separation of boundaries. + +![](../assets/live_stream_1.jpg) + +```dart +Scaffold( + body: Column( + children: [ + Expanded( + child: // Your video implementation here, + ), + Expanded( + child: Column( + children: [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ), + ], + ), +) +``` + +### Overlapping chat with a transparency gradient + +Another way to add chat is to overlay the video content with messages which progressively fade out +as we go to the top of the screen. This gives the content a more rich feel as it takes the whole +screen and allows the chat to be more homogeneously integrated with the content. + +The second type looks like this: + +![](../assets/live_stream_2.jpg) + +We can use a `Stack` for achieving this: + +```dart +Scaffold( + body: Stack( + children: [ + // Add your video implementation here + ShaderMask( + shaderCallback: (rect) { + return LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black, Colors.transparent], + stops: [0.4, 0.65] + ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); + }, + blendMode: BlendMode.dstIn, + child: Column( + children: [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ), + ], + ), + ) +``` diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_custom_attachments.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_custom_attachments.mdx new file mode 100644 index 000000000..9fe7f6aad --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_custom_attachments.mdx @@ -0,0 +1,264 @@ +--- +id: adding_custom_attachments +sidebar_position: 3 +title: Adding Custom Attachments +--- + +Adding Your Own Types Of Attachments To A Message + +### Introduction + +Stream Chat supports attachment types like images, video and files by default. You can also add your +own types of attachments through the SDK such as location, audio, etc. + +This involves doing three things: + +1) Rendering the attachment thumbnail in the `MessageInput` + +2) Sending a message with the custom attachment + +3) Rendering the custom message attachment + +To do this, let's check out an example to add location sharing to Stream Chat. + +### Location Sharing + +Let's build an example of location sharing option in the app: + +![](../assets/location_sharing_example.jpg) + +* Show a "Share Location" button next to MessageInput Textfield. + +* When the user presses this button, it should fetch the current location coordinates of the user, and send a message on the channel as follows: + +```dart +Message( + text: 'This is my location', + attachments: [ + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': 'fetched_latitude', + 'longitude': 'fetched_longitude', + }, + ), + ], +) +``` + +For our example, we are going to use [geolocator](https://pub.dev/packages/geolocator) library. +Please check their [setup instructions](https://pub.dev/packages/geolocator) on their docs. + +NOTE: If you are testing on iOS simulator, you will need to set some dummy coordinates, as mentioned [here](https://stackoverflow.com/a/31238119/7489541). +Also don't forget to enable "location update" capability in background mode, from XCode. + +On the receiver end, `location` type attachment should be rendered in map view, in the `MessageListView`. +We are going to use [Google Static Maps API](https://developers.google.com/maps/documentation/maps-static/overview) to render the map in the message. +You can use other libraries as well such as [google_maps_flutter](https://pub.dev/packages/google_maps_flutter). + +First, we add a button which when clicked fetches and shares location into the `MessageInput`: + +```dart +MessageInput( + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + var channel = StreamChannel.of(context).channel; + var user = StreamChat.of(context).user; + + _determinePosition().then((value) { + channel.sendMessage( + Message( + text: 'This is my location', + attachments: [ + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': value.latitude.toString(), + 'longitude': value.longitude.toString(), + }, + ), + ], + ), + ); + }).catchError((err) { + print('Error getting location!'); + }); + }, + ), + ], +), + +Future _determinePosition() async { + bool serviceEnabled; + LocationPermission permission; + + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return Future.error('Location services are disabled.'); + } + + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.deniedForever) { + return Future.error( + 'Location permissions are permanently denied, we cannot request permissions.'); + } + + if (permission == LocationPermission.denied) { + return Future.error( + 'Location permissions are denied'); + } + } + + return await Geolocator.getCurrentPosition(); +} +``` + +Next, we build the Static Maps URL (Add your API key before using the code snippet): + +```dart + String _buildMapAttachment(String lat, String long) { + var baseURL = 'https://maps.googleapis.com/maps/api/staticmap?'; + var url = Uri( + scheme: 'https', + host: 'maps.googleapis.com', + port: 443, + path: '/maps/api/staticmap', + queryParameters: { + 'center': '${lat},${long}', + 'zoom': '15', + 'size': '600x300', + 'maptype': 'roadmap', + 'key': 'YOUR_API_KEY', + 'markers': 'color:red|${lat},${long}' + }); + + return url.toString(); + } +``` + +And then modify the `MessageListView` and tell it how to build a location attachment, using the `messageBuilder` property and copying the default message implementation overriding the `customAttachmentBuilders` property: + +```dart +MessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + customAttachmentBuilders: { + 'location': (context, message, attachments) { + final attachmentWidget = Image.network( + _buildMapAttachment( + attachments[0].extraData['latitude'], + attachments[0].extraData['longitude'], + ), + ); + + return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); + } + }, + ); + }, +), +``` + +This gives us the final location attachment: + +![](../assets/location_sharing_example_message.jpg) + +Additionally, you can also add a thumbnail if a message has a location attachment (unlike in this case, where we sent the message directly). + +To do this, we will: + +1) Add an attachment instead of sending a message + +2) Customize the `MessageInput` + +First, we add the attachment when the location button is clicked: + +```dart + GlobalKey _messageInputKey = GlobalKey(); + + MessageInput( + key: _messageInputKey, + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + _determinePosition().then((value) { + _messageInputKey.currentState.addAttachment( + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': value.latitude.toString(), + 'longitude': value.longitude.toString(), + }, + ), + ); + }).catchError((err) { + print('Error getting location!'); + }); + }, + ), + ], + ), +``` + +After this, we can build the thumbnail: + +```dart +MessageInput( + key: _messageInputKey, + actions: [ + InkWell( + child: Icon( + Icons.location_on, + size: 20.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + _determinePosition().then((value) { + _messageInputKey.currentState.addAttachment( + Attachment( + uploadState: UploadState.success(), + type: 'location', + extraData: { + 'latitude': value.latitude.toString(), + 'longitude': value.longitude.toString(), + }, + ), + ); + }).catchError((err) { + print('Error getting location!'); + }); + }, + ), + ], + attachmentThumbnailBuilders: { + 'location': (context, attachment) { + return Image.network( + _buildMapAttachment( + attachment.extraData['latitude'], + attachment.extraData['longitude'], + ), + ); + }, + }, +), +``` + +And we can see the thumbnails in the MessageInput: + +![](../assets/location_sharing_example_message_thumbnail.jpg) diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_local_data_persistence.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_local_data_persistence.mdx new file mode 100644 index 000000000..e6283ca04 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_local_data_persistence.mdx @@ -0,0 +1,76 @@ +--- +id: adding_local_data_persistence +sidebar_position: 9 +title: Adding Local Data Persistence +--- + +Adding Local Data Persistence + +### Introduction + +Most messaging apps need to work regardless of whether the app is currently connected to the internet. +Local data persistence stores the fetched data from the backend on a local SQLite database using the +moor package in Flutter. All packages in the SDK can use local data persistence to store messages +across multiple platforms. + +### Implementation + +To add data persistence you can extend the class ChatPersistenceClient and pass an instance to the StreamChatClient. + +```dart +class CustomChatPersistentClient extends ChatPersistenceClient { +... +} + +final client = StreamChatClient( + apiKey ?? kDefaultStreamApiKey, + logLevel: Level.INFO, +)..chatPersistenceClient = CustomChatPersistentClient(); +``` + +We provide an official persistent client in the [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) +package that works using the library [moor](https://moor.simonbinder.eu), an SQLite ORM. + +Add this to your package's `pubspec.yaml` file, using the latest version. + +```yaml +dependencies: + stream_chat_persistence: ^latest_version +``` + +You should then run `flutter packages get` + +The usage is pretty simple. + +1. Create a new instance of `StreamChatPersistenceClient` providing `logLevel` and `connectionMode` + +```dart +final chatPersistentClient = StreamChatPersistenceClient( + logLevel: Level.INFO, + connectionMode: ConnectionMode.background, +); +``` + +2. Pass the instance to the official `StreamChatClient` + +```dart + final client = StreamChatClient( + apiKey ?? kDefaultStreamApiKey, + logLevel: Level.INFO, + )..chatPersistenceClient = chatPersistentClient; +``` + +And you are ready to go... + +Note that passing `ConnectionMode.background` the database uses a background isolate to unblock the main thread. +The `StreamChatClient` uses the `chatPersistentClient` to synchronize the database with the newest +information every time it receives new data about channels/messages/users. + +### Multi-user + +The DB file is named after the `userId`, so if you instantiate a client using a different `userId` you will use a different database. +Calling `client.disconnectUser(flushChatPersistence: true)` flushes all current database data. + +### Updating/deleting/sending a message while offline + +The information about the action is saved in offline storage. When the client returns online, everything is retried. diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_localization.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_localization.mdx new file mode 100644 index 000000000..c61f26c70 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_localization.mdx @@ -0,0 +1,191 @@ +--- +id: adding_localization +sidebar_position: 2 +title: Adding Localization (l10n) / Internationalization (i18n) +--- + +Adding Localization To UI Widgets + +### Introduction + +We have a dedicated package for adding localization to our UI widgets. It's called `stream_chat_localizations` and you can find it [here](https://pub.dev/packages/stream_chat_localizations). + +![](../assets/localization_support.jpg) + +## What is Localization? + +If you deploy your app to users who speak another language, you'll need to internationalize (localize) it. That means you need to write the app in a way that makes it possible to localize values like text and layouts for each language or locale that the app supports. For more information, see the [Flutter documentation](https://flutter.dev/docs/development/accessibility-and-localization/internationalization). + +What this package allows you to do is to provide localized strings for the Stream chat widgets. For example, depending on the application locale, the Stream Chat widgets will display the appropriate language. The locale will be set automatically, based on system preferences, or you could set it programmatically in your app. The package supports several different languages, with more to be added. The package allows you to override any supported language or add a new language that isn't supported. + +:::note +If you want to translate messages, or enable automatic translation, please see the [Translation documentation](https://getstream.io/chat/docs/flutter-dart/translation/?language=dart). +::: + +### Supported languages + +At the moment we support the following languages: +- [English](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart) +- [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart) +- [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart) +- [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart) +- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) +- [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) +- [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) +More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages. + +### Add dependency + +Add this to your package's `pubspec.yaml` file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_localizations.svg)](https://pub.dartlang.org/packages/stream_chat_localizations) +```yaml +dependencies: + stream_chat_localizations: ^latest_version +``` + +Then run `flutter packages get` + +### Usage + +Generally, Flutter and the Stream Chat SDK will use the system locale of the user's device, if that locale is supported (see below). If the locale is not supported we will default to `en` (however it's always possible to [customize that](#changing-the-default-language)). +Make sure to read more about localization in the [official Flutter docs](https://flutter.dev/docs/development/accessibility-and-localization/internationalization). + +```dart +import 'package:flutter/material.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + Locale('es'), + Locale('ja'), + Locale('ko'), + ], + // Add GlobalStreamChatLocalizations.delegates + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); + } +} +``` + +## Setting a language +The application language can be changed through system preferences or programmatically. + +### System Preferences +The application locale can be changed by changing the language for your device or emulator within the device's system preferences. + +[iOS change language](https://support.apple.com/en-us/HT204031) + +[Android change language](https://support.google.com/websearch/answer/3333234?co=GENIE.Platform%3DAndroid&hl=en) + +Note that the language needs to be supported in your application to work. + +### Programmatically +You can also set the locale programmatically in your Flutter application without changing the device's language. + +```dart +return MaterialApp( + ... + locale: const Locale('fr'), + ... +); +``` + +There are many ways that this can be set for additional control. For information and examples, see this [Stack Overflow post](https://stackoverflow.com/questions/49441212/flutter-multi-lingual-application-how-to-override-the-locale). + +### Adding a new language + +To add a new language, create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it, adding it to the `delegates` array. + +Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/add_new_lang.dart) to see how to add a new language. + +### Override existing languages + +To override an existing language, create a new class extending that particular language class and create a delegate for it, adding it to the `delegates` array. + +Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/override_lang.dart) to see how to override an existing language. + +### Changing the default language + +To change the default language you can use the `MaterialApp.localeListResolutionCallback` property. +Here is an example of how that would look like: + +```dart + MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + Locale('es'), + Locale('ja'), + Locale('ko'), + ], + // locales are the locales of the device + // supportedLocales are the app supported locales + localeListResolutionCallback: (locales, supportedLocales) { + // We map the supported locales to language codes + // note that this is completely optional and this logic can be changed as you like + final supportedLanguageCodes = + supportedLocales.map((e) => e.languageCode); + if (locales != null) { + // we iterate over the locales and find the first one that is supported + for (final locale in locales) { + if (supportedLanguageCodes.contains(locale.languageCode)) { + return locale; + } + } + } + + // if we didn't find a supported language, we return the Italian language + return const Locale('it'); + }, + // Add GlobalStreamChatLocalizations.delegates + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + ... + +``` + +In this case, we're using Italian as the default language. + +### ⚠️ Note on **iOS** + +For translation to work on **iOS** you need to add supported locales to +`ios/Runner/Info.plist` as described [here](https://flutter.dev/docs/development/accessibility-and-localization/internationalization#specifying-supportedlocales). + +Example: + +```xml +CFBundleLocalizations + + en + nb + fr + it + es + ja + ko + +``` diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx new file mode 100644 index 000000000..328ba5462 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx @@ -0,0 +1,263 @@ +--- +id: adding_push_notifications +sidebar_position: 1 +title: Adding Push Notifications (V1 legacy) +--- + +Adding Push Notifications To Your Application + +:::note +Version 1 (legacy) of push notifications won't be removed immediately but there won't be any new features. That's why new applications are highly recommended to use version 2 from the beginning to leverage upcoming new features. +::: + +### Introduction + +Push notifications are a core part of the experience for a messaging app. Users often need to be notified +of new messages and old notifications sometimes need to be updated silently as well. + +This guide details how to add push notifications to your app. + +Make sure to check [this section](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) of the docs to read about the push delivery logic. + +### Setup FCM + +To integrate push notifications in your Flutter app you need to use the package [firebase_messaging](https://pub.dev/packages/firebase_messaging). + + +Follow the [Firebase documentation](https://firebase.flutter.dev/docs/messaging/overview/) to know how to set up the plugin for both Android and iOS. + + +Once that's done FCM should be able to send push notifications to your devices. + +### Integration with Stream + +#### Step 1 + +From the [Firebase Console](https://console.firebase.google.com/), select the project your app belongs to. + +#### Step 2 + +Click on the gear icon next to `Project Overview` and navigate to **Project settings** + +![](../assets/firebase_project_settings.jpeg) + +#### Step 3 + +Navigate to the `Cloud Messaging` tab + +#### Step 4 + +Under `Project Credentials`, locate the `Server key` and copy it + +![](../assets/server_key.png) + +#### Step 5 + +Upload the `Server Key` in your chat dashboard + +![](../assets/dashboard_firebase_enable.jpeg) + +![](../assets/dashboard_firebase_key.jpeg) + + +:::note +We are setting up the Android section, but this will work for both Android and iOS if you're using Firebase for both of them! +::: + +#### Step 6 + +Save your push notification settings changes + +![](../assets/dashboard_save_changes.jpeg) + +**OR** + +Upload the `Server Key` via API call using a backend SDK + +```js +await client.updateAppSettings({ + firebase_config: { + server_key: 'server_key', + notification_template: `{"message":{"notification":{"title":"New messages","body":"You have {{ unread_count }} new message(s) from {{ sender.name }}"},"android":{"ttl":"86400s","notification":{"click_action":"OPEN_ACTIVITY_1"}}}}`, + data_template: `{"sender":"{{ sender.id }}","channel":{"type": "{{ channel.type }}","id":"{{ channel.id }}"},"message":"{{ message.id }}"}` + }, +}); +``` + +### Registering a device at Stream Backend + +Once you configure Firebase server key and set it up on Stream dashboard a device that is supposed to receive push notifications needs to be registered at Stream backend. This is usually done by listening for Firebase device token updates and passing them to the backend as follows: + +```dart +firebaseMessaging.onTokenRefresh.listen((token) { + client.addDevice(token, PushProvider.firebase); +}); +``` + +### Possible issues + + +We only send push notifications when the user doesn't have any active websocket connection (which is established when you call `client.connectUser`). If you set the [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html) property of the StreamChat widget, when your app goes to background, your device will keep the ws connection alive for 1 minute, and so within this period, you won't receive any push notification. + +Make sure to read the [general push docs](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) in order to avoid known gotchas that may make your relationship with notifications go bad 😢 + +### Testing if Push Notifications are Setup Correctly + +If you're not sure if you've set up push notifications correctly (e.g. you don't always receive them, they work unreliably), you can follow these steps to make sure your config is correct and working: + +1. Clone our repo for push testing git clone git@github.com:GetStream/chat-push-test.git + +2. `cd flutter` + +3. In folder run `flutter pub get` + +4. Input your api key and secret in `lib/main.dart` + +5. Change the bundle identifier/application ID and development team/user so you can run the app in your device (**do not** run on iOS simulator, Android emulator is fine) + +6. Add your `google-services.json/GoogleService-Info.plist` + +7. Run the app + +8. Accept push notification permission (iOS only) + +9. Tap on `Device ID` and copy it + +10. Send the app to background + +11. After configuring [stream-cli](https://github.com/GetStream/stream-cli) paste the following command on command line using your user ID + +```shell +stream chat:push:test -u +``` + +You should get a test push notification + +### App in the background but still connected + +The [StreamChat](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat-class.html) widget lets you define a [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html) handler in order to handle events while the app is in the background, but the client is still connected. + +This is useful because it lets you keep the connection alive in cases in which the app goes in the background just for some seconds (eg: multitasking, picking pictures from the gallery...) + +You can even customize the [backgroundKeepAlive](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/backgroundKeepAlive.html) duration. + +In order to show notifications in such a case we suggest using the package [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications); follow the package guide to successfully set up the plugin. + +Once that's done you should set the [onBackgroundEventReceived](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat/onBackgroundEventReceived.html); here is an example: + +```dart +... +StreamChat( + client: client, + onBackgroundEventReceived: (e) { + final currentUserId = client.state.user.id; + if (![ + EventType.messageNew, + EventType.notificationMessageNew, + ].contains(event.type) || + event.user.id == currentUserId) { + return; + } + if (event.message == null) return; + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + final initializationSettingsAndroid = + AndroidInitializationSettings('launch_background'); + final initializationSettingsIOS = IOSInitializationSettings(); + final initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + await flutterLocalNotificationsPlugin.show( + event.message.id.hashCode, + event.message.user.name, + event.message.text, + NotificationDetails( + android: AndroidNotificationDetails( + 'message channel', + 'Message channel', + 'Channel used for showing messages', + priority: Priority.high, + importance: Importance.high, + ), + iOS: IOSNotificationDetails(), + ), + ); + }, + child: .... +); +... +``` + +As you can see we generate a local notification whenever a message.new or notification.message_new event is received. + +### Foreground notifications + +Sometimes you may want to show a notification when the app is in the foreground. +For example, when you're in a channel and you receive a new message from someone in another channel. + +For this scenario, you can also use the `flutter_local_notifications` package to show a notification. + +You need to listen for new events using `StreamChatClient.on` and handle them accordingly. + +Here we're checking if the event is a `message.new` or `notification.message_new` event, and if the message is from a different user than the current user. In that case we'll show a notification. + +```dart +client.on( + EventType.messageNew, + EventType.notificationMessageNew, +).listen((event) { + if (event.message?.user?.id == client.state.currentUser?.id) { + return; + } + showLocalNotification(event, client.state.currentUser!.id, context); +}); +``` + +:::note +You should also check that the channel of the message is different than the channel in the foreground. +How you do this depends on your app infrastructure and how you handle navigation. +Take a look at the [Stream Chat v1 sample app](https://github.com/GetStream/flutter-samples/blob/main/packages/stream_chat_v1/lib/home_page.dart#L11) to see how we're doing it over there. +::: + +### Saving notification messages to the offline storage + +You may want to save received messages when you receive them via a notification so that later on when you open the app they're already there. + +To do this we need to update the push notification data payload at Stream Dashboard and clear the notification one: + +```json +{ + "message_id": "{{ message.id }}", + "channel_id": "{{ channel.id }}", + "channel_type": "{{ channel.type }}" +} +``` + +Then we need to integrate the package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) in our app that exports a persistence client, learn [here](https://pub.dev/packages/stream_chat_persistence#usage) how to set it up. + +Then during the call `firebaseMessaging.configure(...)` we need to set the `onBackgroundMessage` parameter using a TOP-LEVEL or STATIC function to handle background messages; here is an example: + +```dart +Future myBackgroundMessageHandler(message) async { + if (message.containsKey('data')) { + final data = message['data']; + final messageId = data['message_id']; + final channelId = data['channel_id']; + final channelType = data['channel_type']; + final cid = '$channelType:$channelId'; + + final client = StreamChatClient(apiKey); + final persistenceClient = StreamChatPersistenceClient(); + await persistenceClient.connect(userId); + + final message = await client.getMessage(messageId).then((res) => res.message); + + await persistenceClient.updateMessages(cid, [message]); + persistenceClient.disconnect(); + + /// This can be done using the package flutter_local_notifications as we did before 👆 + _showLocalNotification(); + } +} +``` diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx new file mode 100644 index 000000000..285c2205a --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx @@ -0,0 +1,301 @@ +--- +id: adding_push_notifications_v2 +sidebar_position: 1 +title: Adding Push Notifications (V2) +--- + +Adding Push Notifications To Your Application + +### Introduction + +Push notifications are a core part of the experience for a messaging app. Users often need to be notified +of new messages and old notifications sometimes need to be updated silently. + +This guide details how to add push notifications to your app. + +You can read more about Stream’s [push delivery logic](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart#push-delivery-rules). + +### Setup FCM + +To integrate push notifications in your Flutter app, you need to use the package [firebase_messaging](https://pub.dev/packages/firebase_messaging). + + +Follow the [Firebase documentation](https://firebase.flutter.dev/docs/messaging/overview/) to set up the plugin for Android and iOS. + + +Once that's done, FCM should be able to send push notifications to your devices. + +### Integration With Stream + +#### Step 1 - Get the Firebase Credentials + +These credentials are the [private key file](https://firebase.google.com/docs/admin/setup#:~:text=To%20generate%20a%20private%20key%20file%20for%20your%20service%20account%3A) for your service account, in firebase console. + +To generate a private key file for your service account, in the Firebase console: + +- Open Settings > Service Accounts. + +- Click **Generate New Private Key**, then confirm by clicking **Generate Key**. + +- Securely store the JSON file containing the key. + +This JSON file contains the credentials which needs to be uploaded to Stream’s server as explained in next step. + +#### Step 2 - Upload the Firebase Credentials to Stream + +You can upload your Firebase credentials using either the dashboard or the app settings API (available only in backend SDKs). + +##### Using the Stream Dashboard + +1. Go to the **Chat Overview** page on Stream Dashboard + +![](../assets/chat_overview_page-2fbd5bbfb70c5623bd37ff7d6c41bf4d.png) + +2. Enable **Firebase Notification** toggle on **Chat Overview** + +![](../assets/firebase_notifications_toggle-5aeabfcbdc24cb8f1fea7d41d0e845fc.png) + +3. Enter your Firebase Credentials and press "Save". + +##### Using the API + +You can also enable Firebase notifications and upload the Firebase credentials using one of our server SDKs. + +For example, using the JavaScript SDK: + +```js +const client = StreamChat.getInstance('api_key', 'api_secret'); +client.updateAppSettings({ + push_config: { + version: 'v2' + }, + firebase_config: { + credentials_json: fs.readFileSync( + './firebase-credentials.json', + 'utf-8', + ), + }); +``` +### Registering a Device With Stream Backend + +Once you configure a Firebase server key and set it up on Stream dashboard then a device that is supposed to receive push notifications needs to be registered on the Stream backend. This is usually done by listening for Firebase device token updates and passing them to the backend as follows: + +```dart +firebaseMessaging.onTokenRefresh.listen((token) { + client.addDevice(token, PushProvider.firebase); +}); +``` + +### Receiving Notifications + +Push notifications behave a bit differently depending on whether you are using iOS or Android. +See [here](https://firebase.flutter.dev/docs/messaging/usage#message-types) to understand the difference between **notification** and **data** payloads. + +#### iOS + +On iOS we send both a **notification** and a **data** payload. +This means you don't need to do anything special to get the notification to show up. However, you might want to handle the data payload to perform some logic when the user taps on the notification. + +To update the template, you can use a backend SDK. +For example, using the javascript SDK: + +```js +const client = StreamChat.getInstance(‘api_key’, ‘api_secret’); +const apn_template = `{ + "aps": { + "alert": { + "title": "New message from {{ sender.name }}", + "body": "{{ truncate message.text 2000 }}" + }, + "mutable-content": 1, + "category": "stream.chat" + }, + "stream": { + "sender": "stream.chat", + "type": "message.new", + "version": "v2", + "id": "{{ message.id }}", + "cid": "{{ channel.cid }}" + } +}`; + +client.updateAppSettings({ + firebase_config: { + apn_template, + }); +``` + +#### Android +On Android we send only a **data** payload. This gives you more flexibility and lets you decide what to do with the notification. + +For example, you can listen and generate a notification from them. + +To generate a notification when a **data-only** message is received and the app is in background: + +```dart +Future onBackgroundMessage(RemoteMessage message) async { + final chatClient = StreamChatClient(apiKey); + + chatClient.connectUser( + User(id: userId), + userToken, + connectWebSocket: false, + ); + + handleNotification(message, chatClient); +} + +void handleNotification( + RemoteMessage message, + StreamChatClient chatClient, +) async { + + final data = message.data; + + if (data['type'] == 'message.new') { + final flutterLocalNotificationsPlugin = await setupLocalNotifications(); + final messageId = data['id']; + final response = await chatClient.getMessage(messageId); + + flutterLocalNotificationsPlugin.show( + 1, + 'New message from ${response.message.user.name} in ${response.channel.name}', + response.message.text, + NotificationDetails( + android: AndroidNotificationDetails( + 'new_message', + 'New message notifications channel', + )), + ); + } +} + +FirebaseMessaging.onBackgroundMessage(onBackgroundMessage); +``` + +In the above example, you get the message details using the `getMessage` method and then you use the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) package to show the actual notification. + +##### Using a Template on Android + +It's still possible to add a **notification** payload to Android notifications. +You can do so by adding a template using a backend SDK. +For example, using the javascript SDK: + +```js +const client = StreamChat.getInstance(‘api_key’, ‘api_secret’); +const notification_template = ` +{ + "title": "{{ sender.name }} @ {{ channel.name }}", + "body": "{{ message.text }}", + "click_action": "OPEN_ACTIVITY_1", + "sound": "default" +}`; + +client.updateAppSettings({ + firebase_config: { + notification_template, + }); +``` + +### Possible Issues + +Make sure to read the [general push notification docs](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) in order to avoid known gotchas that may make your relationship with notifications difficult 😢. + +### Testing if Push Notifications are Setup Correctly + +If you're not sure whether you've set up push notifications correctly, for example, you don't always receive them, or they don’t work reliably, then you can follow these steps to make sure your config is correct and working: +1. Clone our repo for push testing: `git clone git@github.com:GetStream/chat-push-test.git` +2. `cd flutter` +3. In that folder run `flutter pub get` +4. Input your api key and secret in `lib/main.dart` +5. Change the bundle identifier/application ID and development team/user so you can run the app on your physical device.**Do not** run on an iOS simulator, as it will not work. Testing on an Android emulator is fine. +6. Add your `google-services.json/GoogleService-Info.plist` +7. Run the app +8. Accept push notification permission (iOS only) +9. Tap on `Device ID` and copy it +11. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID: +```shell +stream chat:push:test -u +``` + +You should get a test push notification 🥳 + + +### Foreground Notifications + +Sometimes you may want to show a notification when the app is in the foreground. +For example, when you're in a channel and you receive a new message from someone in another channel. + +For this scenario, you can also use the `flutter_local_notifications` package to show a notification. + +You need to listen for new events using `FirebaseMessaging.onMessage.listen()` and handle them accordingly: + +```dart +FirebaseMessaging.onMessage.listen((message) async { + handleNotification( + message, + chatClient, + ); +}); +``` + +:::note +You should also check that the channel of the message is different than the channel in the foreground. +How you do this depends on your app infrastructure and how you handle navigation. +Take a look at the [Stream Chat v1 sample app](https://github.com/GetStream/flutter-samples/blob/main/packages/stream_chat_v1/lib/home_page.dart#L11) to see how we're doing it over there. +::: + +### Saving Notification Messages to the Offline Storage (Only Android) + +When the app is closed you may want to save received messages when you receive them via a notification so that later on when you open the app they're already there. + +To do this you need to integrate the package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) in our app that exports a persistence client, see [here](https://pub.dev/packages/stream_chat_persistence#usage) how to set it up. + +Then calling `FirebaseMessaging.onBackgroundMessage(...)` you need to use a TOP-LEVEL or STATIC function to handle background messages; here is an example: + +```dart +Future onBackgroundMessage(RemoteMessage message) async { + final chatClient = StreamChatClient(apiKey); + final persistenceClient = StreamChatPersistenceClient(); + + await persistenceClient.connect(userId); + + chatClient.connectUser( + User(id: userId), + userToken, + connectWebSocket: false, + ); + + handleNotification(message, chatClient); +} + +void handleNotification( + RemoteMessage message, + StreamChatClient chatClient, +) async { + final data = message.data; + if (data['type'] == 'message.new') { + final flutterLocalNotificationsPlugin = await setupLocalNotifications(); + final messageId = data['id']; + final cid = data['cid']; + final response = await chatClient.getMessage(messageId); + await persistenceClient.updateMessages(cid, [response.message]); + + persistenceClient.disconnect(); + + flutterLocalNotificationsPlugin.show( + 1, + 'New message from ${response.message.user.name} in ${response.channel.name}', + response.message.text, + NotificationDetails( + android: AndroidNotificationDetails( + 'new_message', + 'New message notifications channel', + )), + ); + } +} + +FirebaseMessaging.onBackgroundMessage(onBackgroundMessage); +``` + diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_message_actions.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_message_actions.mdx new file mode 100644 index 000000000..c3dc2abaa --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_message_actions.mdx @@ -0,0 +1,82 @@ +--- +id: customize_message_actions +sidebar_position: 8 +title: Customize Message Actions +--- + +Customizing Message Actions + +### Introduction + +Message actions pop up in message overlay, when you long-press a message. + +![](../assets/message_actions.jpg) + +We have provided granular control over these actions. + +By default we render the following message actions: + +* edit message + +* delete message + +* reply + +* thread reply + +* copy message + +* flag message + +* pin message + +:::note +Edit and delete message are only available on messages sent by the user. +Additionally, pinning a message requires you to add the roles which are allowed to pin messages. +::: + +### Partially remove some message actions + +For example, if you only want to keep "copy message" and "delete message", +here is how to do it using the `messageBuilder` with our `MessageWidget`. + +```dart +MessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + showFlagButton: false, + showEditMessage: false, + showCopyMessage: true, + showDeleteMessage: details.isMyMessage, + showReplyMessage: false, + showThreadReplyMessage: false, + ); + }, +) +``` + +### Add a new custom message action + +The SDK also allows you to add new actions into the dialog. + +For example, let's suppose you want to introduce a new message action - "Demo Action": + +We use the `customActions` parameter of the `MessageWidget` to add extra actions. + +```dart +MessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + customActions: [ + MessageAction( + leading: Icon(Icons.add), + title: Text('Demo Action'), + onTap: (message) { + /// Complete action here + }, + ), + ], + ); + }, +) +``` diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_message_widget.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_message_widget.mdx new file mode 100644 index 000000000..af7014fb2 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_message_widget.mdx @@ -0,0 +1,182 @@ +--- +id: customize_message_widget +sidebar_position: 11 +title: Customizing The MessageWidget +--- + +Customizing Text Messages + +### Introduction + +Every application provides a unique look and feel to their own messaging interface including and not +limited to fonts, colors, and shapes. + +This guide details how to customize the `MessageWidget` in the Stream Chat Flutter UI SDK. + +### Building Custom Messages + +This guide goes into detail about the ability to customize the `MessageWidget`. However, if you want +to customize the default `MessageWidget` in the `MessageListView` provided, you can use the `.copyWith()` method +provided inside the `messageBuilder` parameter of the `MessageListView` like this: + +```dart +MessageListView( + messageBuilder: (context, details, messageList, defaultImpl) { + // Your implementation of the message here + // E.g: return Text(details.message.text ?? ''); + }, +), +``` + +### Theming + +You can customize the `MessageWidget` using the `StreamChatTheme` class, so that you can change the +message theme at the top instead of creating your own `MessageWidget` at the lower implementation level. + +There are several things you can change in the theme including text styles and colors of various elements. + +You can also set a different theme for the user's own messages and messages received by them. + +:::note +Theming allows you to change minor factors like style while using the widget directly allows you much +more customization such as replacing a certain widget with another. Some things can only be customized +through the widget and not the theme. +::: + +Here is an example: + +```dart +StreamChatThemeData( + + /// Sets theme for user's messages + ownMessageTheme: MessageThemeData( + messageBackgroundColor: colorTheme.textHighEmphasis, + ), + + /// Sets theme for received messages + otherMessageTheme: MessageThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(8), + ), + ), + +) +``` + +![](../assets/message_theming.png) + +#### Change message text style + +The `MessageWidget` has multiple `Text` widgets that you can manipulate the styles of. The three main +are the actual message text, user name, message links, and the message timestamp. + +```dart +MessageThemeData( + messageTextStyle: TextStyle(...), + createdAtStyle: TextStyle(...), + messageAuthorStyle: TextStyle(...), + messageLinksStyle: TextStyle(...), +) +``` + +![](../assets/message_styles.png) + +#### Change avatar theme + +You can change the attributes of the avatar (if displayed) using the `avatarTheme` property. + +```dart +MessageThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(8), + ), +) +``` + +![](../assets/message_rounded_avatar.png) + +#### Changing Reaction theme + +You also customize the reactions attached to every message using the theme. + +```dart +MessageThemeData( + reactionsBackgroundColor: Colors.red, + reactionsBorderColor: Colors.redAccent, + reactionsMaskColor: Colors.pink, +), +``` + +![](../assets/message_reaction_theming.png) + +### Changing Message Actions + +When a message is long pressed, the `MessageActionsModal` is shown. + +The `MessageWidget` allows showing or hiding some options if you so choose. + +```dart +MessageWidget( + ... + showUsername = true, + showTimestamp = true, + showReactions = true, + showDeleteMessage = true, + showEditMessage = true, + showReplyMessage = true, + showThreadReplyMessage = true, + showResendMessage = true, + showCopyMessage = true, + showFlagButton = true, + showPinButton = true, + showPinHighlight = true, +), +``` + +![](../assets/message_widget_actions.png) + +### Building attachments + +The `customAttachmentBuilder` property allows you to build any kind of attachment (inbuilt or custom) +in your own way. While a separate guide is written for this, it is included here because of relevance. + +```dart +MessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + customAttachmentBuilders: { + 'location': (context, message, attachments) { + final attachmentWidget = Image.network( + _buildMapAttachment( + attachments[0].extraData['latitude'], + attachments[0].extraData['longitude'], + ), + ); + + return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); + } + }, + ); + }, +), +``` + +### Widget Builders + +Some parameters allow you to construct your own widget in place of some elements in the `MessageWidget`. + +These are: +* `userAvatarBuilder` : Allows user to substitute their own widget in place of the user avatar. +* `editMessageInputBuilder` : Allows user to substitute their own widget in place of the input in edit mode. +* `textBuilder` : Allows user to substitute their own widget in place of the text. +* `bottomRowBuilder` : Allows user to substitute their own widget in the bottom of the message when not deleted. +* `deletedBottomRowBuilder` : Allows user to substitute their own widget in the bottom of the message when deleted. + +```dart +MessageWidget( + ... + textBuilder: (context, message) { + // Add your own text implementation here. + }, +), +``` diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_text_messages.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_text_messages.mdx new file mode 100644 index 000000000..a2d373330 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/customize_text_messages.mdx @@ -0,0 +1,158 @@ +--- +id: customize_text_messages +sidebar_position: 6 +title: Customize Text Messages +--- + +Customizing Text Messages + +### Introduction + +Every application provides a unique look and feel to their own messaging interface including and not +limited to fonts, colors, and shapes. + +This guide details how to customize message text in the `MessageListView` / `MessageWidget` in the +Stream Chat Flutter UI SDK. + +:::note +This guide is specifically for the `MessageListView` but if you intend to display a `MessageWidget` +separately, follow the same process without the `.copyWith` and use the default constructor instead. +::: + +### Basics of customizing a `MessageWidget` + +First, add a `MessageListView` in the appropriate place where you intend to display messages from a +channel. + +```dart +MessageListView( + ... +) +``` + +Now, we use the `messageBuilder` parameter to build a custom message. The builder function also provides +the default implementation of the `MessageWidget` so that we can change certain aspects of the widget +without redoing all of the default parameters. + +:::note +In earlier versions of the SDK, some `MessageWidget` parameters were exposed directly through the `MessageListView`, +however, this quickly becomes hard to maintain as more parameters and customizations are added to the +`MessageWidget`. Newer version utilise a cleaner interface to change the parameters by supplying a +default message implementation as aforementioned. +::: + +```dart +MessageListView( + ... + messageBuilder: (context, messageDetails, messageList, defaultWidget) { + return defaultWidget; + }, +) +``` + +We use `.copyWith()` to customize the widget: + +```dart +MessageListView( + ... + messageBuilder: (context, messageDetails, messageList, defaultWidget) { + return defaultWidget.copyWith( + ... + ); + }, +) +``` + +### Customizing text + +If you intend to simply change the theme for the text, you need not recreate the whole widget. The +`MessageWidget` has a `messageTheme` parameter that allows you to pass the theme for most aspects +of the message. + +```dart +MessageListView( + ... + messageBuilder: (context, messageDetails, messageList, defaultWidget) { + return defaultWidget.copyWith( + messageTheme: MessageTheme( + ... + messageText: TextStyle(), + ), + ); + }, +) +``` + +If you want to replace the entire text widget in the `MessageWidget`, you can use the `textBuilder` +parameter which provides a builder for creating a widget to substitute the default text.parameter + +```dart +MessageListView( + ... + messageBuilder: (context, messageDetails, messageList, defaultWidget) { + return defaultWidget.copyWith( + textBuilder: (context, message) { + return Text(message.text); + }, + ); + }, +) +``` + +### Adding Hashtags + +To add elements like hashtags, we can override the `textBuilder` in the MessageWidget: + +```dart +MessageListView( + ... + messageBuilder: (context, messageDetails, messageList, defaultWidget) { + return defaultWidget.copyWith( + textBuilder: (context, message) { + final text = _replaceHashtags(message.text).replaceAll('\n', '\\\n'); + final messageTheme = StreamChatTheme.of(context).ownMessageTheme; + + return MarkdownBody( + data: text, + onTapLink: ( + String link, + String href, + String title, + ) { + // Do something with tapped hashtag + }, + styleSheet: MarkdownStyleSheet.fromTheme( + Theme.of(context).copyWith( + textTheme: Theme.of(context).textTheme.apply( + bodyColor: messageTheme.messageText.color, + decoration: messageTheme.messageText.decoration, + decorationColor: messageTheme.messageText.decorationColor, + decorationStyle: messageTheme.messageText.decorationStyle, + fontFamily: messageTheme.messageText.fontFamily, + ), + ), + ).copyWith( + a: messageTheme.messageLinks, + p: messageTheme.messageText, + ), + ); + }, + ); + }, +) + +String _replaceHashtags(String text) { + RegExp exp = new RegExp(r"\B#\w\w+"); + exp.allMatches(text).forEach((match){ + text = text.replaceAll( + '${match.group(0)}', '[${match.group(0)}](${match.group(0).replaceAll(' ', '')})'); + }); + return text; +} +``` + +We can replace the hashtags using RegEx and add links for the MarkdownBody which is done here in the +`_replaceHashtags()` function. +Inside the textBuilder, we use the `flutter_markdown` package to build our hashtags as links. + +![](../assets/hashtag_example.jpg) diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/end_to_end_chat_encryption.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/end_to_end_chat_encryption.mdx new file mode 100644 index 000000000..8a85c6a50 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/end_to_end_chat_encryption.mdx @@ -0,0 +1,258 @@ +--- +id: end_to_end_chat_encryption +sidebar_position: 12 +title: End To End Chat Encryption +--- + +## Introduction + +When you communicate over a chat application with another person or group, +you may exchange sensitive information, like personally identifiable information, financial details, or passwords. +A chat application should use end-to-end encryption to ensure that users' data stays secure. + +:::note +Before you start, keep in mind that this guide is a basic example intended for educational purposes only. +If you want to implement end-to-end encryption in your production app, please consult a security professional first. +There’s a lot more to consider from a security perspective that isn’t covered here. +::: + +## What is End-to-End Encryption? + +End-to-end encryption (E2EE) is the process of securing a message from third parties so that only the sender and receiver can access the message. +E2EE provides security by storing the message in an encrypted form on the application's server or database. + +You can only access the message by decrypting and signing it using a known public key (distributed freely) +and a corresponding private key (only known by the owner). + +Each user in the application has their own public-private key pair. +Public keys are distributed publicly and encrypt the sender’s messages. +The receiver can only decrypt the sender’s message with the matching private key. + +Check out the diagram below for an example: + +![](../assets/end_to_end_encryption.png) + +## Setup + +### Dependencies + +Add the [webcrypto](https://pub.dev/packages/webcrypto) package in your `pubspec.yaml` file. + +```yaml +dependencies: + webcrypto: ^0.5.2 # latest version +``` + +### Generate Key Pair + +Write a function that generates a key pair using the **ECDH** algorithm and the **P-256** elliptic curve (**P-256** is well-supported and +offers the right balance of security and performance). + +The pair will consist of two keys: +- **PublicKey**: The key that is linked to a user to encrypt messages. +- **PrivateKey**: The key that is stored locally to decrypt messages. + +```dart +Future generateKeys() async { + final keyPair = await EcdhPrivateKey.generateKey(EllipticCurve.p256); + final publicKeyJwk = await keyPair.publicKey.exportJsonWebKey(); + final privateKeyJwk = await keyPair.privateKey.exportJsonWebKey(); + + return JsonWebKeyPair( + privateKey: json.encode(privateKeyJwk), + publicKey: json.encode(publicKeyJwk), + ); +} + +// Model class for storing keys +class JsonWebKeyPair { + const JsonWebKeyPair({ + required this.privateKey, + required this.publicKey, + }); + + final String privateKey; + final String publicKey; +} +``` + +### Generate a Crypto Key + +Next, create a symmetric **Crypto Key** using the keys generated in the previous step. +You will use those keys to encrypt and decrypt messages. + +```dart +// SendersJwk -> sender.privateKey +// ReceiverJwk -> receiver.publicKey +Future> deriveKey(String senderJwk, String receiverJwk) async { + // Sender's key + final senderPrivateKey = json.decode(senderJwk); + final senderEcdhKey = await EcdhPrivateKey.importJsonWebKey( + senderPrivateKey, + EllipticCurve.p256, + ); + + // Receiver's key + final receiverPublicKey = json.decode(receiverJwk); + final receiverEcdhKey = await EcdhPublicKey.importJsonWebKey( + receiverPublicKey, + EllipticCurve.p256, + ); + + // Generating CryptoKey + final derivedBits = await senderEcdhKey.deriveBits(256, receiverEcdhKey); + return derivedBits; +} +``` + +### Encrypting Messages + +Once you have generated the **Crypto Key**, you're ready to encrypt the message. +You can use the **AES-GCM** algorithm for its known security and performance balance and good browser availability. + +```dart +// The "iv" stands for initialization vector (IV). To ensure the encryption’s strength, +// each encryption process must use a random and distinct IV. +// It’s included in the message so that the decryption procedure can use it. +final Uint8List iv = Uint8List.fromList('Initialization Vector'.codeUnits); +``` + +```dart +Future encryptMessage(String message, List deriveKey) async { + // Importing cryptoKey + final aesGcmSecretKey = await AesGcmSecretKey.importRawKey(deriveKey); + + // Converting message into bytes + final messageBytes = Uint8List.fromList(message.codeUnits); + + // Encrypting the message + final encryptedMessageBytes = + await aesGcmSecretKey.encryptBytes(messageBytes, iv); + + // Converting encrypted message into String + final encryptedMessage = String.fromCharCodes(encryptedMessageBytes); + return encryptedMessage; +} +``` + +### Decrypting Messages + +Decrypting a message is the opposite of encrypting one. +To decrypt a message to a human-readable format, use the code snippet below: + +```dart +Future decryptMessage(String encryptedMessage, List deriveKey) async { + // Importing cryptoKey + final aesGcmSecretKey = await AesGcmSecretKey.importRawKey(deriveKey); + + // Converting message into bytes + final messageBytes = Uint8List.fromList(encryptedMessage.codeUnits); + + // Decrypting the message + final decryptedMessageBytes = + await aesGcmSecretKey.decryptBytes(messageBytes, iv); + + // Converting decrypted message into String + final decryptedMessage = String.fromCharCodes(decryptedMessageBytes); + return decryptedMessage; +} +``` + +## Implement as a Stream Chat Feature + +Now that your setup is complete you can use it to implement end-to-end encryption in your app. + +### Store User's Public Key + +The first thing you need to do is store the generated `publicKey` as an `extraData` property, in order +for other users to encrypt messages. + +```dart +// Generating keyPair using the function defined in above steps +final keyPair = generateKeys(); +``` + +```dart +await client.connectUser( + User( + id: 'cool-shadow-7', + name: 'Cool Shadow', + image: 'https://getstream.io/cool-shadow', + + // set publicKey as a extraData property + extraData: { 'publicKey': keyPair.publicKey }, + ), + client.devToken('cool-shadow-7').rawValue, +); +``` + +### Sending Encrypted Messages + +Now you will use the `encryptMessage()` function created in the previous steps to encrypt the message. + +To do that, you need to make some minor changes to the **MessageInput** widget. + +```dart +final receiverJwk = receiver.extraData['publicKey']; + +// Generating derivedKey using user's privateKey and receiver's publicKey +final derivedKey = await deriveKey(keyPair.privateKey, receiverJwk); +``` + +```dart +MessageInput( + + ... + + preMessageSending: (message) async { + // Encrypting the message text using derivedKey + final encryptedMessage = await encryptMessage(message.text, derivedKey); + + // Creating a new message with the encrypted message text + final newMessage = message.copyWith(text: encryptedMessage); + + return newMessage; + }, +), +``` + +`preMessageSending` is a parameter that allows your app to process the message before it goes to Stream’s server. +Here, you have used it to encrypt the message before sending it to Stream’s backend. + +### Showing Decrypted Messages + +Now, it’s time to decrypt the message and present it in a human-readable format to the receiver. + +You can customize the **MessageListView** widget to have a custom `messagebuilder`, that can decrypt the message. + +```dart +MessageListView( + ... + messageBuilder: (context, messageDetails, currentMessages, defaultWidget) { + // Retrieving the message from details + final message = messageDetails.message; + + // Decrypting the message text using the derivedKey + final decryptedMessageFuture = decryptMessage(message.text, derivedKey); + return FutureBuilder( + future: decryptedMessageFuture, + builder: (context, snapshot) { + if (snapshot.hasError) return Text('Error: ${snapshot.error}'); + if (!snapshot.hasData) return Container(); + + // Updating the original message with the decrypted text + final decryptedMessage = message.copyWith(text: snapshot.data); + + // Returning defaultWidget with updated message + return defaultWidget.copyWith( + message: decryptedMessage, + ); + }, + ); + }, +), +``` + +That's it! That's all you need to implement E2EE in a Stream powered chat app. + +For more details, check out our [end-to-end encrypted chat article](https://getstream.io/blog/end-to-end-encrypted-chat-in-flutter/#whats-end-to-end-encryption). diff --git a/docusaurus/docs/Flutter/guides/migration_guide_2_0.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/migration_guide_2_0.mdx similarity index 100% rename from docusaurus/docs/Flutter/guides/migration_guide_2_0.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/migration_guide_2_0.mdx diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/understanding_filters.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/understanding_filters.mdx new file mode 100644 index 000000000..292afd072 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/understanding_filters.mdx @@ -0,0 +1,194 @@ +--- +id: understanding_filters +sidebar_position: 10 +title: Understanding Filters +--- + +Understanding Filters + +### Introduction + +Filters are used to get a specific subset of objects (channels, users, messages, members, etc) which +fit the conditions specified. Earlier versions of the SDK contained String-based filters which are now replaced by type-safe +filters. This guide aims to explain the different types of filters and how to use them. + +### Types Of Filters + +#### Filter.equal + +The 'equal' filter gets the objects where the given key has the specified value. + +```dart +Filter.equal('type', 'messaging'), +``` + +#### Filter.notEqual + +The 'notEqual' filter gets the objects where the given key does not have the specified value. + +```dart +Filter.notEqual('type', 'messaging'), +``` + +#### Filter.greater + +The 'greater' filter gets the objects where the given key has a higher value than the specified value. + +```dart +Filter.greater('count', 5), +``` + +#### Filter.greaterOrEqual + +The 'greaterOrEqual' filter gets the objects where the given key has an equal or higher value than the specified value. + +```dart +Filter.greaterOrEqual('count', 5), +``` + +#### Filter.less + +The 'less' filter gets the objects where the given key has a lesser value than the specified value. + +```dart +Filter.less('count', 5), +``` + +#### Filter.lessOrEqual + +The 'lessOrEqual' filter gets the objects where the given key has a lesser or equal value than the specified value. + +```dart +Filter.lessOrEqual('count', 5), +``` + +#### Filter.in_ + +The 'in_' filter allows getting objects where the key matches any in a specified array. + +```dart +Filter.in_('members', [user.id]) +``` + +:::note +Since 'in' is a keyword in Dart, the filter has an underscore added. This does not apply to the 'notIn' +keyword. +::: + +#### Filter.notIn + +The 'notIn' filter allows getting objects where the key matches none in a specified array. + +```dart +Filter.notIn('members', [user.id]) +``` + +#### Filter.query + +The 'query' filter matches values by performing text search with the specified value. + +```dart +Filter.query('name', 'demo') +``` + +#### Filter.autoComplete + +The 'autoComplete' filter matches values with the specified prefix. + +```dart +Filter.autoComplete('name', 'demo') +``` + +#### Filter.exists + +The 'exists' filter matches values that exist, or don't exist, based on the specified boolean value. + +```dart +Filter.exists('name') +``` + +#### Filter.notExists + +The 'notExists' filter checks if the specified key doesn't exist. This is a simplified call to `Filter.exists` +with the value set to false. + +```dart +Filter.notExists('name') +``` + +#### Filter.contains + +The 'contains' filter matches any list that contains the specified value. + +```dart +Filter.contains('teams', 'red') +``` + +#### Filter.empty + +The 'empty' filter constructor returns an empty filter. It's the equivalent of an empty map `{}`; + +```dart +Filter.empty(); +``` + +#### Filter.raw + +The 'raw' filter constructor lets you specify a raw filter. We suggest using this only if you can't manage to build what you want using the other constructors. + +```dart +Filter.raw(value: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.currentUser!.id, + ], + 'distinct': true, +}); +``` + +#### Filter.custom + +The 'custom' filter is used to create a custom filter in case it does not exists or it's not been added to the SDK yet. +Note that the filter must be supported by the Stream backend in order to work. + +```dart +Filter.custom( + operator: '\$max', + value: 10, +) +``` + +### Group Queries + +#### Filter.and + +The 'and' operator combines multiple queries. + +```dart +final filter = Filter.and([ + Filter.equal('type', 'messaging'), + Filter.in_('members', [user.id]) +]) +``` + +#### Filter.or + +Combines the provided filters and matches the values matched by at least one of the filters. + +```dart +final filter = Filter.or([ + Filter.in_('bannedUsers', [user.id]), + Filter.in_('shadowBannedUsers', [user.id]) +]) +``` + +#### Filter.nor + +Combines the provided filters and matches the values not matched by all the filters. + +```dart +final filter = Filter.nor([ + Filter.in_('bannedUsers', [user.id]), + Filter.in_('shadowBannedUsers', [user.id]) +]) +``` diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/user_token_generation_with_firebase_auth.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/user_token_generation_with_firebase_auth.mdx new file mode 100644 index 000000000..56192f537 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/user_token_generation_with_firebase_auth.mdx @@ -0,0 +1,448 @@ +--- +id: token_generation_with_firebase +sidebar_position: 5 +title: User Token Generation With Firebase Auth and Cloud Functions +--- + +Securely generate Stream Chat user tokens using Firebase Authentication and Cloud Functions. + +:::note +This guide assumes that you are familiar with Firebase Authentication and Cloud Functions for Flutter and using the Flutter Stream Chat SDK. +::: + +### Introduction + +In this guide, you'll explore how you can use Firebase Auth as an authentication provider and create Firebase Cloud functions to securely +generate Stream Chat user tokens. + +You will use Stream's [NodeJS client](https://getstream.io/chat/docs/node/?language=javascript) for Stream account creation and +token generation, and [Flutter Cloud Functions for Firebase](https://firebase.flutter.dev/docs/functions/overview) to invoke the cloud functions +from your Flutter app. + +Stream supports several different [backend clients](https://getstream.io/chat/sdk/#backend-clients) to integrate with your server. This guide only shows an easy way to integrate Stream Chat authentication using Firebase and Flutter. + +### Flutter Firebase + +See the [Flutter Firebase getting started](https://firebase.flutter.dev/docs/overview) docs for setup and installation instructions. + +You will also need to add the [Flutter Firebase Authentication](https://firebase.flutter.dev/docs/auth/overview), and [Flutter Firebase Cloud Functions](https://firebase.flutter.dev/docs/functions/overview) packages to your app. Depending on the platform that you target, there may be specific configurations that you need to do. + +#### Starting Code + +The following code shows a basic application with **FirebaseAuth** and **FirebaseFunctions**. + +You will extend this later to execute cloud functions. + +```dart +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'dart:async'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Auth(), + ), + ); + } +} + +class Auth extends StatefulWidget { + Auth({Key? key}) : super(key: key); + + @override + _AuthState createState() => _AuthState(); +} + +class _AuthState extends State { + late FirebaseAuth auth; + late FirebaseFunctions functions; + + @override + void initState() { + super.initState(); + auth = FirebaseAuth.instance; + functions = FirebaseFunctions.instance; + } + + final email = 'test@getstream.io'; + final password = 'password'; + + Future createAccount() async { + // Create Firebase account + await auth.createUserWithEmailAndPassword(email: email, password: password); + print('Firebase account created'); + } + + Future signIn() async { + // Sign in with Firebase + await auth.signInWithEmailAndPassword(email: email, password: password); + print('Firebase signed in'); + } + + Future signOut() async { + // Revoke Stream chat token. + final callable = functions.httpsCallable('revokeStreamUserToken'); + await callable(); + print('Stream user token revoked'); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AuthenticationState( + streamUser: auth.authStateChanges(), + ), + ElevatedButton( + onPressed: createAccount, + child: Text('Create account'), + ), + ElevatedButton( + onPressed: signIn, + child: Text('Sign in'), + ), + ElevatedButton( + onPressed: signOut, + child: Text('Sign out'), + ), + ], + ), + ); + } +} + +class AuthenticationState extends StatelessWidget { + const AuthenticationState({ + Key? key, + required this.streamUser, + }) : super(key: key); + + final Stream streamUser; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: streamUser, + builder: (context, snapshot) { + if (snapshot.hasData) { + return (snapshot.data != null) + ? Text('Authenticated') + : Text('Not Authenticated'); + } + return Text('Not Authenticated'); + }, + ); + } +} + +``` + +Running the above will give this: + +![](../assets/authentication_demo_app.jpg) + +The `Auth` widget handles all of the authentication logic. It initializes a `FirebaseAuth.instance` and uses that +in the `createAccount`, `signIn` and `signOut` methods. There is a button to envoke each of these methods. + +The `FirebaseFunctions.instance` will be used later in this guide. + +The `AuthenticationState` widget listens to `auth.authStateChanges()` to display a message +indicating if a user is authenticated. + +### Firebase Cloud Functions + +Firebase Cloud Functions allows you to extend Firebase with custom operations that an event can trigger: +- **Internal event**: For example, when creating a new Firebase account this is automatically triggered. +- **External event**: For example, directly calling a cloud function from your Flutter application. + +To set up your local environment to deploy cloud functions, please see the +[Cloud Functions getting started](https://firebase.flutter.dev/docs/overview) docs. + +After initializing your project with cloud functions, you should have a **functions** folder in your project, including a `package.json` file. + +There should be two dependencies already added, **firebase-admin** and **firebase-functions**. You will also need to add the **stream-chat** dependency. + +Navigate to the **functions** folder and run `npm install stream-chat --save-prod`. + +This will install the node module and add it as a dependency to `package.json`. + +Now open `index.js` and add the following (this is the complete example): + +```js +const StreamChat = require('stream-chat').StreamChat; +const functions = require("firebase-functions"); +const admin = require("firebase-admin"); + +admin.initializeApp(); + +const serverClient = StreamChat.getInstance(functions.config().stream.key, functions.config().stream.secret); + + +// When a user is deleted from Firebase their associated Stream account is also deleted. +exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => { + return serverClient.deleteUser(user.uid); +}); + +// Create a Stream user and return auth token. +exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + // Create user using the serverClient. + await serverClient.upsertUser({ + id: context.auth.uid, + name: context.auth.token.name, + email: context.auth.token.email, + image: context.auth.token.image, + }); + + /// Create and return user auth token. + return serverClient.createToken(context.auth.uid); + } catch (err) { + console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not create Stream user"); + } + } +}); + +// Get Stream user token. +exports.getStreamUserToken = functions.https.onCall((data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + return serverClient.createToken(context.auth.uid); + } catch (err) { + console.error(`Unable to get user token with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not get Stream user"); + } + } +}); + +// Revoke the authenticated user's Stream chat token. +exports.revokeStreamUserToken = functions.https.onCall((data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + return serverClient.revokeUserToken(context.auth.uid); + } catch (err) { + console.error(`Unable to revoke user token with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not get Stream user"); + } + } +}); + +``` + +First, you import the necessary packages and call `admin.initializeApp();` to set up Firebase cloud functions. + +Next, you initialize the **StreamChat** server client by calling `StreamChat.getInstance`. This function requires your Stream app's +**token** and **secret**. You can get this from the Stream Dashboard for your app. + +Set these values as environment data on Firebase Functions. + +```bash + firebase functions:config:set stream.key="app-key" stream.secret="app-secret" +``` + +*Replace **app-key** and **app-secret** with the values for your Stream app.* + +This creates an object of **stream** with properties **key** and **secret**. To access this environment +data use `functions.config().stream.key` and `functions.config().stream.secret`. + +See the [Firebase environment configuration](https://firebase.google.com/docs/functions/config-env) +documentation for additional information. + +To deploy these functions to Firebase, run: + +```bash +firebase deploy --only functions +``` + +### Create a Stream User and Get the User's Token + +In the `createStreamUserAndGetToken` cloud function you create an `onCall` HTTPS handler, which exposes +a cloud function that can be envoked from your Flutter app. + +```js +// Create a Stream user and return auth token. +exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + // Create user using the serverClient. + await serverClient.upsertUser({ + id: context.auth.uid, + name: context.auth.token.name, + email: context.auth.token.email, + image: context.auth.token.image, + }); + + /// Create and return user auth token. + return serverClient.createToken(context.auth.uid); + } catch (err) { + console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not create Stream user"); + } + } +}); +``` + +This function first does a check to see that the client that calls it is authenticated, +by ensuring that `context.auth` is not null. If it is null, then it throws an `HttpsError` with a descriptive +message. This error can be caught in your Flutter application. + +If the caller is authenticated the function proceeds to use the `serverClient` to create a new Stream Chat +user by calling the `upsertUser` method and passing in some user data. It uses the authenticated caller's **uid** as an **id**. + +After the user is created it generates a token for that user. This token is then returned to the caller. + +To call this from Flutter, you will need to use the `cloud_functions` package. + +Update the **createAccount** method in your Flutter code to the following: + +```dart +Future createAccount() async { + // Create Firebase account + await auth.createUserWithEmailAndPassword(email: email, password: password); + print('Firebase account created'); + + // Create Stream user and get token + final callable = functions.httpsCallable('createStreamUserAndGetToken'); + final results = await callable(); + print('Stream account created, token: ${results.data}'); +} +``` + +Calling this method will do the following: +1. Create a new Firebase User and authenticate that user. +2. Call the `createStreamUserAndGetToken` cloud function and get the Stream user token for the authenticated user. + +As you can see, calling a cloud function is easy and will also send all the necessary user authentication information (such as the UID) +in the request. + +Once you have the Stream user token, you can authenticate your Stream Chat user as you normally would. + +Please see our [initialization documention](https://getstream.io/chat/docs/flutter-dart/init_and_users/?language=dart) for more information. + +As you can see below, the User ID matches on both Firebase's and Stream's user database. + +##### Firebase Authentication Database + +![Firebase Auth Database with new user created](../assets/firebase_authentication_dashboard.jpg) + +##### Stream Chat User Database + +![Stream chat user database new account created](../assets/stream_chat_user_database.jpg) + + +### Get the Stream User Token + +The `getStreamUserToken` cloud function is very similar to the `createStreamUserAndGetToken` function. The only difference is +that it only creates a user token and does not create a new user account on Stream. + +Update the **signIn** method in your Flutter code to the following: + +```dart +Future signIn() async { + // Sign in with Firebase + await auth.signInWithEmailAndPassword(email: email, password: password); + print('Firebase signed in'); + + // Get Stream user token + final callable = functions.httpsCallable('getStreamUserToken'); + final results = await callable(); + print('Stream user token retrieved: ${results.data}'); +} +``` + +Calling this method will do the following: +1. Sign in using Firebase Auth. +2. Call the `getStreamUserToken` cloud function to get a Stream user token. + +:::note +The user needs to be authenticated to call this cloud function. Otherwise, the function will throw +the **failed-precondition** error that you specified. +::: + +### Revoke Stream User Token + +You may also want to revoke the Stream user token if you sign out from Firebase. + +Update the `signOut` method in your Flutter code to the following: + +```dart +Future signOut() async { + // Revoke Stream user token. + final callable = functions.httpsCallable('revokeStreamUserToken'); + await callable(); + print('Stream user token revoked'); + + // Sign out Firebase. + await auth.signOut(); + print('Firebase signed out'); +} +``` +:::note +Call the cloud function before signing out from Firebase. +::: + +### Delete Stream User + +When deleting a Firebase user account, it would make sense also to delete the +associated Stream user account. + +The cloud function looks like this: + +```js +// When a user is deleted from Firebase their associated Stream account is also deleted. +exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => { + return serverClient.deleteUser(user.uid); +}); +``` + +In this function, you are listening to delete events on Firebase auth. When an account is deleted, this function will be triggered, and you can get the +user's **uid** and call the `deleteUser` method on the `serverClient`. + +This is not an external cloud function; it can only be triggered when an +account is deleted. + +### Conclusion + +In this guide, you have seen how to securely create Stream Chat tokens using +Firebase Authentication and Cloud Functions. + +The principles shown in this guide can be applied to your preferred authentication +provider and cloud architecture of choice. \ No newline at end of file diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/_category_.json b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/_category_.json new file mode 100644 index 000000000..242afb7be --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Stream Chat Flutter", + "position": 3 +} \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/channel_header.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/channel_header.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/channel_header.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/channel_header.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_header.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/channel_list_header.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/channel_list_header.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/channel_list_header.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/channel_list_view.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/channel_list_view.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/channel_list_view.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/channel_list_view.mdx diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/introduction.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/introduction.mdx new file mode 100644 index 000000000..28640483e --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/introduction.mdx @@ -0,0 +1,18 @@ +--- +id: introduction +sidebar_position: 1 +title: Introduction +--- + +Understanding The UI Package Of The Flutter SDK + +### What function does `stream_chat_flutter` serve? + +The UI SDK (`stream_chat_flutter`) contains official Flutter components for Stream Chat, a service for building chat applications. + +While the Stream Chat service provides the backend for messaging and the LLC provides an easy way to +use it in your Flutter apps, we wanted to make sure that adding Chat functionality to your app was as quick as possible. + +The UI package is built on top of the low-level client and the core package and allows you to build a +full fledged app with either the inbuilt components, modify existing components, or easily add widgets +of your own to match your app's style better. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_input.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_input.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/message_input.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_input.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_list_view.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_list_view.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/message_list_view.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_list_view.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_search_list_view.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_search_list_view.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_widget.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_widget.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/message_widget.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/message_widget.mdx diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/setup.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/setup.mdx new file mode 100644 index 000000000..22e5c6c54 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/setup.mdx @@ -0,0 +1,48 @@ +--- +id: setup +sidebar_position: 2 +title: Setup +--- + +Understanding Setup For `stream_chat_flutter` + +### Add pub.dev dependency + +First, you need to add the `stream_chat_flutter` dependency to your `pubspec.yaml`. + +You can either run this command: + +```shell +flutter pub add stream_chat_flutter +``` + +OR + +Add this line in the dependencies section of your pubspec.yaml after substituting latest version: + +```yaml +dependencies: + stream_chat_flutter: ^latest_version +``` + +You can find the package details on [pub.dev](https://pub.dev/packages/stream_chat_flutter). + +### Details On Platform Support + +`stream_chat_flutter` was originally created for Android and iOS mobile platforms. As Flutter matured, +support for additional platforms was added and the package now has experimental support for web and desktop as +[detailed here](https://getstream.io/blog/announcing-experimental-multi-platform-support-for-the-stream-flutter-sdk/). + +However, platforms other than mobile may have additional constraints due to not supporting all plugins, +which will be addressed by the respective plugin creators over time. + +### Setup: iOS + +The library uses [flutter file picker plugin](https://github.com/miguelpruivo/flutter_file_picker) to pick files from the os. +Follow [this wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki/Setup#ios) to fulfill iOS requirements. + +We also use [video_player](https://pub.dev/packages/video_player) to reproduce videos. +Follow [this guide](https://pub.dev/packages/video_player#installation) to fulfill the requirements. + +To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin. +Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements. diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/stream_chat_and_theming.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/stream_chat_and_theming.mdx new file mode 100644 index 000000000..d527b738a --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/stream_chat_and_theming.mdx @@ -0,0 +1,71 @@ +--- +id: stream_chat_and_theming +sidebar_position: 3 +title: StreamChat And Theming +--- + +Understanding How To Customize Widgets Using `StreamChatTheme` + +Find the pub.dev documentation [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatTheme-class.html) and [here](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatThemeData-class.html) + +### Background + +Stream's UI SDK makes it easy for developers to add custom styles and attributes to our widgets. Like most Flutter frameworks, Stream exposes a dedicated widget for theming. + +Using `StreamChatTheme`, users can customize most aspects of our UI widgets by setting attributes using `StreamChatThemeData`. + +Similar to the `Theme` and `ThemeData` in Flutter, Stream Chat uses a top level [inherited widget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) to provide theming information throughout your application. This can be optionally set at the top of your application tree or at a localized point in your widget sub-tree. + +If you'd like to customize the look and feel of Stream chat across your entire application, we recommend setting your theme at the top level. Conversely, users can customize specific screens or widgets by wrapping components in a `StreamChatTheme`. + +### A closer look at StreamChatThemeData + +Looking at the constructor for `StreamChatThemeData`, we can see the full list of properties and widgets available for customization. + +Some high-level properties such as `textTheme` or `colorTheme` can be set application-wide directly from this class. In contrast, larger components such as `ChannelHeader`, `MessageInputs`, etc. have been broken up into smaller theme objects. + +```dart +factory StreamChatThemeData({ + Brightness? brightness, + TextTheme? textTheme, + ColorTheme? colorTheme, + ChannelListHeaderTheme? channelListHeaderTheme, + ChannelPreviewTheme? channelPreviewTheme, + ChannelTheme? channelTheme, + MessageTheme? otherMessageTheme, + MessageTheme? ownMessageTheme, + MessageInputTheme? messageInputTheme, + Widget Function(BuildContext, Channel)? defaultChannelImage, + Widget Function(BuildContext, User)? defaultUserImage, + IconThemeData? primaryIconTheme, + List? reactionIcons, + }); +``` + +### Stream Chat Theme in use + +Let's take a look at customizing widgets using `StreamChatTheme`. In the example below, we can change the default color theme to yellow and override the channel header's typography and colors. + +```dart +builder: (context, child) => StreamChat( + client: client, + child: child, + streamChatThemeData: StreamChatThemeData( + colorTheme: ColorTheme.light( + primaryAccent: const Color(0xffffe072), + ), + channelTheme: ChannelTheme( + channelHeaderTheme: ChannelHeaderTheme( + color: const Color(0xffd34646), + title: TextStyle( + color: Colors.white, + ), + ), + ), + ), + ), +``` + +We are creating this class at the very top of our widget tree using the `streamChatThemeData` parameter found in the `StreamChat` widget. + +![](../assets/using_theme.jpg) diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/user_list_view.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/user_list_view.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter/user_list_view.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter/user_list_view.mdx diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/_category_.json b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/_category_.json new file mode 100644 index 000000000..8d738e893 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Stream Chat Flutter Core", + "position": 4 +} \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/channel_list_core.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/channel_list_core.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter_core/channel_list_core.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/channel_list_core.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/channels_bloc.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/channels_bloc.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter_core/channels_bloc.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/channels_bloc.mdx diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/introduction.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/introduction.mdx new file mode 100644 index 000000000..3c6a70f2b --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/introduction.mdx @@ -0,0 +1,80 @@ +--- +id: introduction +sidebar_position: 1 +title: Introduction +--- + +Understanding The Core Package Of The Flutter SDK + +This package provides business logic to fetch common things required for integrating Stream Chat into your application. +The core package allows more customisation and hence provides business logic but no UI components. +Please use the `stream_chat_flutter` package for the full fledged suite of UI components or `stream_chat` for the low-level client. + +### Background + +In the early days of the Flutter SDK, the SDK was only split into the LLC (`stream_chat`) and +the UI package (`stream_chat_flutter`). With this you could use a fully built interface with the UI package +or a fully custom interface with the LLC. However, we soon recognised the need for a third intermediary +package which made tasks like building and modifying a list of channels or messages easy but without +the complexity of using low level components. The Core package (`stream_chat_flutter_core`) is a manifestation +of the same idea and allows you to build an interface with Stream Chat without having to deal with +low level code and architecture as well as implementing your own theme and UI effortlessly. +Also, it has very few dependencies. + +We will now explore the components of this intermediary package and understand how it helps you build +the experience you want your users to have. + +The package primarily contains three types of classes: + +* Business Logic Components +* Core Components +* Core Controllers + +### Business Logic Components + +These components allow you to have the maximum and lower-level control of the queries being executed. + +In BLoCs, the basic functionalities - such as queries for messages, channels or queries - are bundled up +and passed along down the tree. Using a BLoC allows you to either create your own way to fetch and +build UIs or use an inbuilt Core widget to do the work such as queries, pagination, etc for you. + +The BLoCs we provide are: + +* ChannelsBloc +* MessageSearchBloc +* UsersBloc + +### Core Components + +Core components usually are an easy way to fetch data associated with Stream Chat. +Core components use functions exposed by the respective BLoCs (for example the ChannelListCore uses the ChannelsBloc) +and use the respective controllers for various operations. Unlike heavier components from the UI +package, core components are decoupled from UI and they expose builders instead to help you build +a fully custom interface. + +Data fetching can be controlled with the controllers of the respective core components. + +* ChannelListCore (Fetch a list of channels) +* MessageListCore (Fetch a list of messages from a channel) +* MessageSearchListCore (Fetch a list of search messages) +* UserListCore (Fetch a list of users) +* StreamChatCore (This is different from the other core components - it is a version of StreamChat decoupled from theme and initialisations.) + +### Core Controllers + +Core Controllers are supplied to respective CoreList widgets which allows reloading and pagination of data whenever needed. + +Unlike the UI package, the Core package allows a fully custom user interface built with the data. This +in turn provides a few challenges: we do not know implicitly when to paginate your list or reload your data. + +While this is handled out of the box in the UI package since the List implementation is inbuilt, a controller +needs to be used in the core package notifying the core components to reload or paginate the data existing +currently. For this, each core component has a respective controller which you can use to call the +specific function (reload / paginate) whenever such an event is triggered through / needed in your UI. + +* ChannelListController +* MessageListController +* MessageSearchListController +* ChannelListController + +This section goes into the individual core package widgets and their functional use. diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/message_list_core.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/message_list_core.mdx new file mode 100644 index 000000000..5fd5877ad --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/message_list_core.mdx @@ -0,0 +1,70 @@ +--- +id: message_list_core +sidebar_position: 5 +title: MessageListCore +--- + +A Widget For Building A List Of Messages + +### Background + +The UI SDK of Stream Chat supplies a `MessageListView` class that builds a list of channels fetching +according to the filters and sort order given. However, in some cases, implementing novel UI is necessary +that cannot be done using the customization approaches given in the widget. + +To do this, we extracted the logic required for fetching channels into a 'Core' widget - a widget that +fetches channels in the expected way via the usual params but does not supply any UI and instead +exposes builders to build the UI in situations such as loading, empty data, errors, and on data received. + +### Basic Example + +`MessageListCore` is a simplified class that allows fetching a list of +messages while exposing UI builders. + +This allows you to construct your own UI while not having to +worry about the specific logic of fetching messages in a channel. + +A `MessageListController` is used to paginate data. + +```dart +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: MessageListCore( + emptyBuilder: (context) { + return Center( + child: Text('Nothing here...'), + ); + }, + loadingBuilder: (context) { + return Center( + child: CircularProgressIndicator(), + ); + }, + messageListBuilder: (context, list) { + return MessagesPage(list); + }, + errorWidgetBuilder: (context, err) { + return Center( + child: Text('Error'), + ); + }, + ), + ), + ], + ), + ); + } +} +``` + +Make sure to have a `StreamChannel` ancestor in order to provide the +information about the channels. \ No newline at end of file diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_bloc.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/message_search_bloc.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_bloc.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/message_search_bloc.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/message_search_list_core.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/message_search_list_core.mdx diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/setup.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/setup.mdx new file mode 100644 index 000000000..15b15a565 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/setup.mdx @@ -0,0 +1,28 @@ +--- +id: setup +sidebar_position: 2 +title: Setup +--- + +Understanding Setup For `stream_chat_flutter_core` + +### Add pub.dev dependency + +First, you need to add the `stream_chat_flutter_core` dependency to your pubspec.yaml + +You can either run this command: + +```shell +flutter pub add stream_chat_flutter_core +``` + +OR + +Add this line in the dependencies section of your pubspec.yaml after substituting latest version: + +```yaml +dependencies: + stream_chat_flutter_core: ^latest_version +``` + +You can find the package details on [pub.dev](https://pub.dev/packages/stream_chat_flutter_core). diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/stream_chat_core.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/stream_chat_core.mdx new file mode 100644 index 000000000..2a43ae522 --- /dev/null +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/stream_chat_core.mdx @@ -0,0 +1,26 @@ +--- +id: stream_chat_core +sidebar_position: 3 +title: StreamChatCore +--- + +`StreamChatCore` is a version of `StreamChat` found in `stream_chat_flutter` that is decoupled from +theme and initialisations. + +`StreamChatCore` is used to provide information about the chat client to the widget tree. +This Widget is used to react to life cycle changes and system updates. +When the app goes into the background, the websocket connection is automatically closed and when it goes back to foreground the connection is opened again. + +Like the `StreamChat` widget in the higher level UI package, the `StreamChatCore` widget should +be on the top level before using any Stream functionality: + +```dart +return MaterialApp( + title: 'Stream Chat Core Example', + home: HomeScreen(), + builder: (context, child) => StreamChatCore( + client: client, + child: child!, + ), + ); +``` diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/user_list_core.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/user_list_core.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter_core/user_list_core.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/user_list_core.mdx diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/users_bloc.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/users_bloc.mdx similarity index 100% rename from docusaurus/docs/Flutter/stream_chat_flutter_core/users_bloc.mdx rename to docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/stream_chat_flutter_core/users_bloc.mdx diff --git a/docusaurus/flutter_versioned_sidebars/version-3.x.x-sidebars.json b/docusaurus/flutter_versioned_sidebars/version-3.x.x-sidebars.json new file mode 100644 index 000000000..6a5949a0e --- /dev/null +++ b/docusaurus/flutter_versioned_sidebars/version-3.x.x-sidebars.json @@ -0,0 +1,8 @@ +{ + "version-3.x.x/defaultSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] + } \ No newline at end of file diff --git a/docusaurus/flutter_versions.json b/docusaurus/flutter_versions.json new file mode 100644 index 000000000..008922d4e --- /dev/null +++ b/docusaurus/flutter_versions.json @@ -0,0 +1,3 @@ +[ + "3.x.x" +] diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 0680109c5..696bbf2c8 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,9 +1,37 @@ -## Upcoming +## 4.0.0 ✅ Added - Added `push_provider_name` to `addDevice` API call +## 4.0.0-beta.2 + +🐞 Fixed + +- Fixed reactions not working for threads in offline mode. +- [[#1046]](https://github.com/GetStream/stream-chat-flutter/issues/1046) After `/mute` command on reload cannot access + any channel. +- [[#1047]](https://github.com/GetStream/stream-chat-flutter/issues/1047) `own_capabilities` extraData missing after + channel update. +- [[#1054]](https://github.com/GetStream/stream-chat-flutter/issues/1054) Fix `Unsupported operation: Cannot remove from an unmodifiable list`. +- [[#1033]](https://github.com/GetStream/stream-chat-flutter/issues/1033) Hard delete from dashboard does not delete message from client. +- Send only `user_id` while reconnecting. + +✅ Added + +- Handle `event.message` in `channel.truncate` events +- Added additional parameters to `channel.truncate` + +## 4.0.0-beta.0 + +✅ Added + +- Added support for ownCapabilities. + +🐞 Fixed + +- Minor fixes and improvements. + ## 3.6.1 🐞 Fixed diff --git a/packages/stream_chat/example/pubspec.yaml b/packages/stream_chat/example/pubspec.yaml index cf1f3874a..f3fad2d17 100644 --- a/packages/stream_chat/example/pubspec.yaml +++ b/packages/stream_chat/example/pubspec.yaml @@ -11,7 +11,8 @@ dependencies: cupertino_icons: ^1.0.0 flutter: sdk: flutter - stream_chat: ^2.2.1 + stream_chat: + path: ../ dev_dependencies: flutter_test: diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index abe8cf48e..29a2d5adb 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -294,6 +294,18 @@ class Channel { return data; } + /// List of user permissions on this channel + List get ownCapabilities => + state?._channelState.channel?.ownCapabilities ?? []; + + /// List of user permissions on this channel + Stream> get ownCapabilitiesStream { + _checkInitialized(); + return state!.channelStateStream + .map((cs) => cs.channel?.ownCapabilities ?? []) + .distinct(); + } + /// Channel extra data as a stream. Stream> get extraDataStream { _checkInitialized(); @@ -487,6 +499,7 @@ class Channel { Future sendMessage( Message message, { bool skipPush = false, + bool skipEnrichUrl = false, }) async { _checkInitialized(); // Cancelling previous completer in case it's called again in the process @@ -534,6 +547,7 @@ class Channel { id!, type, skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, ); state!.updateMessage(response.message); if (cooldown > 0) cooldownStartedAt = DateTime.now(); @@ -550,7 +564,10 @@ class Channel { /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually updating the message. - Future updateMessage(Message message) async { + Future updateMessage( + Message message, { + bool skipEnrichUrl = false, + }) async { final originalMessage = message; // Cancelling previous completer in case it's called again in the process @@ -588,7 +605,10 @@ class Channel { message = await attachmentsUploadCompleter.future; } - final response = await _client.updateMessage(message); + final response = await _client.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); final m = response.message.copyWith( ownReactions: message.ownReactions, @@ -618,12 +638,14 @@ class Channel { Message message, { Map? set, List? unset, + bool skipEnrichUrl = false, }) async { try { final response = await _client.partialUpdateMessage( message.id, set: set, unset: unset, + skipEnrichUrl: skipEnrichUrl, ); final updatedMessage = response.message.copyWith( @@ -1237,7 +1259,9 @@ class Channel { if (preferOffline && cid != null) { final updatedState = await _client.chatPersistenceClient ?.getChannelStateByCid(cid!, messagePagination: messagesPagination); - if (updatedState != null && updatedState.messages.isNotEmpty) { + if (updatedState != null && + updatedState.messages != null && + updatedState.messages!.isNotEmpty) { if (this.state == null) { _initState(updatedState); } else { @@ -1320,14 +1344,6 @@ class Channel { return _client.unmuteChannel(cid!); } - /// Bans the user with given [userID] from the channel. - @Deprecated("Use 'banMember' instead. This method will be removed in v4.0.0") - Future banUser( - String userID, - Map options, - ) => - banMember(userID, options); - /// Bans the member with given [userID] from the channel. Future banMember( String userID, @@ -1342,12 +1358,6 @@ class Channel { return _client.banUser(userID, opts); } - /// Remove the ban for the user with given [userID] in the channel. - @Deprecated( - "Use 'unbanMember' instead. This method will be removed in v4.0.0", - ) - Future unbanUser(String userID) => unbanMember(userID); - /// Remove the ban for the member with given [userID] in the channel. Future unbanMember(String userID) async { _checkInitialized(); @@ -1548,7 +1558,7 @@ class ChannelClientState { void _checkExpiredAttachmentMessages(ChannelState channelState) async { final expiredAttachmentMessagesId = channelState.messages - .where((m) => + ?.where((m) => !_updatedMessagesIds.contains(m.id) && m.attachments.isNotEmpty && m.attachments.any((e) { @@ -1575,7 +1585,8 @@ class ChannelClientState { .map((e) => e.id) .toList(); - if (expiredAttachmentMessagesId.isNotEmpty) { + if (expiredAttachmentMessagesId != null && + expiredAttachmentMessagesId.isNotEmpty) { await _channel._initializedCompleter.future; _updatedMessagesIds.addAll(expiredAttachmentMessagesId); _channel.getMessagesById(expiredAttachmentMessagesId); @@ -1585,9 +1596,10 @@ class ChannelClientState { void _listenMemberAdded() { _subscriptions.add(_channel.on(EventType.memberAdded).listen((Event e) { final member = e.member; + final existingMembers = channelState.members ?? []; updateChannelState(channelState.copyWith( members: [ - ...channelState.members, + ...existingMembers, member!, ], )); @@ -1597,11 +1609,13 @@ class ChannelClientState { void _listenMemberRemoved() { _subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) { final user = e.user; + final existingMembers = channelState.members ?? []; + final existingRead = channelState.read ?? []; updateChannelState(channelState.copyWith( - members: channelState.members + members: existingMembers .where((m) => m.userId != user!.id) .toList(growable: false), - read: channelState.read + read: existingRead .where((r) => r.user.id != user!.id) .toList(growable: false), )); @@ -1771,9 +1785,10 @@ class ChannelClientState { updateMessage(message); if (message.pinned) { + final _existingPinnedMessages = _channelState.pinnedMessages ?? []; _channelState = _channelState.copyWith( pinnedMessages: [ - ..._channelState.pinnedMessages, + ..._existingPinnedMessages, message, ], ); @@ -1811,10 +1826,6 @@ class ChannelClientState { })); } - /// Add a [message] to this [channelState]. - @Deprecated('Use updateMessage instead') - void addMessage(Message message) => updateMessage(message); - /// Updates the [message] in the state if it exists. Adds it otherwise. void updateMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { @@ -1909,7 +1920,7 @@ class ChannelClientState { ) .listen( (event) { - final readList = List.from(_channelState.read); + final readList = List.from(_channelState.read ?? []); final userReadIndex = read.indexWhere((r) => r.user.id == event.user!.id); @@ -1930,31 +1941,34 @@ class ChannelClientState { } /// Channel message list. - List get messages => _channelState.messages; + List get messages => _channelState.messages ?? []; /// Channel message list as a stream. Stream> get messagesStream => channelStateStream - .map((cs) => cs.messages) + .map((cs) => cs.messages ?? []) .distinct(const ListEquality().equals); /// Channel pinned message list. - List get pinnedMessages => _channelState.pinnedMessages; + List get pinnedMessages => + _channelState.pinnedMessages ?? []; /// Channel pinned message list as a stream. Stream> get pinnedMessagesStream => channelStateStream - .map((cs) => cs.pinnedMessages) + .map((cs) => cs.pinnedMessages ?? []) .distinct(const ListEquality().equals); /// Get channel last message. Message? get lastMessage => - _channelState.messages.isNotEmpty ? _channelState.messages.last : null; + _channelState.messages != null && _channelState.messages!.isNotEmpty + ? _channelState.messages!.last + : null; /// Get channel last message. Stream get lastMessageStream => messagesStream.map((event) => event.isNotEmpty ? event.last : null); /// Channel members list. - List get members => _channelState.members + List get members => (_channelState.members ?? []) .map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id])) .toList(); @@ -1975,7 +1989,7 @@ class ChannelClientState { channelStateStream.map((cs) => cs.watcherCount); /// Channel watchers list. - List get watchers => _channelState.watchers + List get watchers => (_channelState.watchers ?? []) .map((e) => _channel.client.state.users[e.id] ?? e) .toList(); @@ -1987,11 +2001,20 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); + /// Channel member for the current user. + Member? get currentUserMember => members.firstWhereOrNull( + (m) => m.user?.id == _channel.client.state.currentUser?.id, + ); + + /// User role for the current user. + String? get currentUserRole => currentUserMember?.role; + /// Channel read list. - List get read => _channelState.read; + List get read => _channelState.read ?? []; /// Channel read list as a stream. - Stream> get readStream => channelStateStream.map((cs) => cs.read); + Stream> get readStream => + channelStateStream.map((cs) => cs.read ?? []); bool _isCurrentUserRead(Read read) => read.user.id == _channel._client.state.currentUser!.id; @@ -2012,7 +2035,7 @@ class ChannelClientState { /// Setter for unread count. set unreadCount(int count) { - final reads = [..._channelState.read]; + final reads = [...read]; final currentUserReadIndex = reads.indexWhere(_isCurrentUserRead); if (currentUserReadIndex < 0) return; @@ -2067,31 +2090,37 @@ class ChannelClientState { /// Update channelState with updated information. void updateChannelState(ChannelState updatedState) { + final _existingStateMessages = _channelState.messages ?? []; + final _updatedStateMessages = updatedState.messages ?? []; final newMessages = [ - ...updatedState.messages, - ..._channelState.messages + ..._updatedStateMessages, + ..._existingStateMessages .where((m) => - !updatedState.messages.any((newMessage) => newMessage.id == m.id)) + !_updatedStateMessages.any((newMessage) => newMessage.id == m.id)) .toList(), ]..sort(_sortByCreatedAt); + final _existingStateWatchers = _channelState.watchers ?? []; + final _updatedStateWatchers = updatedState.watchers ?? []; final newWatchers = [ - ...updatedState.watchers, - ..._channelState.watchers + ..._updatedStateWatchers, + ..._existingStateWatchers .where((w) => - !updatedState.watchers.any((newWatcher) => newWatcher.id == w.id)) + !_updatedStateWatchers.any((newWatcher) => newWatcher.id == w.id)) .toList(), ]; final newMembers = [ - ...updatedState.members, + ...updatedState.members ?? [], ]; + final _existingStateRead = _channelState.read ?? []; + final _updatedStateRead = updatedState.read ?? []; final newReads = [ - ...updatedState.read, - ..._channelState.read + ..._updatedStateRead, + ..._existingStateRead .where((r) => - !updatedState.read.any((newRead) => newRead.user.id == r.user.id)) + !_updatedStateRead.any((newRead) => newRead.user.id == r.user.id)) .toList(), ]; @@ -2243,9 +2272,9 @@ class ChannelClientState { _pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) { final now = DateTime.now(); var expiredMessages = channelState.pinnedMessages - .where((m) => m.pinExpires?.isBefore(now) == true) + ?.where((m) => m.pinExpires?.isBefore(now) == true) .toList(); - if (expiredMessages.isNotEmpty) { + if (expiredMessages != null && expiredMessages.isNotEmpty) { expiredMessages = expiredMessages .map((m) => m.copyWith( pinExpires: null, diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 69fd3180e..d16f56c51 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -29,7 +29,6 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/version.dart'; @@ -66,10 +65,6 @@ class StreamChatClient { this.logLevel = Level.WARNING, 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. - Read more here: https://getstream.io/blog/chat-edge-infrastructure - ''') Location? location, String? baseURL, Duration connectTimeout = const Duration(seconds: 6), Duration receiveTimeout = const Duration(seconds: 6), @@ -426,6 +421,7 @@ class StreamChatClient { } void _connectionStatusHandler(ConnectionStatus status) async { + final previousState = wsConnectionStatus; final currentState = _wsConnectionStatus = status; handleEvent(Event( @@ -433,7 +429,8 @@ class StreamChatClient { online: status == ConnectionStatus.connected, )); - if (currentState == ConnectionStatus.connected) { + if (currentState == ConnectionStatus.connected && + previousState != ConnectionStatus.connected) { // connection recovered final cids = state.channels.keys.toList(growable: false); if (cids.isNotEmpty) { @@ -621,7 +618,7 @@ class StreamChatClient { final channels = res.channels; final users = channels - .expand((it) => it.members) + .expand((it) => it.members ?? []) .map((it) => it.user) .toList(growable: false); @@ -1238,12 +1235,14 @@ class StreamChatClient { String channelId, String channelType, { bool skipPush = false, + bool skipEnrichUrl = false, }) => _chatApi.message.sendMessage( channelId, channelType, message, skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, ); /// Lists all the message replies for the [parentId] @@ -1267,8 +1266,14 @@ class StreamChatClient { ); /// Update the given message - Future updateMessage(Message message) => - _chatApi.message.updateMessage(message); + Future updateMessage( + Message message, { + bool skipEnrichUrl = false, + }) => + _chatApi.message.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); /// Partially update the given [messageId] /// Use [set] to define values to be set @@ -1277,11 +1282,13 @@ class StreamChatClient { String messageId, { Map? set, List? unset, + bool skipEnrichUrl = false, }) => _chatApi.message.partialUpdateMessage( messageId, set: set, unset: unset, + skipEnrichUrl: skipEnrichUrl, ); /// Deletes the given message @@ -1547,18 +1554,6 @@ class ClientState { /// The current user as a stream Stream get currentUserStream => _currentUserController.stream; - // coverage:ignore-start - - /// The current user - @Deprecated('Use `.currentUser` instead, Will be removed in future releases') - OwnUser? get user => _currentUserController.valueOrNull; - - /// The current user as a stream - @Deprecated( - 'Use `.currentUserStream` instead, Will be removed in future releases', - ) - Stream get userStream => _currentUserController.stream; - // coverage:ignore-end /// The current user diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 5dfa9aa36..388dda575 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -5,9 +5,9 @@ import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/stream_chat.dart'; -/// The retry queue associated to a channel +/// The retry queue associated to a channel. class RetryQueue { - /// Instantiate a new RetryQueue object + /// Instantiate a new RetryQueue object. RetryQueue({ required this.channel, this.logger, @@ -17,13 +17,13 @@ class RetryQueue { _listenFailedEvents(); } - /// The channel of this queue + /// The channel of this queue. final Channel channel; - /// The client associated with this [channel] + /// The client associated with this [channel]. final StreamChatClient client; - /// The logger associated to this queue + /// The logger associated to this queue. final Logger? logger; late final RetryPolicy _retryPolicy; @@ -63,7 +63,7 @@ class RetryQueue { }).addTo(_compositeSubscription); } - /// Add a list of messages + /// Add a list of messages. void add(List messages) { if (messages.isEmpty) return; if (!_messageQueue.containsAllMessage(messages)) { @@ -113,6 +113,7 @@ class RetryQueue { } catch (e) { if (e is! StreamChatNetworkError || !e.isRetriable) { _messageQueue.removeMessage(message); + _sendFailedEvent(message); return true; } // retry logic @@ -174,10 +175,10 @@ class RetryQueue { } } - /// Whether our [_messageQueue] has messages or not + /// Whether our [_messageQueue] has messages or not. bool get hasMessages => _messageQueue.isNotEmpty; - /// Call this method to dispose this object + /// Call this method to dispose this object. void dispose() { _messageQueue.clear(); _compositeSubscription.dispose(); diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 8894e14cc..20cdfc518 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -16,12 +16,14 @@ class MessageApi { String channelType, Message message, { bool skipPush = false, + bool skipEnrichUrl = false, }) async { final response = await _client.post( '/channels/$channelType/$channelId/message', data: { 'message': message, 'skip_push': skipPush, + 'skip_enrich_url': skipEnrichUrl, }, ); return SendMessageResponse.fromJson(response.data); @@ -51,11 +53,15 @@ class MessageApi { /// Updates the given [message] Future updateMessage( - Message message, - ) async { + Message message, { + bool skipEnrichUrl = false, + }) async { final response = await _client.post( '/messages/${message.id}', - data: {'message': message}, + data: { + 'message': message, + 'skip_enrich_url': skipEnrichUrl, + }, ); return UpdateMessageResponse.fromJson(response.data); } @@ -67,12 +73,14 @@ class MessageApi { String messageId, { Map? set, List? unset, + bool skipEnrichUrl = false, }) async { final response = await _client.put( '/messages/$messageId', data: { if (set != null) 'set': set, if (unset != null) 'unset': unset, + 'skip_enrich_url': skipEnrichUrl, }, ); return UpdateMessageResponse.fromJson(response.data); diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index 09fe10834..72ff55250 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/models/action.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; @@ -66,6 +67,21 @@ class Attachment extends Equatable { topLevelFields + dbSpecificTopLevelFields, )); + factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) => + Attachment( + type: ogAttachment.type, + title: ogAttachment.title, + titleLink: ogAttachment.titleLink, + text: ogAttachment.text, + imageUrl: ogAttachment.imageUrl, + thumbUrl: ogAttachment.thumbUrl, + authorName: ogAttachment.authorName, + authorLink: ogAttachment.authorLink, + assetUrl: ogAttachment.assetUrl, + ogScrapeUrl: ogAttachment.ogScrapeUrl, + uploadState: const UploadState.success(), + ); + ///The attachment type based on the URL resource. This can be: audio, ///image or video final String? type; @@ -108,8 +124,7 @@ class Attachment extends Equatable { final String? assetUrl; /// Actions from a command - @JsonKey(defaultValue: []) - final List actions; + final List? actions; final Uri? localUri; @@ -229,6 +244,33 @@ class Attachment extends Equatable { extraData: extraData ?? this.extraData, ); + Attachment merge(Attachment? other) { + if (other == null) return this; + return copyWith( + type: other.type, + titleLink: other.titleLink, + title: other.title, + thumbUrl: other.thumbUrl, + text: other.text, + pretext: other.pretext, + ogScrapeUrl: other.ogScrapeUrl, + imageUrl: other.imageUrl, + footerIcon: other.footerIcon, + footer: other.footer, + fields: other.fields, + fallback: other.fallback, + color: other.color, + authorName: other.authorName, + authorLink: other.authorLink, + authorIcon: other.authorIcon, + assetUrl: other.assetUrl, + actions: other.actions, + file: other.file, + uploadState: other.uploadState, + extraData: other.extraData, + ); + } + @override List get props => [ id, diff --git a/packages/stream_chat/lib/src/core/models/attachment.g.dart b/packages/stream_chat/lib/src/core/models/attachment.g.dart index 381eb99c5..2ebe8c533 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.g.dart @@ -26,9 +26,8 @@ Attachment _$AttachmentFromJson(Map json) => Attachment( authorIcon: json['author_icon'] as String?, assetUrl: json['asset_url'] as String?, actions: (json['actions'] as List?) - ?.map((e) => Action.fromJson(e as Map)) - .toList() ?? - [], + ?.map((e) => Action.fromJson(e as Map)) + .toList(), extraData: json['extra_data'] as Map? ?? const {}, file: json['file'] == null ? null @@ -64,7 +63,7 @@ Map _$AttachmentToJson(Attachment instance) { writeNotNull('author_link', instance.authorLink); writeNotNull('author_icon', instance.authorIcon); writeNotNull('asset_url', instance.assetUrl); - val['actions'] = instance.actions.map((e) => e.toJson()).toList(); + writeNotNull('actions', instance.actions?.map((e) => e.toJson()).toList()); writeNotNull('file', instance.file?.toJson()); val['upload_state'] = instance.uploadState.toJson(); val['extra_data'] = instance.extraData; 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 c524a7421..634af64f4 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 037c1431e..c3e834354 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -13,6 +13,7 @@ class ChannelModel { String? id, String? type, String? cid, + this.ownCapabilities, ChannelConfig? config, this.createdBy, this.frozen = false, @@ -51,6 +52,10 @@ class ChannelModel { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String cid; + /// List of user permissions on this channel + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final List? ownCapabilities; + /// The channel configuration data @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final ChannelConfig config; @@ -101,6 +106,7 @@ class ChannelModel { 'id', 'type', 'cid', + 'own_capabilities', 'config', 'created_by', 'frozen', @@ -127,6 +133,7 @@ class ChannelModel { String? id, String? type, String? cid, + List? ownCapabilities, ChannelConfig? config, User? createdBy, bool? frozen, @@ -143,6 +150,7 @@ class ChannelModel { id: id ?? this.id, type: type ?? this.type, cid: cid ?? this.cid, + ownCapabilities: ownCapabilities ?? this.ownCapabilities, config: config ?? this.config, createdBy: createdBy ?? this.createdBy, frozen: frozen ?? this.frozen, @@ -164,6 +172,7 @@ class ChannelModel { id: other.id, type: other.type, cid: other.cid, + ownCapabilities: other.ownCapabilities, config: other.config, createdBy: other.createdBy, frozen: other.frozen, 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 94d9a81ad..9bd8f062e 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 @@ -10,6 +10,9 @@ ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( id: json['id'] as String?, type: json['type'] as String?, cid: json['cid'] as String?, + ownCapabilities: (json['own_capabilities'] as List?) + ?.map((e) => e as String) + .toList(), config: json['config'] == null ? null : ChannelConfig.fromJson(json['config'] as Map), @@ -48,6 +51,7 @@ Map _$ChannelModelToJson(ChannelModel instance) { } writeNotNull('cid', readonly(instance.cid)); + writeNotNull('own_capabilities', readonly(instance.ownCapabilities)); writeNotNull('config', readonly(instance.config)); writeNotNull('created_by', readonly(instance.createdBy)); val['frozen'] = instance.frozen; diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 17439dc0f..3ba3c5f89 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -7,42 +7,40 @@ import 'package:stream_chat/src/core/models/user.dart'; part 'channel_state.g.dart'; -const _emptyPinnedMessages = []; - /// The class that contains the information about a channel @JsonSerializable() class ChannelState { /// Constructor used for json serialization ChannelState({ this.channel, - this.messages = const [], - this.members = const [], - this.pinnedMessages = _emptyPinnedMessages, + this.messages, + this.members, + this.pinnedMessages, this.watcherCount, - this.watchers = const [], - this.read = const [], + this.watchers, + this.read, }); /// The channel to which this state belongs final ChannelModel? channel; /// A paginated list of channel messages - final List messages; + final List? messages; /// A paginated list of channel members - final List members; + final List? members; /// A paginated list of pinned messages - final List pinnedMessages; + final List? pinnedMessages; /// The count of users watching the channel final int? watcherCount; /// A paginated list of users watching the channel - final List watchers; + final List? watchers; /// The list of channel reads - final List read; + final List? read; /// Create a new instance from a json static ChannelState fromJson(Map json) => @@ -56,7 +54,7 @@ class ChannelState { ChannelModel? channel, List? messages, List? members, - List pinnedMessages = _emptyPinnedMessages, + List? pinnedMessages, int? watcherCount, List? watchers, List? read, @@ -65,11 +63,7 @@ class ChannelState { channel: channel ?? this.channel, messages: messages ?? this.messages, members: members ?? this.members, - // Hack to avoid using the default value in case nothing is provided. - // FIXME: Use non-nullable by default instead of empty list. - pinnedMessages: pinnedMessages == _emptyPinnedMessages - ? this.pinnedMessages - : pinnedMessages, + pinnedMessages: pinnedMessages ?? this.pinnedMessages, watcherCount: watcherCount ?? this.watcherCount, watchers: watchers ?? this.watchers, read: read ?? this.read, diff --git a/packages/stream_chat/lib/src/core/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart index 5100932ca..afec76c48 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.g.dart @@ -11,36 +11,31 @@ ChannelState _$ChannelStateFromJson(Map json) => ChannelState( ? null : ChannelModel.fromJson(json['channel'] as Map), messages: (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - const [], + ?.map((e) => Message.fromJson(e as Map)) + .toList(), members: (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - const [], + ?.map((e) => Member.fromJson(e as Map)) + .toList(), pinnedMessages: (json['pinned_messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - _emptyPinnedMessages, + ?.map((e) => Message.fromJson(e as Map)) + .toList(), watcherCount: json['watcher_count'] as int?, watchers: (json['watchers'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList() ?? - const [], + ?.map((e) => User.fromJson(e as Map)) + .toList(), read: (json['read'] as List?) - ?.map((e) => Read.fromJson(e as Map)) - .toList() ?? - const [], + ?.map((e) => Read.fromJson(e as Map)) + .toList(), ); Map _$ChannelStateToJson(ChannelState instance) => { 'channel': instance.channel?.toJson(), - 'messages': instance.messages.map((e) => e.toJson()).toList(), - 'members': instance.members.map((e) => e.toJson()).toList(), + 'messages': instance.messages?.map((e) => e.toJson()).toList(), + 'members': instance.members?.map((e) => e.toJson()).toList(), 'pinned_messages': - instance.pinnedMessages.map((e) => e.toJson()).toList(), + instance.pinnedMessages?.map((e) => e.toJson()).toList(), 'watcher_count': instance.watcherCount, - 'watchers': instance.watchers.map((e) => e.toJson()).toList(), - 'read': instance.read.map((e) => e.toJson()).toList(), + 'watchers': instance.watchers?.map((e) => e.toJson()).toList(), + 'read': instance.read?.map((e) => e.toJson()).toList(), }; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 10ff713e2..017bb06fc 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -8,13 +8,13 @@ import 'package:uuid/uuid.dart'; part 'message.g.dart'; -class _PinExpires { - const _PinExpires(); +class _NullConst { + const _NullConst(); } -const _pinExpires = _PinExpires(); +const _nullConst = _NullConst(); -/// Enum defining the status of a sending message +/// Enum defining the status of a sending message. enum MessageSendingStatus { /// Message is being sent sending, @@ -40,10 +40,10 @@ enum MessageSendingStatus { sent, } -/// The class that contains the information about a message +/// The class that contains the information about a message. @JsonSerializable() class Message extends Equatable { - /// Constructor used for json serialization + /// Constructor used for json serialization. Message({ String? id, this.text, @@ -58,44 +58,47 @@ class Message extends Equatable { this.ownReactions, this.parentId, this.quotedMessage, - this.quotedMessageId, + String? quotedMessageId, this.replyCount = 0, this.threadParticipants, this.showInChannel, this.command, DateTime? createdAt, DateTime? updatedAt, + this.deletedAt, this.user, this.pinned = false, this.pinnedAt, DateTime? pinExpires, this.pinnedBy, this.extraData = const {}, - this.deletedAt, - this.status = MessageSendingStatus.sent, + this.status = MessageSendingStatus.sending, this.i18n, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + _createdAt = createdAt, + _updatedAt = updatedAt, + _quotedMessageId = quotedMessageId; - /// Create a new instance from a json + /// Create a new instance from JSON. factory Message.fromJson(Map json) => _$MessageFromJson( Serializer.moveToExtraDataFromRoot(json, topLevelFields), + ).copyWith( + status: MessageSendingStatus.sent, ); /// The message ID. This is either created by Stream or set client side when /// the message is added. final String id; - /// The text of this message + /// The text of this message. final String? text; - /// The status of a sending message + /// The status of a sending message. @JsonKey(ignore: true) final MessageSendingStatus status; - /// The message type + /// The message type. @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, @@ -107,15 +110,15 @@ class Message extends Equatable { @JsonKey(includeIfNull: false) final List attachments; - /// The list of user mentioned in the message + /// The list of user mentioned in the message. @JsonKey(toJson: User.toIds) final List mentionedUsers; - /// A map describing the count of number of every reaction + /// A map describing the count of number of every reaction. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final Map? reactionCounts; - /// A map describing the count of score of every reaction + /// A map describing the count of score of every reaction. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final Map? reactionScores; @@ -130,12 +133,14 @@ class Message extends Equatable { /// The ID of the parent message, if the message is a thread reply. final String? parentId; - /// A quoted reply message + /// A quoted reply message. @JsonKey(toJson: Serializer.readOnly) final Message? quotedMessage; + final String? _quotedMessageId; + /// The ID of the quoted message, if the message is a quoted reply. - final String? quotedMessageId; + String? get quotedMessageId => _quotedMessageId ?? quotedMessage?.id; /// Reserved field indicating the number of replies for this message. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @@ -148,10 +153,10 @@ class Message extends Equatable { /// Check if this message needs to show in the channel. final bool? showInChannel; - /// If true the message is silent + /// If true the message is silent. final bool silent; - /// If true the message is shadowed + /// If true the message is shadowed. @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, @@ -162,56 +167,61 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? command; + final DateTime? _createdAt; + + /// Reserved field indicating when the message was deleted. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final DateTime? deletedAt; + /// Reserved field indicating when the message was created. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime createdAt; + DateTime get createdAt => _createdAt ?? DateTime.now(); + + final DateTime? _updatedAt; /// Reserved field indicating when the message was updated last time. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime updatedAt; + DateTime get updatedAt => _updatedAt ?? DateTime.now(); - /// User who sent the message + /// User who sent the message. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final User? user; - /// If true the message is pinned + /// If true the message is pinned. final bool pinned; - /// Reserved field indicating when the message was pinned + /// Reserved field indicating when the message was pinned. @JsonKey(toJson: Serializer.readOnly) final DateTime? pinnedAt; - /// Reserved field indicating when the message will expire + /// Reserved field indicating when the message will expire. /// - /// if `null` message has no expiry + /// If `null` message has no expiry. final DateTime? pinExpires; - /// Reserved field indicating who pinned the message + /// Reserved field indicating who pinned the message. @JsonKey(toJson: Serializer.readOnly) final User? pinnedBy; - /// Message custom extraData + /// Message custom extraData. @JsonKey(includeIfNull: false) final Map extraData; - /// True if the message is a system info + /// True if the message is a system info. bool get isSystem => type == 'system'; - /// True if the message has been deleted + /// True if the message has been deleted. bool get isDeleted => type == 'deleted'; - /// True if the message is ephemeral + /// True if the message is ephemeral. bool get isEphemeral => type == 'ephemeral'; - /// Reserved field indicating when the message was deleted. - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime? deletedAt; - /// A Map of translations. @JsonKey(includeIfNull: false) final Map? i18n; /// Known top level fields. + /// /// Useful for [Serializer] methods. static const topLevelFields = [ 'id', @@ -244,7 +254,7 @@ class Message extends Equatable { 'i18n', ]; - /// Serialize to json + /// Serialize to json. Map toJson() => Serializer.moveFromExtraDataToRoot( _$MessageToJson(this), ); @@ -256,18 +266,18 @@ class Message extends Equatable { String? type, List? attachments, List? mentionedUsers, + bool? silent, + bool? shadowed, Map? reactionCounts, Map? reactionScores, List? latestReactions, List? ownReactions, String? parentId, - Message? quotedMessage, - String? quotedMessageId, + Object? quotedMessage = _nullConst, + Object? quotedMessageId = _nullConst, int? replyCount, List? threadParticipants, bool? showInChannel, - bool? shadowed, - bool? silent, String? command, DateTime? createdAt, DateTime? updatedAt, @@ -275,7 +285,7 @@ class Message extends Equatable { User? user, bool? pinned, DateTime? pinnedAt, - Object? pinExpires = _pinExpires, + Object? pinExpires = _nullConst, User? pinnedBy, Map? extraData, MessageSendingStatus? status, @@ -284,41 +294,68 @@ class Message extends Equatable { assert(() { if (pinExpires is! DateTime && pinExpires != null && - pinExpires is! _PinExpires) { + pinExpires is! _NullConst) { throw ArgumentError('`pinExpires` can only be set as DateTime or null'); } return true; }(), 'Validate type for pinExpires'); + + assert(() { + if (quotedMessage is! Message && + quotedMessage != null && + quotedMessage is! _NullConst) { + throw ArgumentError( + '`quotedMessage` can only be set as Message or null', + ); + } + return true; + }(), 'Validate type for quotedMessage'); + + assert(() { + if (quotedMessageId is! String && + quotedMessageId != null && + quotedMessageId is! _NullConst) { + throw ArgumentError( + '`quotedMessage` can only be set as String or null', + ); + } + return true; + }(), 'Validate type for quotedMessage'); + return Message( id: id ?? this.id, text: text ?? this.text, type: type ?? this.type, attachments: attachments ?? this.attachments, mentionedUsers: mentionedUsers ?? this.mentionedUsers, + silent: silent ?? this.silent, + shadowed: shadowed ?? this.shadowed, reactionCounts: reactionCounts ?? this.reactionCounts, reactionScores: reactionScores ?? this.reactionScores, latestReactions: latestReactions ?? this.latestReactions, ownReactions: ownReactions ?? this.ownReactions, parentId: parentId ?? this.parentId, - quotedMessage: quotedMessage ?? this.quotedMessage, - quotedMessageId: quotedMessageId ?? this.quotedMessageId, + quotedMessage: quotedMessage == _nullConst + ? this.quotedMessage + : quotedMessage as Message?, + quotedMessageId: quotedMessageId == _nullConst + ? _quotedMessageId + : quotedMessageId as String?, replyCount: replyCount ?? this.replyCount, threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, command: command ?? this.command, - createdAt: createdAt ?? this.createdAt, - silent: silent ?? this.silent, - extraData: extraData ?? this.extraData, - user: user ?? this.user, - shadowed: shadowed ?? this.shadowed, - updatedAt: updatedAt ?? this.updatedAt, + createdAt: createdAt ?? _createdAt, + updatedAt: updatedAt ?? _updatedAt, deletedAt: deletedAt ?? this.deletedAt, - status: status ?? this.status, + user: user ?? this.user, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt ?? this.pinnedAt, - pinnedBy: pinnedBy ?? this.pinnedBy, pinExpires: - pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?, + pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?, + pinnedBy: pinnedBy ?? this.pinnedBy, + extraData: extraData ?? this.extraData, + status: status ?? this.status, i18n: i18n ?? this.i18n, ); } @@ -331,6 +368,8 @@ class Message extends Equatable { type: other.type, attachments: other.attachments, mentionedUsers: other.mentionedUsers, + silent: other.silent, + shadowed: other.shadowed, reactionCounts: other.reactionCounts, reactionScores: other.reactionScores, latestReactions: other.latestReactions, @@ -343,17 +382,15 @@ class Message extends Equatable { showInChannel: other.showInChannel, command: other.command, createdAt: other.createdAt, - silent: other.silent, - extraData: other.extraData, - user: other.user, - shadowed: other.shadowed, updatedAt: other.updatedAt, deletedAt: other.deletedAt, - status: other.status, + user: other.user, pinned: other.pinned, pinnedAt: other.pinnedAt, pinExpires: other.pinExpires, pinnedBy: other.pinnedBy, + extraData: other.extraData, + status: other.status, i18n: other.i18n, ); @@ -377,8 +414,8 @@ class Message extends Equatable { shadowed, silent, command, - createdAt, - updatedAt, + _createdAt, + _updatedAt, deletedAt, user, pinned, diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index b3b58c589..e2ff89b49 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -49,6 +49,9 @@ Message _$MessageFromJson(Map json) => Message( updatedAt: json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), user: json['user'] == null ? null : User.fromJson(json['user'] as Map), @@ -63,9 +66,6 @@ Message _$MessageFromJson(Map json) => Message( ? null : User.fromJson(json['pinned_by'] as Map), extraData: json['extra_data'] as Map? ?? const {}, - deletedAt: json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), i18n: (json['i18n'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ), @@ -99,6 +99,7 @@ Map _$MessageToJson(Message instance) { val['silent'] = instance.silent; writeNotNull('shadowed', readonly(instance.shadowed)); writeNotNull('command', readonly(instance.command)); + writeNotNull('deleted_at', readonly(instance.deletedAt)); writeNotNull('created_at', readonly(instance.createdAt)); writeNotNull('updated_at', readonly(instance.updatedAt)); writeNotNull('user', readonly(instance.user)); @@ -107,7 +108,6 @@ Map _$MessageToJson(Message instance) { val['pin_expires'] = instance.pinExpires?.toIso8601String(); val['pinned_by'] = readonly(instance.pinnedBy); val['extra_data'] = instance.extraData; - writeNotNull('deleted_at', readonly(instance.deletedAt)); writeNotNull('i18n', instance.i18n); return val; } diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 2dfb7cd74..b17698076 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -44,10 +44,10 @@ abstract class ChatPersistenceClient { Future getChannelByCid(String cid); /// Get stored channel [Member]s by providing channel [cid] - Future> getMembersByCid(String cid); + Future?> getMembersByCid(String cid); /// Get stored channel [Read]s by providing channel [cid] - Future> getReadsByCid(String cid); + Future?> getReadsByCid(String cid); /// Get stored [Message]s by providing channel [cid] /// @@ -78,15 +78,11 @@ abstract class ChatPersistenceClient { getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination), ]); return ChannelState( - // ignore: cast_nullable_to_non_nullable - members: data[0] as List, - // ignore: cast_nullable_to_non_nullable - read: data[1] as List, + members: data[0] as List?, + read: data[1] as List?, channel: data[2] as ChannelModel?, - // ignore: cast_nullable_to_non_nullable - messages: data[3] as List, - // ignore: cast_nullable_to_non_nullable - pinnedMessages: data[4] as List, + messages: data[3] as List?, + pinnedMessages: data[4] as List?, ); } @@ -146,7 +142,7 @@ abstract class ChatPersistenceClient { bulkUpdateMessages({cid: messages}); /// Bulk updates the message data of multiple channels. - Future bulkUpdateMessages(Map> messages); + Future bulkUpdateMessages(Map?> messages); /// Updates the pinned message data of a particular channel [cid] with /// the new [messages] data @@ -154,7 +150,7 @@ abstract class ChatPersistenceClient { bulkUpdatePinnedMessages({cid: messages}); /// Bulk updates the message data of multiple channels. - Future bulkUpdatePinnedMessages(Map> messages); + Future bulkUpdatePinnedMessages(Map?> messages); /// Returns all the threads by parent message of a particular channel by /// providing channel [cid] @@ -169,7 +165,7 @@ abstract class ChatPersistenceClient { bulkUpdateMembers({cid: members}); /// Bulk updates the members data of multiple channels. - Future bulkUpdateMembers(Map> members); + Future bulkUpdateMembers(Map?> members); /// Updates the read data of a particular channel [cid] with /// the new [reads] data @@ -177,7 +173,7 @@ abstract class ChatPersistenceClient { bulkUpdateReads({cid: reads}); /// Bulk updates the read data of multiple channels. - Future bulkUpdateReads(Map> reads); + Future bulkUpdateReads(Map?> reads); /// Updates the users data with the new [users] data Future updateUsers(List users); @@ -230,10 +226,10 @@ abstract class ChatPersistenceClient { final membersToDelete = []; final channels = []; - final channelWithMessages = >{}; - final channelWithPinnedMessages = >{}; - final channelWithReads = >{}; - final channelWithMembers = >{}; + final channelWithMessages = ?>{}; + final channelWithPinnedMessages = ?>{}; + final channelWithReads = ?>{}; + final channelWithMembers = ?>{}; final users = []; final reactions = []; @@ -252,8 +248,9 @@ abstract class ChatPersistenceClient { // Preparing deletion data membersToDelete.add(cid); - reactionsToDelete.addAll(state.messages.map((it) => it.id)); - pinnedReactionsToDelete.addAll(state.pinnedMessages.map((it) => it.id)); + reactionsToDelete.addAll(state.messages?.map((it) => it.id) ?? []); + pinnedReactionsToDelete + .addAll(state.pinnedMessages?.map((it) => it.id) ?? []); // preparing addition data channelWithReads[cid] = reads; @@ -261,14 +258,14 @@ abstract class ChatPersistenceClient { channelWithMessages[cid] = messages; channelWithPinnedMessages[cid] = pinnedMessages; - reactions.addAll(messages.expand(_expandReactions)); - pinnedReactions.addAll(pinnedMessages.expand(_expandReactions)); + reactions.addAll(messages?.expand(_expandReactions) ?? []); + pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []); users.addAll([ channel.createdBy, - ...messages.map((it) => it.user), - ...reads.map((it) => it.user), - ...members.map((it) => it.user), + ...messages?.map((it) => it.user) ?? [], + ...reads?.map((it) => it.user) ?? [], + ...members?.map((it) => it.user) ?? [], ...reactions.map((it) => it.user), ...pinnedReactions.map((it) => it.user), ].withNullifyer); diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart new file mode 100644 index 000000000..993351b72 --- /dev/null +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -0,0 +1,96 @@ +/// Describes capabilities of a user vis-a-vis a channel +class PermissionType { + /// Capability required to send a message in the channel + /// Channel is not frozen (or user has UseFrozenChannel permission) + /// and user has CreateMessage permission. + static const String sendMessage = 'send-message'; + + /// Capability required to receive connect events in the channel + static const String connectEvents = 'connect-events'; + + /// Capability required to send a message + /// Reactions are enabled for the channel, channel is not frozen + /// (or user has UseFrozenChannel permission) and user has + /// CreateReaction permission + static const String sendReaction = 'send-reaction'; + + /// Capability required to send links in a channel + /// send-message + user has AddLinks permission + static const String sendLinks = 'send-links'; + + /// Capability required to send thread reply + /// send-message + channel has replies enabled + static const String sendReply = 'send-reply'; + + /// Capability to freeze a channel + /// User has UpdateChannelFrozen permission. + /// The name implies freezing, + /// but unfreezing is also allowed when this capability is present + static const String freezeChannel = 'freeze-channel'; + + /// User has UpdateChannelCooldown permission. + /// Allows to enable/disable slow mode in the channel + static const String setChannelCooldown = 'set-channel-cooldown'; + + /// User has RemoveOwnChannelMembership or UpdateChannelMembers permission + static const String leaveChannel = 'leave-channel'; + + /// User can mute channel + static const String muteChannel = 'mute-channel'; + + /// Ability to receive read events + static const String readEvents = 'read-events'; + + /// Capability required to pin a message in a channel + /// Corresponds to PinMessage permission + static const String pinMessage = 'pin-message'; + + /// Capability required to quote a message in a channel + static const String quoteMessage = 'quote-message'; + + /// Capability required to flag a message in a channel + static const String flagMessage = 'flag-message'; + + /// User has ability to delete any message in the channel + /// User has DeleteMessage permission + /// which applies to any message in the channel + static const String deleteAnyMessage = 'delete-any-message'; + + /// User has ability to delete their own message in the channel + /// User has DeleteMessage permission which applies only to owned messages + static const String deleteOwnMessage = 'delete-own-message'; + + /// User has ability to update/edit any message in the channel + /// User has UpdateMessage permission which + /// applies to any message in the channel + static const String updateAnyMessage = 'update-any-message'; + + /// User has ability to update/edit their own message in the channel + /// User has UpdateMessage permission which applies only to owned messages + static const String updateOwnMessage = 'update-own-message'; + + /// User can search for message in a channel + /// Search feature is enabled (it will also have + /// permission check in the future) + static const String searchMessages = 'search-messages'; + + /// Capability required to send typing events in a channel + /// (Typing events are enabled) + static const String sendTypingEvents = 'send-typing-events'; + + /// Capability required to upload a file in a channel + /// Uploads are enabled and user has UploadAttachment + static const String uploadFile = 'upload-file'; + + /// Capability required to delete channel + /// User has DeleteChannel permission + static const String deleteChannel = 'delete-channel'; + + /// Capability required update/edit channel info + /// User has UpdateChannel permission + static const String updateChannel = 'update-channel'; + + /// Capability required to update/edit channel members + /// Channel is not distinct and user has UpdateChannelMembers permission + static const String updateChannelMembers = 'update-channel-members'; +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 794d75059..0fce9d63f 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -7,7 +7,38 @@ export 'package:dio/src/options.dart'; export 'package:dio/src/options.dart' show ProgressCallback; export 'package:logging/logging.dart' show Logger, Level, LogRecord; export 'package:rate_limiter/rate_limiter.dart'; +export 'package:uuid/uuid.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/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; diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index cc2c6eb98..46bec2d5c 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '3.6.1'; +const PACKAGE_VERSION = '4.0.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index c57051950..8ed38305c 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 3.6.1 +version: 4.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 196576836..9452a433b 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -244,9 +244,13 @@ void main() { group('`.sendMessage`', () { test('should work fine', () async { - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + ); - final sendMessageResponse = SendMessageResponse()..message = message; + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(status: MessageSendingStatus.sent); when(() => client.sendMessage( any(that: isSameMessageAs(message)), @@ -329,6 +333,7 @@ void main() { .map((it) => it.copyWith(uploadState: const UploadState.success())) .toList(growable: false), + status: MessageSendingStatus.sent, )); expectLater( @@ -455,7 +460,10 @@ void main() { group('`.updateMessage`', () { test('should work fine', () async { - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); final updateMessageResponse = UpdateMessageResponse() ..message = message; @@ -530,6 +538,7 @@ void main() { any(that: isSameMessageAs(message)), )).thenAnswer((_) async => UpdateMessageResponse() ..message = message.copyWith( + status: MessageSendingStatus.sent, attachments: attachments .map((it) => it.copyWith(uploadState: const UploadState.success())) @@ -678,7 +687,7 @@ void main() { [ isSameMessageAs( updateMessageResponse.message.copyWith( - status: MessageSendingStatus.sent, + status: MessageSendingStatus.sending, ), matchText: true, matchSendingStatus: true, @@ -707,7 +716,10 @@ void main() { group('`.deleteMessage`', () { test('should work fine', () async { const messageId = 'test-message-id'; - final message = Message(id: messageId); + final message = Message( + id: messageId, + status: MessageSendingStatus.sent, + ); when(() => client.deleteMessage(messageId)) .thenAnswer((_) async => EmptyResponse()); @@ -744,7 +756,6 @@ void main() { const messageId = 'test-message-id'; final message = Message( id: messageId, - status: MessageSendingStatus.sending, ); expectLater( @@ -1077,7 +1088,10 @@ void main() { group('`.sendReaction`', () { test('should work fine', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); final reaction = Reaction(type: type, messageId: message.id); @@ -1118,7 +1132,10 @@ void main() { test('should work fine with score passed explicitly', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); const score = 5; final reaction = Reaction( @@ -1178,7 +1195,10 @@ void main() { test('should work fine with score passed explicitly and in extraData', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); const score = 5; const extraDataScore = 3; @@ -1249,7 +1269,10 @@ void main() { 'should restore previous message if `client.sendReaction` throws', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); final reaction = Reaction(type: type, messageId: message.id); @@ -1310,6 +1333,7 @@ void main() { latestReactions: [prevReaction], reactionScores: const {prevType: 1}, reactionCounts: const {prevType: 1}, + status: MessageSendingStatus.sent, ); const type = 'test-reaction-type-2'; @@ -1341,7 +1365,7 @@ void main() { emitsInOrder([ [ isSameMessageAs( - newMessage.copyWith(status: MessageSendingStatus.sent), + newMessage, matchReactions: true, matchSendingStatus: true, ), @@ -1374,6 +1398,7 @@ void main() { final message = Message( id: 'test-message-id', parentId: 'test-parent-id', // is thread message + status: MessageSendingStatus.sent, ); final reaction = Reaction(type: type, messageId: message.id); @@ -1423,6 +1448,7 @@ void main() { final message = Message( id: 'test-message-id', parentId: 'test-parent-id', // is thread message + status: MessageSendingStatus.sent, ); final reaction = Reaction(type: type, messageId: message.id); @@ -1490,6 +1516,7 @@ void main() { latestReactions: [prevReaction], reactionScores: const {prevType: 1}, reactionCounts: const {prevType: 1}, + status: MessageSendingStatus.sent, ); const type = 'test-reaction-type-2'; @@ -1567,6 +1594,7 @@ void main() { latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) @@ -1614,6 +1642,7 @@ void main() { latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) @@ -1668,11 +1697,13 @@ void main() { ); final message = Message( id: messageId, - parentId: parentId, // is thread + parentId: parentId, + // is thread ownReactions: [reaction], latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) @@ -1725,6 +1756,7 @@ void main() { latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index a7856c928..bd0403de9 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -105,11 +105,11 @@ void main() { ); expect(res, isNotNull); - expect(res.messages.length, channelState.messages.length); - expect(res.pinnedMessages.length, channelState.pinnedMessages.length); - expect(res.members.length, channelState.members.length); - expect(res.read.length, channelState.read.length); - expect(res.watchers.length, channelState.watchers.length); + expect(res.messages?.length, channelState.messages?.length); + expect(res.pinnedMessages?.length, channelState.pinnedMessages?.length); + expect(res.members?.length, channelState.members?.length); + expect(res.read?.length, channelState.read?.length); + expect(res.watchers?.length, channelState.watchers?.length); expect(res.watcherCount, channelState.watcherCount); verify(() => client.post(path, data: any(named: 'data'))).called(1); diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index e89491a2c..ed7fefd62 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -32,6 +32,7 @@ void main() { data: { 'message': message, 'skip_push': false, + 'skip_enrich_url': false, }, )).thenAnswer((_) async => successResponse(path, data: { 'message': message.toJson(), @@ -58,6 +59,7 @@ void main() { data: { 'message': message, 'skip_push': true, + 'skip_enrich_url': false, }, )).thenAnswer((_) async => successResponse(path, data: { 'message': message.toJson(), @@ -137,7 +139,10 @@ void main() { when(() => client.post( path, - data: {'message': message}, + data: { + 'message': message, + 'skip_enrich_url': false, + }, )).thenAnswer( (_) async => successResponse(path, data: {'message': message.toJson()}), ); @@ -162,7 +167,11 @@ void main() { when(() => client.put( path, - data: {'set': set, 'unset': unset}, + data: { + 'set': set, + 'unset': unset, + 'skip_enrich_url': false, + }, )).thenAnswer( (_) async => successResponse(path, data: {'message': message.toJson()}), ); @@ -180,7 +189,11 @@ void main() { verify(() => client.put( path, - data: {'set': set, 'unset': unset}, + data: { + 'set': set, + 'unset': unset, + 'skip_enrich_url': false, + }, )).called(1); verifyNoMoreInteractions(client); }); diff --git a/packages/stream_chat/test/src/core/models/attachment_test.dart b/packages/stream_chat/test/src/core/models/attachment_test.dart index 4e08f7f2a..edcc0da6f 100644 --- a/packages/stream_chat/test/src/core/models/attachment_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_test.dart @@ -19,8 +19,10 @@ void main() { attachment.thumbUrl, 'https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif', ); + expect(attachment.actions, isNotNull); + expect(attachment.actions, isNotEmpty); expect(attachment.actions, hasLength(3)); - expect(attachment.actions[0], isA()); + expect(attachment.actions![0], isA()); }); test('should serialize to json correctly', () { diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index 58f810a4d..65a589b89 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -30,14 +30,16 @@ void main() { channelState.channel?.extraData['image'], 'https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png', ); + expect(channelState.messages, isNotNull); + expect(channelState.messages, isNotEmpty); expect(channelState.messages, hasLength(25)); - expect(channelState.messages[0], isA()); - expect(channelState.messages[0], isNotNull); + expect(channelState.messages![0], isA()); + expect(channelState.messages![0], isNotNull); expect( - channelState.messages[0].createdAt, + channelState.messages![0].createdAt, DateTime.parse('2020-01-29T03:23:02.843948Z'), ); - expect(channelState.messages[0].user, isA()); + expect(channelState.messages![0].user, isA()); expect(channelState.watcherCount, 5); }); diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 7c49cc7dc..121d279d1 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -117,19 +117,20 @@ class TestPersistenceClient extends ChatPersistenceClient { Future updateUsers(List users) => Future.value(); @override - Future bulkUpdateMembers(Map> members) => + Future bulkUpdateMembers(Map?> members) => Future.value(); @override - Future bulkUpdateMessages(Map> messages) => + Future bulkUpdateMessages(Map?> messages) => Future.value(); @override - Future bulkUpdatePinnedMessages(Map> messages) => + Future bulkUpdatePinnedMessages(Map?> messages) => Future.value(); @override - Future bulkUpdateReads(Map> reads) => Future.value(); + Future bulkUpdateReads(Map?> reads) => + Future.value(); } void main() { diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart index 5519b2b3d..68242333f 100644 --- a/packages/stream_chat/test/src/ws/websocket_test.dart +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -59,7 +59,10 @@ void main() { }); test('`connect` successfully with the provided user', () async { - final user = OwnUser(id: 'test-user'); + final user = OwnUser( + id: 'test-user', + name: 'test', + ); const connectionId = 'test-connection-id'; // Sends connect event to web-socket stream final timer = Timer(const Duration(milliseconds: 300), () { @@ -90,6 +93,44 @@ void main() { addTearDown(timer.cancel); }); + test('`connect` successfully without user details', () async { + final user = OwnUser( + id: 'test-user', + name: 'test', + ); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final event = await webSocket.connect( + user, + includeUserDetails: true, + ); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me, isNotNull); + expect(event.me!.id, user.id); + + addTearDown(timer.cancel); + }); + test('`connect` should throw if already in connection attempt', () async { final user = OwnUser(id: 'test-user'); webSocket.connect(user); diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b7d574815..0fdfd392a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.0.0 ✅ Added @@ -11,6 +11,37 @@ - [[#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 + +- Added support to pass `autoCorrect` to `StreamMessageInput` for the text input field +- Added support to control the visibility of the default emoji suggestions overlay in `StreamMessageInput` +- Added support to build custom widget for scrollToBottom in `StreamMessageListView` + +🐞 Fixed + +- Minor fixes and improvements +-[[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`. +- Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput` +- Removed dependency on `visibility_detector` +- [[#1071]](https://github.com/GetStream/stream-chat-flutter/issues/1071): Fixed the way attachment actions were handled in full screen + +## 4.0.0-beta.1 + +✅ Added + +- Deprecated old widgets in favor of Stream-prefixed ones. +- Use channel capabilities to show/hide actions. +- Deprecated `ChannelListView` in favor of `StreamChannelListView`. +- Deprecated `ChannelPreview` in favor of `StreamChannelListTile`. +- Deprecated `ChannelAvatar` in favor of `StreamChannelAvatar`. +- Deprecated `ChannelName` in favor of `StreamChannelName`. +- Deprecated `MessageInput` in favor of `StreamMessageInput`. +- Separated `MessageInput` widget in smaller components. (For example `CountDownButton`, `StreamAttachmentPicker`...) +- Updated `stream_chat_flutter_core` dependency to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). +- Added OpenGraph preview support for links in `StreamMessageInput`. +- Removed video compression. ## 3.6.1 @@ -20,12 +51,18 @@ 🐞 Fixed -- [[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`. +- Minor fixes and improvements +-[[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`. - Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput` - Removed dependency on `visibility_detector` ## 3.5.1 +🛑️ Breaking Changes + +- `pinPermissions` is no longer needed in `MessageListView`. +- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController` + 🐞 Fixed - Mentions overlay now doesn't overflow when there is not enough height available @@ -58,7 +95,12 @@ ✅ Added -- Videos can now be auto-played in `FullScreenMedia`, by setting the `autoplayVideos` argument to true. +- Videos can now be auto-played in `FullScreenMedia` +- Extra customisation options for `MessageInput` + +🔄 Changed + +- Add `didUpdateWidget` override in `MessageInput` widget to handle changes to `focusNode`. ## 3.3.2 diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 4b1a8d5da..2550e7a51 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -68,7 +68,6 @@ 59062C6EC2CCFE110AC70AB8 /* Pods-Runner.release.xcconfig */, 4684439012E1DB1A82103E26 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -356,13 +355,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = EHV7XZLAHA; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -488,13 +491,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = EHV7XZLAHA; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -515,13 +522,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = EHV7XZLAHA; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 6b5a299f6..ab8d0dfc8 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -87,7 +87,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 { @@ -98,13 +99,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(attachmentLimit: 3), + StreamMessageInput(attachmentLimit: 3), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 1fd3ccb6a..6f4c51e60 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -84,7 +84,7 @@ class _SplitViewState extends State { ); } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, this.onTap, @@ -92,22 +92,26 @@ class ChannelListPage extends StatelessWidget { final void Function(Channel)? onTap; + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + @override Widget build(BuildContext context) => Scaffold( - body: ChannelsBloc( - child: ChannelListView( - onChannelTap: onTap != null - ? (channel, _) { - onTap!(channel); - } - : null, - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - ), + body: StreamChannelListView( + onChannelTap: widget.onTap, + controller: _listController, ), ); } @@ -121,15 +125,15 @@ class ChannelPage extends StatelessWidget { Widget build(BuildContext context) => Navigator( onGenerateRoute: (settings) => MaterialPageRoute( builder: (context) => Scaffold( - appBar: const ChannelHeader( + appBar: const StreamChannelHeader( showBackButton: false, ), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 459d86901..94524ebbe 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -1,4 +1,5 @@ // ignore_for_file: public_member_api_docs +// ignore_for_file: prefer_expression_function_bodies import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -27,7 +28,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// - We make [StreamChat] the root Widget of our application /// /// - We create a single [ChannelPage] widget under [StreamChat] with three -/// widgets: [ChannelHeader], [MessageListView] and [MessageInput] +/// widgets: [StreamChannelHeader], [StreamMessageListView] +/// and [StreamMessageInput] /// /// If you now run the simulator you will see a single channel UI. void main() async { @@ -66,10 +68,8 @@ class MyApp extends StatelessWidget { final Channel channel; @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( - // ignore: prefer_expression_function_bodies builder: (context, widget) { return StreamChat( client: client, @@ -90,18 +90,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: const [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6abc260e0..15f7914c4 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -1,4 +1,6 @@ // ignore_for_file: public_member_api_docs +// ignore_for_file: prefer_expression_function_bodies + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -14,7 +16,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// > Note: the SDK uses Flutter’s [Navigator] to move from one route to /// another. This allows us to avoid any boiler-plate code. /// > Of course, you can take total control of how navigation works by -/// customizing widgets like [Channel] and [ChannelList]. +/// customizing widgets like [StreamChannel] and [StreamChannelListView]. /// /// If you run the application, you will see that the first screen shows a /// list of conversations, you can open each by tapping and go back to the list. @@ -25,7 +27,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The [ChannelListPage] widget retrieves the list of channels based on a /// custom query and ordering. In this case we are showing the list of /// channels in which the current user is a member and we order them based -/// on the time they had a new message. [ChannelListView] handles pagination +/// on the time they had a new message. +/// [StreamChannelListView] handles pagination /// and updates automatically when new channels are created or when a new /// message is added to a channel. void main() async { @@ -55,40 +58,65 @@ class MyApp extends StatelessWidget { final StreamChatClient client; @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( client: client, child: child, ), - home: const ChannelListPage(), + home: ChannelListPage( + client: client, + ), ); } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, + required this.client, }) : super(key: key); + final StreamChatClient client; + @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ), ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { @@ -97,18 +125,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: const [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 538dbd685..7d994ae16 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -1,4 +1,5 @@ // ignore_for_file: public_member_api_docs +// ignore_for_file: prefer_expression_function_bodies import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -15,9 +16,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// We start by changing how channel previews are shown in the channel list /// and include the number of unread messages for each. /// -/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder]; -/// this will override the default [ChannelPreview] and allows you to create -/// one yourself. +/// We're passing a custom widget +/// to [StreamChannelListView.itemBuilder]; +/// this will override the default [StreamChannelListTile] and allows you +/// to create one yourself. /// /// There are a couple interesting things we do in this widget: /// @@ -56,7 +58,6 @@ class MyApp extends StatelessWidget { final StreamChatClient client; @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( @@ -68,31 +69,57 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - channelPreviewBuilder: _channelPreviewBuilder, - // sort: [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), - ), - ), - ); + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + void dispose() { + _listController.dispose(); + super.dispose(); } - Widget _channelPreviewBuilder(BuildContext context, Channel channel) { + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + itemBuilder: _channelPreviewBuilder, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); + + Widget _channelPreviewBuilder( + BuildContext context, + List channels, + int index, + StreamChannelListTile defaultTile, + ) { + final channel = channels[index]; final lastMessage = channel.state?.messages.reversed.firstWhereOrNull( (message) => !message.isDeleted, ); @@ -112,16 +139,17 @@ class ChannelListPage extends StatelessWidget { ), ); }, - leading: ChannelAvatar( + leading: StreamChannelAvatar( channel: channel, ), - title: ChannelName( - textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith( + title: StreamChannelName( + textStyle: StreamChannelPreviewTheme.of(context).titleStyle!.copyWith( color: StreamChatTheme.of(context) .colorTheme .textHighEmphasis .withOpacity(opacity), ), + channel: channel, ), subtitle: Text(subtitle), trailing: channel.state!.unreadCount > 0 @@ -140,16 +168,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); 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 5a05e4f9e..376b0ba84 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -1,4 +1,6 @@ // ignore_for_file: public_member_api_docs +// ignore_for_file: prefer_expression_function_bodies + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -8,8 +10,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// to create sub-conversations inside the same channel. /// /// Using threaded conversations is very simple and mostly a matter of -/// plugging the [MessageListView] to another widget that renders the widget. -/// To make this simple, such a widget only needs to build [MessageListView] +/// plugging the [StreamMessageListView] +/// to another widget that renders the widget. +/// To make this simple, such a widget only needs +/// to build [StreamMessageListView] /// with the parent attribute set to the thread’s root message. /// /// Now we can open threads and create new ones as well. If you long-press a @@ -41,7 +45,6 @@ class MyApp extends StatelessWidget { final StreamChatClient client; @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( @@ -53,28 +56,48 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), - ), - ), - ); + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + void dispose() { + _listController.dispose(); + super.dispose(); } + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); } class ChannelPage extends StatelessWidget { @@ -83,24 +106,21 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - ), - const MessageInput(), - ], - ), - ); - } + const StreamMessageInput(), + ], + ), + ); } class ThreadPage extends StatelessWidget { @@ -112,21 +132,22 @@ class ThreadPage extends StatelessWidget { final Message? parent; @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ThreadHeader( + appBar: StreamThreadHeader( parent: parent!, ), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( parentMessage: parent, ), ), - MessageInput( - parentMessage: parent, + StreamMessageInput( + messageInputController: StreamMessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index 86ac2e1fd..7977d3be5 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -1,4 +1,5 @@ // ignore_for_file: public_member_api_docs +// ignore_for_file: prefer_expression_function_bodies import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -8,7 +9,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// the SDK supports easily. /// /// Replacing the built-in message component with your own is done by passing -/// it as a builder function to the [MessageListView] widget. +/// it as a builder function to the [StreamMessageListView] widget. /// /// The message builder function will get the usual [BuildContext] argument /// as well as the [Message] object and its position inside the list. @@ -47,7 +48,6 @@ class MyApp extends StatelessWidget { final StreamChatClient client; @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return MaterialApp( builder: (context, child) => StreamChat( @@ -59,28 +59,48 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), - ), - ), - ); + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + void dispose() { + _listController.dispose(); + super.dispose(); } + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); } class ChannelPage extends StatelessWidget { @@ -89,18 +109,17 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( messageBuilder: _messageBuilder, ), ), - const MessageInput(), + const StreamMessageInput(), ], ), ); @@ -110,7 +129,7 @@ class ChannelPage extends StatelessWidget { BuildContext context, MessageDetails details, List messages, - MessageWidget _, + StreamMessageWidget _, ) { final message = details.message; final isCurrentUser = 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 19d5d1ae1..e9c0168fa 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -1,4 +1,6 @@ // ignore_for_file: public_member_api_docs +// ignore_for_file: prefer_expression_function_bodies + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -58,24 +60,24 @@ class MyApp extends StatelessWidget { final defaultTheme = StreamChatThemeData.fromTheme(themeData); final colorTheme = defaultTheme.colorTheme; final customTheme = defaultTheme.merge(StreamChatThemeData( - channelPreviewTheme: ChannelPreviewThemeData( - avatarTheme: AvatarThemeData( + channelPreviewTheme: StreamChannelPreviewThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), - messageListViewTheme: const MessageListViewThemeData( + messageListViewTheme: const StreamMessageListViewThemeData( backgroundColor: Colors.grey, backgroundImage: DecorationImage( image: AssetImage('assets/background_doodle.png'), fit: BoxFit.cover, ), ), - otherMessageTheme: MessageThemeData( + otherMessageTheme: StreamMessageThemeData( messageBackgroundColor: colorTheme.textHighEmphasis, messageTextStyle: TextStyle( color: colorTheme.barsBg, ), - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), @@ -93,28 +95,48 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), - ), - ), - ); + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + void dispose() { + _listController.dispose(); + super.dispose(); } + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); } class ChannelPage extends StatelessWidget { @@ -123,24 +145,21 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - ), - const MessageInput(), - ], - ), - ); - } + const StreamMessageInput(), + ], + ), + ); } class ThreadPage extends StatelessWidget { @@ -152,21 +171,22 @@ class ThreadPage extends StatelessWidget { final Message? parent; @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ThreadHeader( + appBar: StreamThreadHeader( parent: parent!, ), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( parentMessage: parent, ), ), - MessageInput( - parentMessage: parent, + StreamMessageInput( + messageInputController: StreamMessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index bd785a29d..ead5158ad 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -38,6 +38,10 @@ dev_dependencies: flutter_test: sdk: flutter +dependency_overrides: + stream_chat_flutter: + path: ../ + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec 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 4a85f2bb3..6852cdb63 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -1,17 +1,23 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro attachment_title} +@Deprecated("Use 'StreamAttachmentTitle' instead") +typedef AttachmentTitle = StreamAttachmentTitle; + +/// {@template attachment_title} /// Title for attachments -class AttachmentTitle extends StatelessWidget { +/// {@endtemplate} +class StreamAttachmentTitle extends StatelessWidget { /// Supply attachment and theme for constructing title - const AttachmentTitle({ + const StreamAttachmentTitle({ Key? key, required this.attachment, required this.messageTheme, }) : super(key: key); /// Theme to apply to text - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// Attachment data to display final Attachment attachment; diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index bd0aacc3d..a1aa60bc0 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -9,10 +9,16 @@ typedef InProgressBuilder = Widget Function(BuildContext, int, int); /// Widget to build on failure typedef FailedBuilder = Widget Function(BuildContext, String); +/// {@macro attachment_upload_state_builder} +@Deprecated("Use 'StreamAttachmentsUploadStateBuilder' instead") +typedef AttachmentUploadStateBuilder = StreamAttachmentUploadStateBuilder; + +/// {@template attachment_upload_state_builder} /// Widget to display attachment upload state -class AttachmentUploadStateBuilder extends StatelessWidget { - /// Constructor for creating an [AttachmentUploadStateBuilder] widget - const AttachmentUploadStateBuilder({ +/// {@endtemplate} +class StreamAttachmentUploadStateBuilder extends StatelessWidget { + /// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget + const StreamAttachmentUploadStateBuilder({ Key? key, required this.message, required this.attachment, @@ -137,7 +143,7 @@ class _PreparingState extends StatelessWidget { ), Align( alignment: Alignment.topRight, - child: UploadProgressIndicator( + child: StreamUploadProgressIndicator( uploaded: 0, total: double.maxFinite.toInt(), ), @@ -177,7 +183,7 @@ class _InProgressState extends StatelessWidget { ), Align( alignment: Alignment.topRight, - child: UploadProgressIndicator( + child: StreamUploadProgressIndicator( uploaded: sent, total: total, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart index 75a890e86..0021fc763 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart @@ -28,10 +28,16 @@ extension AttachmentSourceX on AttachmentSource { } } +/// {@macro attachment_widget} +@Deprecated("Use 'StreamAttachmentWidget' instead") +typedef AttachmentWidget = StreamAttachmentWidget; + +/// {@template attachment_widget} /// Abstract class for deriving attachment types -abstract class AttachmentWidget extends StatelessWidget { +/// {@endtemplate} +abstract class StreamAttachmentWidget extends StatelessWidget { /// Constructor for creating attachment widget - const AttachmentWidget({ + const StreamAttachmentWidget({ Key? key, required this.message, required this.attachment, diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 0b5d580fe..6bac62e94 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -10,10 +10,16 @@ import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +/// {@macro file_attachment} +@Deprecated("Use 'StreamFileAttachment' instead") +typedef FileAttachment = StreamFileAttachment; + +/// {@template file_attachment} /// Widget for displaying file attachments -class FileAttachment extends AttachmentWidget { +/// {@endtemplate} +class StreamFileAttachment extends StreamAttachmentWidget { /// Constructor for creating a widget when attachment is of type 'file' - const FileAttachment({ + const StreamFileAttachment({ Key? key, required Message message, required Attachment attachment, @@ -157,7 +163,7 @@ class FileAttachment extends AttachmentWidget { type: MaterialType.transparency, shape: _getDefaultShape(context), child: source.when( - local: () => VideoThumbnailImage( + local: () => StreamVideoThumbnailImage( fit: BoxFit.cover, video: attachment.file!.path!, placeholderBuilder: (_) => const Center( @@ -168,7 +174,7 @@ class FileAttachment extends AttachmentWidget { ), ), ), - network: () => VideoThumbnailImage( + network: () => StreamVideoThumbnailImage( fit: BoxFit.cover, video: attachment.assetUrl!, placeholderBuilder: (_) => const Center( @@ -278,7 +284,7 @@ class FileAttachment extends AttachmentWidget { ); return attachment.uploadState.when( preparing: () => Text(fileSize(size), style: textStyle), - inProgress: (sent, total) => UploadProgressIndicator( + inProgress: (sent, total) => StreamUploadProgressIndicator( uploaded: sent, total: total, showBackground: false, diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 1974b0404..dc354432e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro giphy_attachment} +@Deprecated("Use 'StreamGiphyAttachment' instead") +typedef GiphyAttachment = StreamGiphyAttachment; + +/// {@template giphy_attachment} /// Widget for showing a GIF attachment -class GiphyAttachment extends AttachmentWidget { - /// Constructor for creating a [GiphyAttachment] widget - const GiphyAttachment({ +/// {@endtemplate} +class StreamGiphyAttachment extends StreamAttachmentWidget { + /// Constructor for creating a [StreamGiphyAttachment] widget + const StreamGiphyAttachment({ Key? key, required Message message, required Attachment attachment, @@ -39,7 +45,7 @@ class GiphyAttachment extends AttachmentWidget { if (imageUrl == null) { return const AttachmentError(); } - if (attachment.actions.isNotEmpty) { + if (attachment.actions != null && attachment.actions!.isNotEmpty) { return _buildSendingAttachment(context, imageUrl); } return _buildSentAttachment(context, imageUrl); @@ -228,7 +234,7 @@ class GiphyAttachment extends AttachmentWidget { alignment: Alignment.centerRight, child: Padding( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: VisibleFootnote(), + child: StreamVisibleFootnote(), ), ), ], @@ -243,11 +249,10 @@ class GiphyAttachment extends AttachmentWidget { final channel = StreamChannel.of(context).channel; return StreamChannel( channel: channel, - child: FullScreenMedia( - mediaAttachments: message.attachments, + child: StreamFullScreenMedia( + mediaAttachmentPackages: message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index a46ff25c5..bb4d81612 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro image_attachment} +@Deprecated("use 'StreamImageAttachment' instead") +typedef ImageAttachment = StreamImageAttachment; + +/// {@template image_attachment} /// Widget for showing an image attachment -class ImageAttachment extends AttachmentWidget { - /// Constructor for creating a [ImageAttachment] widget - const ImageAttachment({ +/// {@endtemplate} +class StreamImageAttachment extends StreamAttachmentWidget { + /// Constructor for creating a [StreamImageAttachment] widget + const StreamImageAttachment({ Key? key, required Message message, required Attachment attachment, @@ -25,8 +31,8 @@ class ImageAttachment extends AttachmentWidget { size: size, ); - /// [MessageThemeData] for showing image title - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for showing image title + final StreamMessageThemeData messageTheme; /// Flag for showing title final bool showTitle; @@ -137,12 +143,12 @@ class ImageAttachment extends AttachmentWidget { StreamChannel.of(context).channel; return StreamChannel( channel: channel, - child: FullScreenMedia( - mediaAttachments: message.attachments, + child: StreamFullScreenMedia( + mediaAttachmentPackages: + message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ); @@ -155,7 +161,7 @@ class ImageAttachment extends AttachmentWidget { ), Padding( padding: const EdgeInsets.all(8), - child: AttachmentUploadStateBuilder( + child: StreamAttachmentUploadStateBuilder( message: message, attachment: attachment, ), @@ -166,7 +172,7 @@ class ImageAttachment extends AttachmentWidget { if (showTitle && attachment.title != null) Material( color: messageTheme.messageBackgroundColor, - child: AttachmentTitle( + child: StreamAttachmentTitle( messageTheme: messageTheme, attachment: attachment, ), 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 8d4dfbfb2..00f29c029 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -2,10 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro url_attachment} +@Deprecated("Use 'StreamUrlAttachment' instead") +typedef UrlAttachment = StreamUrlAttachment; + +/// {@template url_attachment} /// Widget to display URL attachment -class UrlAttachment extends StatelessWidget { - /// Constructor for creating a [UrlAttachment] - const UrlAttachment({ +/// {@endtemplate} +class StreamUrlAttachment extends StatelessWidget { + /// Constructor for creating a [StreamUrlAttachment] + const StreamUrlAttachment({ Key? key, required this.urlAttachment, required this.hostDisplayName, @@ -26,8 +32,8 @@ class UrlAttachment extends StatelessWidget { /// Padding for text final EdgeInsets textPadding; - /// [MessageThemeData] for showing image title - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for showing image title + final StreamMessageThemeData messageTheme; /// The function called when tapping on a link final void Function(String)? onLinkTap; diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index 3df1f4513..620ae66b7 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro video_attachment} +@Deprecated("Use 'StreamVideoAttachment' instead") +typedef VideoAttachment = StreamVideoAttachment; + +/// {@template video_attachment} /// Widget for showing a video attachment -class VideoAttachment extends AttachmentWidget { - /// Constructor for creating a [VideoAttachment] widget - const VideoAttachment({ +/// {@endtemplate} +class StreamVideoAttachment extends StreamAttachmentWidget { + /// Constructor for creating a [StreamVideoAttachment] widget + const StreamVideoAttachment({ Key? key, required Message message, required Attachment attachment, @@ -23,8 +29,8 @@ class VideoAttachment extends AttachmentWidget { size: size, ); - /// [MessageThemeData] for showing title - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for showing title + final StreamMessageThemeData messageTheme; /// Callback when show message is tapped final ShowMessageCallback? onShowMessage; @@ -43,7 +49,7 @@ class VideoAttachment extends AttachmentWidget { } return _buildVideoAttachment( context, - VideoThumbnailImage( + StreamVideoThumbnailImage( video: attachment.file!.path!, height: size?.height, width: size?.width, @@ -58,7 +64,7 @@ class VideoAttachment extends AttachmentWidget { } return _buildVideoAttachment( context, - VideoThumbnailImage( + StreamVideoThumbnailImage( video: attachment.assetUrl!, height: size?.height, width: size?.width, @@ -84,12 +90,12 @@ class VideoAttachment extends AttachmentWidget { MaterialPageRoute( builder: (_) => StreamChannel( channel: channel, - child: FullScreenMedia( - mediaAttachments: message.attachments, + child: StreamFullScreenMedia( + mediaAttachmentPackages: + message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ), @@ -111,7 +117,7 @@ class VideoAttachment extends AttachmentWidget { ), Padding( padding: const EdgeInsets.all(8), - child: AttachmentUploadStateBuilder( + child: StreamAttachmentUploadStateBuilder( message: message, attachment: attachment, ), @@ -123,7 +129,7 @@ class VideoAttachment extends AttachmentWidget { if (attachment.title != null) Material( color: messageTheme.messageBackgroundColor, - child: AttachmentTitle( + child: StreamAttachmentTitle( messageTheme: messageTheme, attachment: attachment, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 4fedfc12f..cf8e65caa 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -9,14 +9,18 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; typedef AttachmentDownloader = Future Function( Attachment attachment, { ProgressCallback? progressCallback, + DownloadedPathCallback? downloadedPathCallback, }); +/// Callback to receive the path once the attachment asset is downloaded +typedef DownloadedPathCallback = void Function(String? path); + /// Widget that shows the options in the gallery view class AttachmentActionsModal extends StatelessWidget { /// Returns a new [AttachmentActionsModal] const AttachmentActionsModal({ Key? key, - required this.currentIndex, + required this.attachment, required this.message, this.onShowMessage, this.imageDownloader, @@ -28,12 +32,12 @@ class AttachmentActionsModal extends StatelessWidget { this.customActions = const [], }) : super(key: key); + /// The attachment object for which the actions are to be performed + final Attachment attachment; + /// The message containing the attachments final Message message; - /// Current page index - final int currentIndex; - /// Callback to show the message final VoidCallback? onShowMessage; @@ -58,10 +62,11 @@ class AttachmentActionsModal extends StatelessWidget { /// List of custom actions final List customActions; - /// Creates a copy of [MessageWidget] with specified attributes overridden. + /// Creates a copy of [StreamMessageWidget] with + /// specified attributes overridden. AttachmentActionsModal copyWith({ Key? key, - int? currentIndex, + Attachment? attachment, Message? message, VoidCallback? onShowMessage, AttachmentDownloader? imageDownloader, @@ -74,7 +79,7 @@ class AttachmentActionsModal extends StatelessWidget { }) => AttachmentActionsModal( key: key ?? this.key, - currentIndex: currentIndex ?? this.currentIndex, + attachment: attachment ?? this.attachment, message: message ?? this.message, onShowMessage: onShowMessage ?? this.onShowMessage, imageDownloader: imageDownloader ?? this.imageDownloader, @@ -137,7 +142,7 @@ class AttachmentActionsModal extends StatelessWidget { if (showSave) _buildButton( context, - message.attachments[currentIndex].type == 'video' + attachment.type == 'video' ? context.translations.saveVideoLabel : context.translations.saveImageLabel, StreamSvgIcon.iconSave( @@ -145,15 +150,16 @@ class AttachmentActionsModal extends StatelessWidget { color: theme.colorTheme.textLowEmphasis, ), () { - final attachment = message.attachments[currentIndex]; final isImage = attachment.type == 'image'; final Future Function( Attachment, { void Function(int, int) progressCallback, + DownloadedPathCallback downloadedPathCallback, }) saveFile = fileDownloader ?? _downloadAttachment; final Future Function( Attachment, { void Function(int, int) progressCallback, + DownloadedPathCallback downloadedPathCallback, }) saveImage = imageDownloader ?? _downloadAttachment; final downloader = isImage ? saveImage : saveFile; @@ -161,6 +167,9 @@ class AttachmentActionsModal extends StatelessWidget { ValueNotifier<_DownloadProgress?>( _DownloadProgress.initial(), ); + final downloadedPathNotifier = ValueNotifier( + null, + ); downloader( attachment, @@ -170,6 +179,9 @@ class AttachmentActionsModal extends StatelessWidget { received, ); }, + downloadedPathCallback: (String? path) { + downloadedPathNotifier.value = path; + }, ).catchError((e, stk) { progressNotifier.value = null; }); @@ -185,6 +197,7 @@ class AttachmentActionsModal extends StatelessWidget { builder: (context) => _buildDownloadProgressDialog( context, progressNotifier, + downloadedPathNotifier, ), ); }, @@ -203,8 +216,12 @@ class AttachmentActionsModal extends StatelessWidget { final channel = StreamChannel.of(context).channel; if (message.attachments.length > 1 || message.text?.isNotEmpty == true) { + final currentAttachmentIndex = + message.attachments.indexWhere( + (element) => element.id == attachment.id, + ); final remainingAttachments = [...message.attachments] - ..removeAt(currentIndex); + ..removeAt(currentAttachmentIndex); channel.updateMessage(message.copyWith( attachments: remainingAttachments, )); @@ -284,73 +301,86 @@ class AttachmentActionsModal extends StatelessWidget { Widget _buildDownloadProgressDialog( BuildContext context, ValueNotifier<_DownloadProgress?> progressNotifier, + ValueNotifier downloadedFilePathNotifier, ) { final theme = StreamChatTheme.of(context); return ValueListenableBuilder( - valueListenable: progressNotifier, - builder: (_, _DownloadProgress? progress, __) { - // Pop the dialog in case the progress is null or it's completed. - if (progress == null || progress.toProgressIndicatorValue == 1.0) { + valueListenable: downloadedFilePathNotifier, + builder: (_, String? path, __) { + final _downloadComplete = path != null && path.isNotEmpty; + // Pop the dialog in case the download has completed + if (_downloadComplete) { Future.delayed( const Duration(milliseconds: 500), () => Navigator.of(context).maybePop(), ); } - return Material( - type: MaterialType.transparency, - child: Center( - child: Container( - height: 182, - width: 182, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: theme.colorTheme.barsBg, - ), + return ValueListenableBuilder( + valueListenable: progressNotifier, + builder: (_, _DownloadProgress? progress, __) { + // Pop the dialog in case the progress is null. + if (progress == null) { + Future.delayed( + const Duration(milliseconds: 500), + () => Navigator.of(context).maybePop(), + ); + } + return Material( + type: MaterialType.transparency, child: Center( - child: progress == null - ? SizedBox( - height: 100, - width: 100, - child: StreamSvgIcon.error( - color: theme.colorTheme.disabled, - ), - ) - : progress.toProgressIndicatorValue == 1.0 + child: Container( + height: 182, + width: 182, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: theme.colorTheme.barsBg, + ), + child: Center( + child: progress == null ? SizedBox( - key: const Key('completedIcon'), - height: 160, - width: 160, - child: StreamSvgIcon.check( + height: 100, + width: 100, + child: StreamSvgIcon.error( color: theme.colorTheme.disabled, ), ) - : SizedBox( - height: 100, - width: 100, - child: Stack( - fit: StackFit.expand, - children: [ - CircularProgressIndicator( - value: progress.toProgressIndicatorValue, - strokeWidth: 8, - valueColor: AlwaysStoppedAnimation( - theme.colorTheme.accentPrimary, - ), + : _downloadComplete + ? SizedBox( + key: const Key('completedIcon'), + height: 160, + width: 160, + child: StreamSvgIcon.check( + color: theme.colorTheme.disabled, ), - Center( - child: Text( - '${progress.toPercentage}%', - style: theme.textTheme.headline.copyWith( - color: theme.colorTheme.textLowEmphasis, + ) + : SizedBox( + height: 100, + width: 100, + child: Stack( + fit: StackFit.expand, + children: [ + CircularProgressIndicator( + strokeWidth: 8, + color: theme.colorTheme.accentPrimary, + ), + Center( + child: Text( + '${progress.receivedValueInMB} MB', + style: + theme.textTheme.headline.copyWith( + color: + theme.colorTheme.textLowEmphasis, + ), + ), ), - ), + ], ), - ], - ), - ), + ), + ), + ), ), - ), - ), + ); + }, ); }, ); @@ -359,6 +389,7 @@ class AttachmentActionsModal extends StatelessWidget { Future _downloadAttachment( Attachment attachment, { ProgressCallback? progressCallback, + DownloadedPathCallback? downloadedPathCallback, }) async { String? filePath; final appDocDir = await getTemporaryDirectory(); @@ -374,6 +405,7 @@ class AttachmentActionsModal extends StatelessWidget { onReceiveProgress: progressCallback, ); final result = await ImageGallerySaver.saveFile(filePath!); + downloadedPathCallback?.call((result as Map)['filePath']); return (result as Map)['filePath']; } } @@ -387,6 +419,8 @@ class _DownloadProgress { final int total; final int received; + String get receivedValueInMB => ((received / 1024) / 1024).toStringAsFixed(2); + double get toProgressIndicatorValue => received / total; int get toPercentage => (received * 100) ~/ total; diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart index 08d8dabf0..b7cd45523 100644 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/back_button.dart @@ -50,7 +50,7 @@ class StreamBackButton extends StatelessWidget { Positioned( top: 7, right: 7, - child: UnreadIndicator( + child: StreamUnreadIndicator( cid: cid, ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index ba24a6407..cf0c74b13 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -24,7 +24,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// child: StreamChannel( /// channel: channel, /// child: Center( -/// child: ChannelImage( +/// child: ChannelAvatar( /// channel: channel, /// ), /// ), @@ -44,6 +44,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. + +@Deprecated( + "'ChannelAvatar' is deprecated and shouldn't be used. " + "Please use 'StreamChannelAvatar' instead.", +) class ChannelAvatar extends StatelessWidget { /// Instantiate a new ChannelImage const ChannelAvatar({ @@ -147,7 +152,7 @@ class ChannelAvatar extends StatelessWidget { return BetterStreamBuilder( stream: streamChat.client.state.currentUserStream.map((it) => it!), initialData: currentUser, - builder: (context, user) => UserAvatar( + builder: (context, user) => StreamUserAvatar( borderRadius: borderRadius ?? previewTheme?.borderRadius, user: user, constraints: constraints ?? previewTheme?.constraints, @@ -170,7 +175,7 @@ class ChannelAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => UserAvatar( + builder: (context, member) => StreamUserAvatar( borderRadius: borderRadius ?? previewTheme?.borderRadius, user: member.user!, constraints: constraints ?? previewTheme?.constraints, @@ -183,7 +188,7 @@ class ChannelAvatar extends StatelessWidget { } // Group conversation - return GroupAvatar( + return StreamGroupAvatar( members: otherMembers, borderRadius: borderRadius ?? previewTheme?.borderRadius, constraints: constraints ?? previewTheme?.constraints, diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 74661c08c..664384797 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Bottom Sheet with options +@Deprecated("Use 'StreamChannelInfoBottomSheet' instead") class ChannelBottomSheet extends StatefulWidget { /// Constructor for creating bottom sheet const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key); @@ -15,11 +16,12 @@ class ChannelBottomSheet extends StatefulWidget { _ChannelBottomSheetState createState() => _ChannelBottomSheetState(); } +// ignore: deprecated_member_use_from_same_package class _ChannelBottomSheetState extends State { bool _showActions = true; late StreamChannelState _streamChannelState; - late ChannelPreviewThemeData _channelPreviewThemeData; + late StreamChannelPreviewThemeData _channelPreviewThemeData; late StreamChatThemeData _streamChatThemeData; late StreamChatState _streamChatState; @@ -53,7 +55,8 @@ class _ChannelBottomSheetState extends State { Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: ChannelName( + child: StreamChannelName( + channel: channel, textStyle: _streamChatThemeData.textTheme.headlineBold, ), ), @@ -62,7 +65,7 @@ class _ChannelBottomSheetState extends State { height: 5, ), Center( - child: ChannelInfo( + child: StreamChannelInfo( showTypingIndicator: false, channel: _streamChannelState.channel, textStyle: _channelPreviewThemeData.subtitleStyle, @@ -74,7 +77,7 @@ class _ChannelBottomSheetState extends State { if (channel.isDistinct && channel.memberCount == 2) Column( children: [ - UserAvatar( + StreamUserAvatar( user: members .firstWhere( (e) => e.user?.id != userAsMember.user?.id, @@ -117,7 +120,7 @@ class _ChannelBottomSheetState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: [ - UserAvatar( + StreamUserAvatar( user: members[index].user!, constraints: const BoxConstraints.tightFor( height: 64, @@ -145,7 +148,7 @@ class _ChannelBottomSheetState extends State { const SizedBox( height: 24, ), - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.user( @@ -155,8 +158,10 @@ class _ChannelBottomSheetState extends State { title: context.translations.viewInfoLabel, onTap: widget.onViewInfoTap, ), - if (!channel.isDistinct) - OptionListTile( + if (!channel.isDistinct && + channel.ownCapabilities + .contains(PermissionType.leaveChannel)) + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.userRemove( @@ -174,8 +179,10 @@ class _ChannelBottomSheetState extends State { }); }, ), - if (isOwner) - OptionListTile( + if (isOwner && + channel.ownCapabilities + .contains(PermissionType.deleteChannel)) + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.delete( @@ -194,7 +201,7 @@ class _ChannelBottomSheetState extends State { }); }, ), - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.closeSmall( @@ -215,7 +222,7 @@ class _ChannelBottomSheetState extends State { void didChangeDependencies() { _streamChannelState = StreamChannel.of(context); _streamChatThemeData = StreamChatTheme.of(context); - _channelPreviewThemeData = ChannelPreviewTheme.of(context); + _channelPreviewThemeData = StreamChannelPreviewTheme.of(context); _streamChatState = StreamChat.of(context); super.didChangeDependencies(); } diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 2716ea007..aea899bdf 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -4,6 +4,11 @@ import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +///{@macro template_name} +@Deprecated("Use 'StreamChannelHeader' instead") +typedef ChannelHeader = StreamChannelHeader; + +/// {@template channel_header} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) /// @@ -49,9 +54,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// Modify it to change the widget appearance. -class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamChannelHeader extends StatelessWidget + implements PreferredSizeWidget { /// Creates a channel header - const ChannelHeader({ + const StreamChannelHeader({ Key? key, this.showBackButton = true, this.onBackPressed, @@ -101,13 +108,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { final Widget? leading; /// AppBar actions - /// By default it shows the [ChannelAvatar] + /// By default it shows the [StreamChannelAvatar] final List? actions; - /// The background color for this [ChannelHeader]. + /// The background color for this [StreamChannelHeader]. final Color? backgroundColor; - /// The elevation for this [ChannelHeader]. + /// The elevation for this [StreamChannelHeader]. final double elevation; @override @@ -118,7 +125,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { centerTitle: centerTitle, ); final channel = StreamChannel.of(context).channel; - final channelHeaderTheme = ChannelHeaderTheme.of(context); + final channelHeaderTheme = StreamChannelHeaderTheme.of(context); final leadingWidget = leading ?? (showBackButton @@ -128,7 +135,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { ) : const SizedBox()); - return ConnectionStatusBuilder( + return StreamConnectionStatusBuilder( statusBuilder: (context, status) { var statusString = ''; var showStatus = true; @@ -148,7 +155,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { final theme = Theme.of(context); - return InfoTile( + return StreamInfoTile( showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( @@ -165,7 +172,8 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { Padding( padding: const EdgeInsets.only(right: 10), child: Center( - child: ChannelAvatar( + child: StreamChannelAvatar( + channel: channel, borderRadius: channelHeaderTheme.avatarTheme?.borderRadius, constraints: @@ -187,12 +195,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { : CrossAxisAlignment.stretch, children: [ title ?? - ChannelName( + StreamChannelName( + channel: channel, textStyle: channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), subtitle ?? - ChannelInfo( + StreamChannelInfo( showTypingIndicator: showTypingIndicator, channel: channel, textStyle: channelHeaderTheme.subtitleStyle, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index ff068d21c..b30058b9b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -3,10 +3,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro channel_info} +@Deprecated("Use 'StreamChannelInfo' instead") +typedef ChannelInfo = StreamChannelInfo; + +/// {@template channel_info} /// Widget which shows channel info -class ChannelInfo extends StatelessWidget { - /// Constructor which creates a [ChannelInfo] widget - const ChannelInfo({ +/// {@endtemplate} +class StreamChannelInfo extends StatelessWidget { + /// Constructor which creates a [StreamChannelInfo] widget + const StreamChannelInfo({ Key? key, required this.channel, this.textStyle, @@ -32,7 +38,7 @@ class ChannelInfo extends StatelessWidget { return BetterStreamBuilder>( stream: channel.state!.membersStream, initialData: channel.state!.members, - builder: (context, data) => ConnectionStatusBuilder( + builder: (context, data) => StreamConnectionStatusBuilder( statusBuilder: (context, status) { switch (status) { case ConnectionStatus.connected: @@ -60,12 +66,13 @@ class ChannelInfo extends StatelessWidget { var text = context.translations.membersCountText(memberCount); final onlineCount = members?.where((m) => m.user?.online == true).length ?? 0; - if (onlineCount > 0) { + if (channel.ownCapabilities.contains(PermissionType.connectEvents) && + onlineCount > 0) { text += ', ${context.translations.watchersCountText(onlineCount)}'; } alternativeWidget = Text( text, - style: ChannelHeaderTheme.of(context).subtitleStyle, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, ); } else { final userId = StreamChat.of(context).currentUser?.id; @@ -93,11 +100,12 @@ class ChannelInfo extends StatelessWidget { return alternativeWidget ?? const Offstage(); } - return TypingIndicator( - parentId: parentId, - alignment: Alignment.center, - alternativeWidget: alternativeWidget, - style: textStyle, + return Align( + child: StreamTypingIndicator( + parentId: parentId, + style: textStyle, + alternativeWidget: alternativeWidget, + ), ); } 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 fbe71ff71..4835cad01 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -11,7 +11,11 @@ typedef TitleBuilder = Widget Function( StreamChatClient client, ); -/// +/// {@macro channel_list_header} +@Deprecated("Use 'StreamChannelListHeader' instead") +typedef ChannelListHeader = StreamChannelListHeader; + +/// {@template channel_list_header} /// It shows the current [StreamChatClient] status. /// /// ```dart @@ -43,11 +47,13 @@ typedef TitleBuilder = Widget Function( /// if you don't have it in the widget tree. /// /// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme] and on its [ChannelListHeaderThemeData] property. +/// [StreamChatTheme] and on its [StreamChannelListHeaderThemeData] property. /// Modify it to change the widget appearance. -class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamChannelListHeader extends StatelessWidget + implements PreferredSizeWidget { /// Instantiates a ChannelListHeader - const ChannelListHeader({ + const StreamChannelListHeader({ Key? key, this.client, this.titleBuilder, @@ -96,17 +102,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the new chat button final List? actions; - /// The background color for this [ChannelListHeader]. + /// The background color for this [StreamChannelListHeader]. final Color? backgroundColor; - /// The elevation for this [ChannelListHeader]. + /// The elevation for this [StreamChannelListHeader]. final double elevation; @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; final user = _client.state.currentUser; - return ConnectionStatusBuilder( + return StreamConnectionStatusBuilder( statusBuilder: (context, status) { var statusString = ''; var showStatus = true; @@ -125,9 +131,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } final chatThemeData = StreamChatTheme.of(context); - final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); + final channelListHeaderThemeData = + StreamChannelListHeaderTheme.of(context); final theme = Theme.of(context); - return InfoTile( + return StreamInfoTile( showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( @@ -143,7 +150,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { leading: leading ?? Center( child: user != null - ? UserAvatar( + ? StreamUserAvatar( user: user, showOnlineStatus: false, onTap: onUserAvatarTap ?? @@ -164,7 +171,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { [ StreamNeumorphicButton( child: IconButton( - icon: ConnectionStatusBuilder( + icon: StreamConnectionStatusBuilder( statusBuilder: (context, status) { Color? color; switch (status) { @@ -242,10 +249,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { const SizedBox(width: 10), Text( context.translations.searchingForNetworkText, - style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: + StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ], ); @@ -255,7 +263,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { StreamChatClient client, ) { final chatThemeData = StreamChatTheme.of(context); - final channelListHeaderTheme = ChannelListHeaderTheme.of(context); + final channelListHeaderTheme = StreamChannelListHeaderTheme.of(context); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ 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 648226ce4..f70badc12 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,8 +1,9 @@ -import 'package:collection/collection.dart'; +// 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'; -import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -12,7 +13,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?); /// Callback called when tapping on a channel typedef ChannelInfoCallback = void Function(Channel); -/// Builder used to create a custom [ChannelPreview] from a [Channel] +/// Builder used to create a custom [StreamChannelPreview] from a [Channel] typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); /// Callback for when 'View Info' is tapped @@ -53,6 +54,7 @@ typedef ViewInfoCallback = void Function(Channel); /// The widget components render the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. +@Deprecated("Use 'StreamChannelListView' instead") class ChannelListView extends StatefulWidget { /// Instantiate a new ChannelListView ChannelListView({ @@ -208,8 +210,6 @@ class ChannelListView extends StatefulWidget { } class _ChannelListViewState extends State { - final _slideController = SlidableController(); - late final _defaultController = ChannelListController(); ChannelListController get _channelListController => @@ -245,7 +245,8 @@ class _ChannelListViewState extends State { child: child, ); - final backgroundColor = ChannelListViewTheme.of(context).backgroundColor; + final backgroundColor = + StreamChannelListViewTheme.of(context).backgroundColor; if (backgroundColor != null) { return ColoredBox( @@ -270,19 +271,21 @@ class _ChannelListViewState extends State { _gridItemBuilder(context, index, channels), ); } - return ListView.separated( - padding: widget.padding, - physics: const AlwaysScrollableScrollPhysics(), - // all channels + progress loader - itemCount: channels.length + 1, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder!(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) => - _listItemBuilder(context, index, channels), + return SlidableAutoCloseBehavior( + child: ListView.separated( + padding: widget.padding, + physics: const AlwaysScrollableScrollPhysics(), + // all channels + progress loader + itemCount: channels.length + 1, + separatorBuilder: (_, index) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder!(context, index); + } + return _separatorBuilder(context, index); + }, + itemBuilder: (context, index) => + _listItemBuilder(context, index, channels), + ), ); } @@ -510,87 +513,92 @@ class _ChannelListViewState extends State { final backgroundColor = chatThemeData.colorTheme.inputBg; final channel = channels[i]; + final canDeleteChannel = + channel.ownCapabilities.contains(PermissionType.deleteChannel); + + final actionPaneChildren = + widget.swipeActions?.length ?? (canDeleteChannel ? 2 : 1); + final actionPaneExtentRatio = actionPaneChildren > 5 + ? 1 / actionPaneChildren + : actionPaneChildren * 0.2; + return StreamChannel( key: ValueKey('CHANNEL-${channel.cid}'), channel: channel, child: Slidable( - controller: _slideController, enabled: widget.swipeToAction, - actionPane: const SlidableBehindActionPane(), - actionExtentRatio: 0.12, - secondaryActions: widget.swipeActions - ?.map((e) => IconSlideAction( - color: e.color, - iconWidget: e.iconWidget, - onTap: () { - e.onTap?.call(channel); - }, - )) - .toList() ?? - [ - IconSlideAction( - color: backgroundColor, - icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed != null - ? () { - widget.onMoreDetailsPressed!(channel); - } - : () { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (context) => StreamChannel( - channel: channel, - child: ChannelBottomSheet( - onViewInfoTap: () { - widget.onViewInfoTap?.call(channel); - }, - ), - ), - ); - }, - ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.currentUser?.id, - ) - ?.role)) - IconSlideAction( - color: backgroundColor, - iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentError, - ), - onTap: widget.onDeletePressed != null - ? () { - widget.onDeletePressed?.call(channel); + endActionPane: ActionPane( + extentRatio: actionPaneExtentRatio, + motion: const BehindMotion(), + children: widget.swipeActions + ?.map((e) => CustomSlidableAction( + backgroundColor: e.color ?? Colors.white, + child: e.iconWidget, + onPressed: (_) { + e.onTap?.call(channel); + }, + )) + .toList() ?? + [ + CustomSlidableAction( + backgroundColor: backgroundColor, + onPressed: widget.onMoreDetailsPressed != null + ? (_) { + widget.onMoreDetailsPressed!(channel); } - : () async { - final res = await showConfirmationDialog( - context, - title: context.translations.deleteConversationLabel, - question: - context.translations.deleteConversationQuestion, - okText: context.translations.deleteLabel, - cancelText: context.translations.cancelLabel, - icon: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentError, + : (_) { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) => StreamChannel( + channel: channel, + child: StreamChannelInfoBottomSheet( + channel: channel, + onViewInfoTap: () { + widget.onViewInfoTap?.call(channel); + }, + ), ), ); - if (res == true) { - await channel.delete(); - } }, + child: const Icon(Icons.more_horiz), ), - ], + if (canDeleteChannel) + CustomSlidableAction( + backgroundColor: backgroundColor, + onPressed: widget.onDeletePressed != null + ? (_) { + widget.onDeletePressed?.call(channel); + } + : (_) async { + final res = await showConfirmationDialog( + context, + title: + context.translations.deleteConversationLabel, + question: context + .translations.deleteConversationQuestion, + okText: context.translations.deleteLabel, + cancelText: context.translations.cancelLabel, + icon: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentError, + ), + ); + if (res == true) { + await channel.delete(); + } + }, + child: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentError, + ), + ), + ], + ), child: widget.channelPreviewBuilder?.call(context, channel) ?? DecoratedBox( decoration: BoxDecoration( @@ -642,7 +650,7 @@ class _ChannelListViewState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ChannelAvatar( + StreamChannelAvatar( channel: channel, borderRadius: BorderRadius.circular(32), selected: selected, @@ -657,8 +665,9 @@ class _ChannelListViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: StreamChannel( channel: channel, - child: const ChannelName( - textStyle: TextStyle( + child: StreamChannelName( + channel: channel, + textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index a1e81005d..f496b28a0 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// /// The widget uses a [StreamBuilder] to render the channel information /// image as soon as it updates. +@Deprecated("Use 'StreamChannelName' instead") class ChannelName extends StatelessWidget { /// Instantiate a new ChannelName const ChannelName({ diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 125d06fea..ed5fe9f85 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@template channel_preview} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) /// @@ -13,11 +14,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// image as soon as it updates. /// /// Usually you don't use this widget as it's the default channel preview -/// used by [ChannelListView]. +/// used by [StreamChannelListView]. /// /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. +/// {@endtemplate} +@Deprecated("Use 'StreamChannelListTile' instead") class ChannelPreview extends StatelessWidget { /// Constructor for creating [ChannelPreview] const ChannelPreview({ @@ -52,7 +55,7 @@ class ChannelPreview extends StatelessWidget { final Widget? subtitle; /// Widget rendering the leading element, by default - /// it shows the [ChannelAvatar] + /// it shows the [StreamChannelAvatar] final Widget? leading; /// Widget rendering the trailing element, @@ -60,7 +63,7 @@ class ChannelPreview extends StatelessWidget { final Widget? trailing; /// Widget rendering the sending indicator, - /// by default it uses the [SendingIndicator] widget + /// by default it uses the [StreamSendingIndicator] widget final Widget? sendingIndicator; @override @@ -80,13 +83,18 @@ class ChannelPreview extends StatelessWidget { ), onTap: () => onTap?.call(channel), onLongPress: () => onLongPress?.call(channel), - leading: leading ?? ChannelAvatar(onTap: onImageTap), + leading: leading ?? + StreamChannelAvatar( + channel: channel, + onTap: onImageTap, + ), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: title ?? - ChannelName( + StreamChannelName( + channel: channel, textStyle: channelPreviewTheme.titleStyle, ), ), @@ -100,7 +108,7 @@ class ChannelPreview extends StatelessWidget { e.user!.id == channel.client.state.currentUser?.id)) { return const SizedBox(); } - return UnreadIndicator( + return StreamUnreadIndicator( cid: channel.cid, ); }, @@ -136,7 +144,7 @@ class ChannelPreview extends StatelessWidget { ))); final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; - return SendingIndicator( + return StreamSendingIndicator( message: lastMessage!, size: channelPreviewTheme.indicatorIconSize, isMessageRead: isMessageRead, @@ -204,7 +212,7 @@ class ChannelPreview extends StatelessWidget { ], ); } - return TypingIndicator( + return StreamTypingIndicator( channel: channel, alternativeWidget: _buildLastMessage(context), style: channelPreviewTheme.subtitleStyle, diff --git a/packages/stream_chat_flutter/lib/src/commands_overlay.dart b/packages/stream_chat_flutter/lib/src/commands_overlay.dart index 9f8aca1e6..6234f6969 100644 --- a/packages/stream_chat_flutter/lib/src/commands_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/commands_overlay.dart @@ -2,10 +2,17 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro commands_overlay} +@Deprecated("Use 'StreamCommandsOverlay' instead") +typedef CommandsOverlay = StreamCommandsOverlay; + +/// {@template commands_overlay} /// Overlay for displaying commands that can be used -class CommandsOverlay extends StatelessWidget { - /// Constructor for creating a [CommandsOverlay] - const CommandsOverlay({ +/// to interact with the channel. +/// {@endtemplate} +class StreamCommandsOverlay extends StatelessWidget { + /// Constructor for creating a [StreamCommandsOverlay] + const StreamCommandsOverlay({ required this.text, required this.onCommandResult, required this.size, diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index f0eca9da0..6f234547f 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro connection_status_builder} +@Deprecated("Use 'StreamConnectionStatusBuilder' instead") +typedef ConnectionStatusBuilder = StreamConnectionStatusBuilder; + +/// {@template connection_status_builder} /// Widget that builds itself based on the latest snapshot of interaction with /// a [Stream] of type [ConnectionStatus]. /// /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// in case no stream is provided. -class ConnectionStatusBuilder extends StatelessWidget { +/// {@endtemplate} +class StreamConnectionStatusBuilder extends StatelessWidget { /// Creates a new ConnectionStatusBuilder - const ConnectionStatusBuilder({ + const StreamConnectionStatusBuilder({ Key? key, required this.statusBuilder, this.connectionStatusStream, diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index c3b48158d..64396ceb3 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -3,10 +3,16 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro date_divider} +@Deprecated("Use 'StreamDateDivider' instead") +typedef DateDivider = StreamDateDivider; + +/// {@template date_divider} /// It shows a date divider depending on the date difference -class DateDivider extends StatelessWidget { - /// Constructor for creating a [DateDivider] - const DateDivider({ +/// {@endtemplate} +class StreamDateDivider extends StatelessWidget { + /// Constructor for creating a [StreamDateDivider] + const StreamDateDivider({ Key? key, required this.dateTime, this.uppercase = false, diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 09d725bb5..667d101df 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -3,10 +3,16 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; -/// Widget to display deleted message -class DeletedMessage extends StatelessWidget { - /// Constructor to create [DeletedMessage] - const DeletedMessage({ +/// {@macro deleted_message} +@Deprecated("Use 'StreamDeletedMessage' instead") +typedef DeletedMessage = StreamDeletedMessage; + +/// {@template deleted_message} +/// Widget to display deleted message. +/// {@endtemplate} +class StreamDeletedMessage extends StatelessWidget { + /// Constructor to create [StreamDeletedMessage] + const StreamDeletedMessage({ Key? key, required this.messageTheme, this.borderRadiusGeometry, @@ -16,7 +22,7 @@ class DeletedMessage extends StatelessWidget { }) : super(key: key); /// The theme of the message - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// The border radius of the message text final BorderRadiusGeometry? borderRadiusGeometry; diff --git a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart index 0a3cb634e..b4e50974c 100644 --- a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart @@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:substring_highlight/substring_highlight.dart'; +/// {@macro emoji_overlay} +@Deprecated("Use 'StreamEmojiOverlay' instead") +typedef EmojiOverlay = StreamEmojiOverlay; + +/// {@template emoji_overlay} /// Overlay for displaying emoji that can be used -class EmojiOverlay extends StatelessWidget { - /// Constructor for creating a [EmojiOverlay] - const EmojiOverlay({ +/// {@endtemplate} +class StreamEmojiOverlay extends StatelessWidget { + /// Constructor for creating a [StreamEmojiOverlay] + const StreamEmojiOverlay({ required this.query, required this.onEmojiResult, required this.size, diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index cc41614c8..5ad300395 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -46,7 +46,7 @@ extension IterableX on Iterable { extension PlatformFileX on PlatformFile { /// Converts the [PlatformFile] into [AttachmentFile] AttachmentFile get toAttachmentFile => AttachmentFile( - //ignore: avoid_redundant_argument_values + // ignore: avoid_redundant_argument_values path: kIsWeb ? null : path, name: name, bytes: bytes, @@ -226,6 +226,53 @@ extension UserListX on List { } } +/// Extensions on Message +extension MessageX on Message { + /// It replaces the user mentions with the actual user names. + Message replaceMentions({bool linkify = true}) { + var messageTextToRender = text; + for (final user in mentionedUsers.toSet()) { + final userId = user.id; + final userName = user.name; + if (linkify) { + messageTextToRender = messageTextToRender?.replaceAll( + '@$userId', + '[@$userName](@${userName.replaceAll(' ', '')})', + ); + } else { + messageTextToRender = messageTextToRender?.replaceAll( + '@$userId', + '@$userName', + ); + } + } + return copyWith(text: messageTextToRender); + } + + /// It returns the message with the translated text if available locally + Message translate(String language) => + copyWith(text: i18n?['${language}_text'] ?? text); + + /// It returns the message replacing the mentioned user names with + /// the respective user ids + Message replaceMentionsWithId() { + if (mentionedUsers.isEmpty) return this; + + var messageTextToSend = text; + if (messageTextToSend == null) return this; + + for (final user in mentionedUsers.toSet()) { + final userName = user.name; + messageTextToSend = messageTextToSend!.replaceAll( + '@$userName', + '@${user.id}', + ); + } + + return copyWith(text: messageTextToSend); + } +} + /// Extensions on [Uri] extension UriX on Uri { /// Return the URI adding the http scheme if it is missing 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 aa185597a..340392b78 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -21,13 +21,18 @@ enum ReturnActionType { /// Callback when show message is tapped typedef ShowMessageCallback = void Function(Message message, Channel channel); +/// {@macro full_screen_media} +@Deprecated("Use 'StreamFullScreenMedia' instead") +typedef FullScreenMedia = StreamFullScreenMedia; + +/// {@template full_screen_media} /// A full screen image widget -class FullScreenMedia extends StatefulWidget { +/// {@endtemplate} +class StreamFullScreenMedia extends StatefulWidget { /// Instantiate a new FullScreenImage - const FullScreenMedia({ + const StreamFullScreenMedia({ Key? key, - required this.mediaAttachments, - required this.message, + required this.mediaAttachmentPackages, this.startIndex = 0, String? userName, this.onShowMessage, @@ -37,10 +42,7 @@ class FullScreenMedia extends StatefulWidget { super(key: key); /// The url of the image - final List mediaAttachments; - - /// Message where attachments are attached - final Message message; + final List mediaAttachmentPackages; /// First index of media shown final int startIndex; @@ -60,10 +62,10 @@ class FullScreenMedia extends StatefulWidget { final bool autoplayVideos; @override - _FullScreenMediaState createState() => _FullScreenMediaState(); + _StreamFullScreenMediaState createState() => _StreamFullScreenMediaState(); } -class _FullScreenMediaState extends State +class _StreamFullScreenMediaState extends State with SingleTickerProviderStateMixin { late final AnimationController _animationController; late final PageController _pageController; @@ -94,8 +96,8 @@ class _FullScreenMediaState extends State duration: const Duration(milliseconds: 300), ); _pageController = PageController(initialPage: widget.startIndex); - for (var i = 0; i < widget.mediaAttachments.length; i++) { - final attachment = widget.mediaAttachments[i]; + for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { + final attachment = widget.mediaAttachmentPackages[i].attachment; if (attachment.type != 'video') continue; final package = VideoPackage(attachment, showControls: true); videoPackages[attachment.id] = package; @@ -108,7 +110,8 @@ class _FullScreenMediaState extends State return; } - final currentAttachment = widget.mediaAttachments[widget.startIndex]; + final currentAttachment = + widget.mediaAttachmentPackages[widget.startIndex].attachment; await Future.wait(videoPackages.values.map( (it) => it.initialize(), @@ -136,7 +139,8 @@ class _FullScreenMediaState extends State return; } - final currentAttachment = widget.mediaAttachments[val]; + final currentAttachment = + widget.mediaAttachmentPackages[val].attachment; for (final e in videoPackages.values) { if (e._attachment != currentAttachment) { @@ -151,7 +155,9 @@ class _FullScreenMediaState extends State } }, itemBuilder: (context, index) { - final attachment = widget.mediaAttachments[index]; + final currentAttachmentPackage = + widget.mediaAttachmentPackages[index]; + final attachment = currentAttachmentPackage.attachment; if (attachment.type == 'image' || attachment.type == 'giphy') { final imageUrl = attachment.imageUrl ?? attachment.assetUrl ?? @@ -168,11 +174,11 @@ class _FullScreenMediaState extends State maxScale: PhotoViewComputedScale.covered, minScale: PhotoViewComputedScale.contained, heroAttributes: PhotoViewHeroAttributes( - tag: widget.mediaAttachments, + tag: widget.mediaAttachmentPackages, ), backgroundDecoration: BoxDecoration( color: ColorTween( - begin: ChannelHeaderTheme.of(context).color, + begin: StreamChannelHeaderTheme.of(context).color, end: Colors.black, ).lerp(_curvedAnimation.value), ), @@ -212,53 +218,66 @@ class _FullScreenMediaState extends State } return const SizedBox(); }, - itemCount: widget.mediaAttachments.length, + itemCount: widget.mediaAttachmentPackages.length, ), FadeTransition( opacity: _opacityAnimation, child: ValueListenableBuilder( valueListenable: _currentPage, - builder: (context, value, child) => Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GalleryHeader( - userName: widget.userName, - sentAt: context.translations.sentAtText( - date: widget.message.createdAt, - time: widget.message.createdAt, - ), - onBackPressed: () { - Navigator.of(context).pop(); - }, - message: widget.message, - currentIndex: value, - onShowMessage: () { - widget.onShowMessage?.call( - widget.message, - StreamChannel.of(context).channel, - ); - }, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - ), - if (!widget.message.isEphemeral) - GalleryFooter( - currentPage: value, - totalPages: widget.mediaAttachments.length, - mediaAttachments: widget.mediaAttachments, - message: widget.message, - mediaSelectedCallBack: (val) { - _currentPage.value = val; - _pageController.animateToPage( - val, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, + builder: (context, value, child) { + final _currentAttachmentPackage = + widget.mediaAttachmentPackages[value]; + final _currentMessage = _currentAttachmentPackage.message; + final _currentAttachment = + _currentAttachmentPackage.attachment; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamGalleryHeader( + userName: widget.userName, + sentAt: context.translations.sentAtText( + date: widget + .mediaAttachmentPackages[_currentPage.value] + .message + .createdAt, + time: widget + .mediaAttachmentPackages[_currentPage.value] + .message + .createdAt, + ), + onBackPressed: () { + Navigator.of(context).pop(); + }, + message: _currentMessage, + attachment: _currentAttachment, + onShowMessage: () { + widget.onShowMessage?.call( + _currentMessage, + StreamChannel.of(context).channel, ); - Navigator.pop(context); }, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, ), - ], - ), + if (!_currentMessage.isEphemeral) + StreamGalleryFooter( + currentPage: value, + totalPages: widget.mediaAttachmentPackages.length, + mediaAttachmentPackages: + widget.mediaAttachmentPackages, + mediaSelectedCallBack: (val) { + _currentPage.value = val; + _pageController.animateToPage( + val, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + Navigator.pop(context); + }, + ), + ], + ); + }, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index f189dae33..24cea1f20 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -9,18 +9,24 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro gallery_footer} +@Deprecated("Use 'StreamGalleryFooter' instead") +typedef GalleryFooter = StreamGalleryFooter; + +/// {@template gallery_footer} /// Footer widget for media display -class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { - /// Creates a channel header - const GalleryFooter({ +/// {@endtemplate} +class StreamGalleryFooter extends StatefulWidget + implements PreferredSizeWidget { + /// Creates a StreamGalleryFooter + const StreamGalleryFooter({ Key? key, - required this.message, this.onBackPressed, this.onTitleTap, this.onImageTap, this.currentPage = 0, this.totalPages = 0, - this.mediaAttachments = const [], + required this.mediaAttachmentPackages, this.mediaSelectedCallBack, this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), @@ -43,30 +49,27 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { final int totalPages; /// All attachments to show - final List mediaAttachments; - - /// Message which attachments are attached to - final Message message; + final List mediaAttachmentPackages; /// Callback when media is selected final ValueChanged? mediaSelectedCallBack; - /// The background color of this [GalleryFooter]. + /// The background color of this [StreamGalleryFooter]. final Color? backgroundColor; @override - _GalleryFooterState createState() => _GalleryFooterState(); + _StreamGalleryFooterState createState() => _StreamGalleryFooterState(); @override final Size preferredSize; } -class _GalleryFooterState extends State { +class _StreamGalleryFooterState extends State { @override Widget build(BuildContext context) { const showShareButton = !kIsWeb; final mediaQueryData = MediaQuery.of(context); - final galleryFooterThemeData = GalleryFooterTheme.of(context); + final galleryFooterThemeData = StreamGalleryFooterTheme.of(context); return SizedBox.fromSize( size: Size( mediaQueryData.size.width, @@ -92,8 +95,8 @@ class _GalleryFooterState extends State { color: galleryFooterThemeData.shareIconColor, ), onPressed: () async { - final attachment = - widget.mediaAttachments[widget.currentPage]; + final attachment = widget + .mediaAttachmentPackages[widget.currentPage].attachment; final url = attachment.imageUrl ?? attachment.assetUrl ?? attachment.thumbUrl!; @@ -149,7 +152,7 @@ class _GalleryFooterState extends State { void _showPhotosModal(context) { final chatThemeData = StreamChatTheme.of(context); - final galleryFooterThemeData = GalleryFooterTheme.of(context); + final galleryFooterThemeData = StreamGalleryFooterTheme.of(context); showModalBottomSheet( context: context, barrierColor: galleryFooterThemeData.bottomSheetBarrierColor, @@ -164,7 +167,7 @@ class _GalleryFooterState extends State { builder: (context) { const crossAxisCount = 3; final noOfRowToShowInitially = - widget.mediaAttachments.length > crossAxisCount ? 2 : 1; + widget.mediaAttachmentPackages.length > crossAxisCount ? 2 : 1; final size = MediaQuery.of(context).size; final initialChildSize = 48 + (size.width * noOfRowToShowInitially) / crossAxisCount; @@ -205,7 +208,7 @@ class _GalleryFooterState extends State { child: GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: widget.mediaAttachments.length, + itemCount: widget.mediaAttachmentPackages.length, padding: const EdgeInsets.all(1), // ignore: lines_longer_than_80_chars gridDelegate: @@ -216,13 +219,16 @@ class _GalleryFooterState extends State { ), itemBuilder: (context, index) { Widget media; - final attachment = widget.mediaAttachments[index]; + final attachmentPackage = + widget.mediaAttachmentPackages[index]; + final attachment = attachmentPackage.attachment; + final message = attachmentPackage.message; if (attachment.type == 'video') { media = InkWell( onTap: () => widget.mediaSelectedCallBack!(index), child: FittedBox( fit: BoxFit.cover, - child: VideoThumbnailImage( + child: StreamVideoThumbnailImage( video: (attachment.file?.path ?? attachment.assetUrl)!, ), @@ -246,7 +252,7 @@ class _GalleryFooterState extends State { return Stack( children: [ media, - if (widget.message.user != null) + if (message.user != null) Padding( padding: const EdgeInsets.all(8), child: Container( @@ -264,8 +270,8 @@ class _GalleryFooterState extends State { ), ], ), - child: UserAvatar( - user: widget.message.user!, + child: StreamUserAvatar( + user: message.user!, constraints: BoxConstraints.tight(const Size(24, 24)), showOnlineStatus: false, diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 2325af696..4f9b83dc9 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -15,13 +15,20 @@ typedef AttachmentActionsBuilder = Widget Function( AttachmentActionsModal defaultActionsModal, ); +/// {@macro gallery_header} +@Deprecated("Use 'StreamGalleryHeader' instead") +typedef GalleryHeader = StreamGalleryHeader; + +/// {@template gallery_header} /// Header/AppBar widget for media display screen -class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamGalleryHeader extends StatelessWidget + implements PreferredSizeWidget { /// Creates a channel header - const GalleryHeader({ + const StreamGalleryHeader({ Key? key, required this.message, - this.currentIndex = 0, + required this.attachment, this.showBackButton = true, this.onBackPressed, this.onShowMessage, @@ -53,16 +60,16 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Message which attachments are attached to final Message message; + /// The attachment that's currently in focus + final Attachment attachment; + /// Username of sender final String userName; /// Text which connotes the time the message was sent final String sentAt; - /// Stores the current index of media shown - final int currentIndex; - - /// The background color of this [GalleryHeader]. + /// The background color of this [StreamGalleryHeader]. final Color? backgroundColor; /// Widget builder for attachment actions modal @@ -72,7 +79,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - final galleryHeaderThemeData = GalleryHeaderTheme.of(context); + final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context); final theme = Theme.of(context); return AppBar( toolbarTextStyle: theme.textTheme.bodyText2, @@ -139,14 +146,14 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { StreamChatTheme.of(context).galleryHeaderTheme; final defaultModal = AttachmentActionsModal( + attachment: attachment, message: message, - currentIndex: currentIndex, onShowMessage: onShowMessage, ); final effectiveModal = attachmentActionsModalBuilder?.call( context, - message.attachments[currentIndex], + attachment, defaultModal, ) ?? defaultModal; diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index 12824f45d..aca07c92e 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -3,10 +3,16 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; +/// {@macro gradient_avatar} +@Deprecated("Use 'StreamGradientAvatar' instead") +typedef GradientAvatar = StreamGradientAvatar; + +/// {@template gradient_avatar} /// Fallback user avatar with a polygon gradient overlayed with text -class GradientAvatar extends StatefulWidget { - /// Constructor for [GradientAvatar] - const GradientAvatar({ +/// {@endtemplate} +class StreamGradientAvatar extends StatefulWidget { + /// Constructor for [StreamGradientAvatar] + const StreamGradientAvatar({ Key? key, required this.name, required this.userId, @@ -19,10 +25,10 @@ class GradientAvatar extends StatefulWidget { final String userId; @override - _GradientAvatarState createState() => _GradientAvatarState(); + _StreamGradientAvatarState createState() => _StreamGradientAvatarState(); } -class _GradientAvatarState extends State { +class _StreamGradientAvatarState extends State { @override Widget build(BuildContext context) => Center( child: RepaintBoundary( diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index bf599c638..f7efa32d9 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -1,11 +1,18 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro group_avatar} +@Deprecated("Use 'StreamGroupAvatar' instead") +typedef GroupAvatar = StreamGroupAvatar; + +/// {@template group_avatar} /// Widget for constructing a group of images -class GroupAvatar extends StatelessWidget { - /// Constructor for creating a [GroupAvatar] - const GroupAvatar({ +/// {@endtemplate} +class StreamGroupAvatar extends StatelessWidget { + /// Constructor for creating a [StreamGroupAvatar] + const StreamGroupAvatar({ Key? key, + this.channel, required this.members, this.constraints, this.onTap, @@ -15,6 +22,9 @@ class GroupAvatar extends StatelessWidget { this.selectionThickness = 4, }) : super(key: key); + /// The channel of the avatar + final Channel? channel; + /// List of images to display final List members; @@ -38,7 +48,7 @@ class GroupAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - final channel = StreamChannel.of(context).channel; + final channel = this.channel ?? StreamChannel.of(context).channel; assert(channel.state != null, 'Channel ${channel.id} is not initialized'); @@ -80,7 +90,7 @@ class GroupAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => UserAvatar( + builder: (context, member) => StreamUserAvatar( showOnlineStatus: false, user: member.user!, borderRadius: BorderRadius.zero, @@ -118,7 +128,8 @@ class GroupAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => UserAvatar( + builder: (context, member) => + StreamUserAvatar( showOnlineStatus: false, user: member.user!, borderRadius: BorderRadius.zero, diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index ff67b068e..9a803c882 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro image_group} +@Deprecated("Use 'StreamImageGroup' instead") +typedef ImageGroup = StreamImageGroup; + +/// {@template image_group} /// Widget for constructing a group of images in message -class ImageGroup extends StatelessWidget { - /// Constructor for creating [ImageGroup] widget - const ImageGroup({ +/// {@endtemplate} +class StreamImageGroup extends StatelessWidget { + /// Constructor for creating [StreamImageGroup] widget + const StreamImageGroup({ Key? key, required this.images, required this.message, @@ -27,8 +33,8 @@ class ImageGroup extends StatelessWidget { /// Message which images are attached to final Message message; - /// [MessageThemeData] to apply to message - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] to apply to message + final StreamMessageThemeData messageTheme; /// Size of iamges final Size size; @@ -129,11 +135,10 @@ class ImageGroup extends StatelessWidget { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: FullScreenMedia( - mediaAttachments: images, + child: StreamFullScreenMedia( + mediaAttachmentPackages: message.getAttachmentPackageList(), startIndex: index, userName: message.user?.name, - message: message, onShowMessage: onShowMessage, ), ), @@ -142,7 +147,7 @@ class ImageGroup extends StatelessWidget { if (res != null) onReturnAction?.call(res); } - Widget _buildImage(BuildContext context, int index) => ImageAttachment( + Widget _buildImage(BuildContext context, int index) => StreamImageAttachment( attachment: images[index], size: size, message: message, diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 965d4fed5..86964bf01 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -2,10 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro info_tile} +@Deprecated("Use 'StreamInfoTile' instead") +typedef InfoTile = StreamInfoTile; + +/// {@template info_tile} /// Tile to display a message, used in stream chat to display connection status -class InfoTile extends StatelessWidget { - /// Constructor for creating an [InfoTile] widget - const InfoTile({ +/// {@endtemplate} +class StreamInfoTile extends StatelessWidget { + /// Constructor for creating an [StreamInfoTile] widget + const StreamInfoTile({ Key? key, required this.message, required this.child, diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 665ddcce2..3d12b73ba 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -1,8 +1,7 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/connection_status_builder.dart'; -import 'package:stream_chat_flutter/src/message_input.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; @@ -59,7 +58,7 @@ abstract class Translations { /// The error shown when loading messages fails String get loadingMessagesError; - /// The text for showing the result count in [MessageSearchListView] + /// The text for showing the result count in [StreamMessageSearchListView] String resultCountText(int count); /// The text for showing the message is deleted @@ -74,44 +73,48 @@ abstract class Translations { /// The text for showing there are no chats String get emptyChatMessagesText; - /// The text for showing the thread separator in case [MessageListView] + /// The text for showing the thread separator in case [StreamMessageListView] /// contains a parent message String threadSeparatorText(int replyCount); - /// The label for "connected" in [ConnectionStatusBuilder] + /// The label for "connected" in [StreamConnectionStatusBuilder] String get connectedLabel; - /// The label for "disconnected" in [ConnectionStatusBuilder] + /// The label for "disconnected" in [StreamConnectionStatusBuilder] String get disconnectedLabel; - /// The label for "reconnecting" in [ConnectionStatusBuilder] + /// The label for "reconnecting" in [StreamConnectionStatusBuilder] String get reconnectingLabel; - /// The label for also send as direct message "checkbox"" in [MessageInput] + /// The label for also send + /// as direct message "checkbox"" in [StreamMessageInput] String get alsoSendAsDirectMessageLabel; /// The label for search Gif String get searchGifLabel; + /// The label for the MessageInput hint when permission denied on sendMessage + String get sendMessagePermissionError; + /// The label for add a comment or send in case of - /// attachments inside [MessageInput] + /// attachments inside [StreamMessageInput] String get addACommentOrSendLabel; - /// The label for write a message in [MessageInput] + /// The label for write a message in [StreamMessageInput] String get writeAMessageLabel; - /// The label for slow mode enabled in [MessageInput] + /// The label for slow mode enabled in [StreamMessageInput] String get slowModeOnLabel; - /// The label for instant commands in [MessageInput] + /// The label for instant commands in [StreamMessageInput] String get instantCommandsLabel; /// The error shown in case the fi"le is too large even after compression - /// while uploading via [MessageInput] + /// while uploading via [StreamMessageInput] String fileTooLargeAfterCompressionError(double limitInMB); /// The error shown in case the file is too large - /// while uploading via [MessageInput] + /// while uploading via [StreamMessageInput] String fileTooLargeError(double limitInMB); /// The text for showing the query while searching for emojis @@ -141,6 +144,12 @@ abstract class Translations { /// The label for "OK" String get okLabel; + /// The label for a link disabled error + String get linkDisabledError; + + /// The additional info on a link disabled error + String get linkDisabledDetails; + /// The label for "add more files" String get addMoreFilesLabel; @@ -380,6 +389,10 @@ class DefaultTranslations implements Translations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; @@ -691,4 +704,11 @@ class DefaultTranslations implements Translations { @override String attachmentLimitExceedError(int limit) => """ Attachment limit exceeded: it's not possible to add more than $limit attachments"""; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } 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 81a5fa4e6..72378c1ca 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -8,21 +8,16 @@ 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'; -extension on Duration { - String format() { - final s = '$this'.split('.')[0].padLeft(8, '0'); - if (s.startsWith('00:')) { - return s.replaceFirst('00:', ''); - } - - return s; - } -} +/// {@macro media_list_view} +@Deprecated("Use 'StreamMediaListView' instead") +typedef MediaListView = StreamMediaListView; +/// {@template media_list_view} /// Constructs a list of media -class MediaListView extends StatefulWidget { - /// Constructor for creating a [MediaListView] widget - const MediaListView({ +/// {@endtemplate} +class StreamMediaListView extends StatefulWidget { + /// Constructor for creating a [StreamMediaListView] widget + const StreamMediaListView({ Key? key, this.selectedIds = const [], this.onSelect, @@ -39,10 +34,10 @@ class MediaListView extends StatefulWidget { final MediaListViewController? controller; @override - _MediaListViewState createState() => _MediaListViewState(); + _StreamMediaListViewState createState() => _StreamMediaListViewState(); } -class _MediaListViewState extends State { +class _StreamMediaListViewState extends State { var _media = []; var _currentPage = 0; final _scrollController = ScrollController(); @@ -252,3 +247,14 @@ class MediaThumbnailProvider extends ImageProvider { @override String toString() => '$runtimeType("${media.id}")'; } + +extension on Duration { + String format() { + final s = '$this'.split('.')[0].padLeft(8, '0'); + if (s.startsWith('00:')) { + return s.replaceFirst('00:', ''); + } + + return s; + } +} diff --git a/packages/stream_chat_flutter/lib/src/mention_tile.dart b/packages/stream_chat_flutter/lib/src/mention_tile.dart deleted file mode 100644 index c302ccb8f..000000000 --- a/packages/stream_chat_flutter/lib/src/mention_tile.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// This widget is used for showing user tiles for mentions -/// Use [title], [subtitle], [leading], [trailing] for -/// substituting widgets in respective positions -@Deprecated('Use `UserMentionTile` instead. Will be removed in future release') -class MentionTile extends StatelessWidget { - /// Constructor for creating a [MentionTile] widget - const MentionTile( - this.member, { - Key? key, - this.title, - this.subtitle, - this.leading, - this.trailing, - }) : super(key: key); - - /// Member to display in the tile - final Member member; - - /// Widget to display as title - final Widget? title; - - /// Widget to display below [title] - final Widget? subtitle; - - /// Widget at the start of the tile - final Widget? leading; - - /// Widget at the end of tile - final Widget? trailing; - - @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return SizedBox( - height: 56, - child: Row( - children: [ - const SizedBox( - width: 16, - ), - leading ?? - UserAvatar( - constraints: BoxConstraints.tight( - const Size( - 40, - 40, - ), - ), - user: member.user!, - ), - const SizedBox( - width: 8, - ), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title ?? - Text( - member.user!.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: chatThemeData.textTheme.bodyBold, - ), - const SizedBox( - height: 2, - ), - subtitle ?? - Text( - '@${member.userId}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: chatThemeData.textTheme.footnoteBold.copyWith( - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - ], - ), - ), - ), - trailing ?? - Padding( - padding: const EdgeInsets.only( - right: 18, - left: 8, - ), - child: StreamSvgIcon.mentions( - color: chatThemeData.colorTheme.accentPrimary, - ), - ), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_action.dart b/packages/stream_chat_flutter/lib/src/message_action.dart index 49a57b1ee..edd732f58 100644 --- a/packages/stream_chat_flutter/lib/src/message_action.dart +++ b/packages/stream_chat_flutter/lib/src/message_action.dart @@ -1,10 +1,16 @@ import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_action} +@Deprecated("Use 'StreamMessageActions' instead") +typedef MessageAction = StreamMessageAction; + +/// {@template message_action} /// Class describing a message action -class MessageAction { - /// returns a new instance of a [MessageAction] - MessageAction({ +/// {@endtemplate} +class StreamMessageAction { + /// returns a new instance of a [StreamMessageAction] + StreamMessageAction({ this.leading, this.title, this.onTap, 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 907f4096c..ffd8f3865 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -3,25 +3,31 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_actions_modal} +@Deprecated("Use 'StreamMessageActionsModal' instead") +typedef MessageActionsModal = StreamMessageActionsModal; + +/// {@template message_actions_modal} /// Constructs a modal with actions for a message -class MessageActionsModal extends StatefulWidget { - /// Constructor for creating a [MessageActionsModal] widget - const MessageActionsModal({ +/// {@endtemplate} +class StreamMessageActionsModal extends StatefulWidget { + /// Constructor for creating a [StreamMessageActionsModal] widget + const StreamMessageActionsModal({ Key? key, required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions = true, - this.showDeleteMessage = true, - this.showEditMessage = true, + this.showReactions, + this.showDeleteMessage, + this.showEditMessage, this.onReplyTap, this.onThreadReplyTap, this.showCopyMessage = true, this.showReplyMessage = true, this.showResendMessage = true, - this.showThreadReplyMessage = true, - this.showFlagButton = true, - this.showPinButton = true, + this.showThreadReplyMessage, + this.showFlagButton, + this.showPinButton, this.editMessageInputBuilder, this.reverse = false, this.customActions = const [], @@ -43,51 +49,54 @@ class MessageActionsModal extends StatefulWidget { /// Message in focus for actions final Message message; - /// [MessageThemeData] for message - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for message + final StreamMessageThemeData messageTheme; /// Flag for showing reactions - final bool showReactions; + final bool? showReactions; /// Callback when copy is tapped final OnMessageTap? onCopyTap; /// Callback when delete is tapped - final bool showDeleteMessage; + final bool? showDeleteMessage; /// Flag for showing copy action final bool showCopyMessage; /// Flag for showing edit action - final bool showEditMessage; + final bool? showEditMessage; /// Flag for showing resend action final bool showResendMessage; /// Flag for showing reply action - final bool showReplyMessage; + final bool? showReplyMessage; /// Flag for showing thread reply action - final bool showThreadReplyMessage; + final bool? showThreadReplyMessage; /// Flag for showing flag action - final bool showFlagButton; + final bool? showFlagButton; /// Flag for showing pin action - final bool showPinButton; + final bool? showPinButton; /// Flag for reversing message final bool reverse; /// List of custom actions - final List customActions; + final List customActions; @override - _MessageActionsModalState createState() => _MessageActionsModalState(); + _StreamMessageActionsModalState createState() => + _StreamMessageActionsModalState(); } -class _MessageActionsModalState extends State { +class _StreamMessageActionsModalState extends State { bool _showActions = true; + late List _userPermissions; + late bool _isMyMessage; @override Widget build(BuildContext context) => _showMessageOptionsModal(); @@ -122,6 +131,19 @@ class _MessageActionsModalState extends State { final shiftFactor = numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; + final hasEditPermission = _userPermissions.contains( + PermissionType.updateAnyMessage, + ) || + _userPermissions.contains(PermissionType.updateOwnMessage); + + final hasDeletePermission = _userPermissions.contains( + PermissionType.deleteAnyMessage, + ) || + _userPermissions.contains(PermissionType.deleteOwnMessage); + + final hasReactionPermission = + _userPermissions.contains(PermissionType.sendReaction); + final child = Center( child: SingleChildScrollView( child: Padding( @@ -131,7 +153,7 @@ class _MessageActionsModalState extends State { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - if (widget.showReactions && + if ((widget.showReactions ?? hasReactionPermission) && (widget.message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( @@ -144,7 +166,7 @@ class _MessageActionsModalState extends State { : -(1.2 - divFactor)), 0, ), - child: ReactionPicker( + child: StreamReactionPicker( message: widget.message, ), ), @@ -168,21 +190,35 @@ class _MessageActionsModalState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (widget.showReplyMessage && - widget.message.status == MessageSendingStatus.sent) + if (widget.showReplyMessage ?? + (_userPermissions + .contains(PermissionType.quoteMessage) && + widget.message.status == + MessageSendingStatus.sent)) _buildReplyButton(context), - if (widget.showThreadReplyMessage && - (widget.message.status == - MessageSendingStatus.sent) && - widget.message.parentId == null) + if (widget.showThreadReplyMessage ?? + _userPermissions + .contains(PermissionType.sendReply) && + (widget.message.status == + MessageSendingStatus.sent) && + widget.message.parentId == null) _buildThreadReplyButton(context), if (widget.showResendMessage) _buildResendMessage(context), - if (widget.showEditMessage) _buildEditMessage(context), + if (widget.showEditMessage ?? + _isMyMessage && hasEditPermission) + _buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context), - if (widget.showFlagButton) _buildFlagButton(context), - if (widget.showPinButton) _buildPinButton(context), - if (widget.showDeleteMessage) + if (widget.showFlagButton ?? + _userPermissions + .contains(PermissionType.flagMessage)) + _buildFlagButton(context), + if (widget.showPinButton ?? + _userPermissions + .contains(PermissionType.pinMessage)) + _buildPinButton(context), + if (widget.showDeleteMessage ?? + (_isMyMessage && hasDeletePermission)) _buildDeleteButton(context), ...widget.customActions .map((action) => _buildCustomAction( @@ -239,7 +275,7 @@ class _MessageActionsModalState extends State { InkWell _buildCustomAction( BuildContext context, - MessageAction messageAction, + StreamMessageAction messageAction, ) => InkWell( onTap: () { @@ -560,7 +596,7 @@ class _MessageActionsModalState extends State { elevation: 2, clipBehavior: Clip.hardEdge, isScrollControlled: true, - backgroundColor: MessageInputTheme.of(context).inputBackgroundColor, + backgroundColor: StreamMessageInputTheme.of(context).inputBackgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), @@ -602,8 +638,10 @@ class _MessageActionsModalState extends State { if (widget.editMessageInputBuilder != null) widget.editMessageInputBuilder!(context, widget.message) else - MessageInput( - editMessage: widget.message, + StreamMessageInput( + messageInputController: StreamMessageInputController( + message: widget.message, + ), preMessageSending: (m) { FocusScope.of(context).unfocus(); Navigator.pop(context); @@ -643,4 +681,13 @@ class _MessageActionsModalState extends State { ), ); } + + @override + void didChangeDependencies() { + final newStreamChannel = StreamChannel.of(context); + _userPermissions = newStreamChannel.channel.ownCapabilities; + _isMyMessage = + widget.message.user?.id == StreamChat.of(context).currentUser?.id; + super.didChangeDependencies(); + } } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 19e9574bc..b830ab37e 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'; @@ -16,15 +18,10 @@ 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/media_list_view_controller.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/video_service.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:video_compress/video_compress.dart'; - -export 'package:video_compress/video_compress.dart' show VideoQuality; /// A callback that can be passed to [MessageInput.onError]. /// @@ -60,7 +57,7 @@ typedef MentionTileBuilder = Widget Function( /// Builder function for building a user mention tile. /// -/// Use [UserMentionTile] for the default implementation. +/// Use [StreamUserMentionTile] for the default implementation. typedef UserMentionTileBuilder = Widget Function( BuildContext context, User user, @@ -156,12 +153,13 @@ const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes /// } /// ``` /// -/// You usually put this widget in the same page of a [MessageListView] +/// You usually put this widget in the same page of a [StreamMessageListView] /// as the bottom widget. /// /// The widget renders the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. +@Deprecated("Use 'StreamMessageInput' instead") class MessageInput extends StatefulWidget { /// Instantiate a new MessageInput const MessageInput({ @@ -191,8 +189,6 @@ class MessageInput extends StatefulWidget { this.mentionsTileBuilder, this.userMentionsTileBuilder, this.maxAttachmentSize = _kDefaultMaxAttachmentSize, - this.compressedVideoQuality = VideoQuality.DefaultQuality, - this.compressedVideoFrameRate = 30, this.onError, this.attachmentLimit = 10, this.onAttachmentLimitExceed, @@ -213,12 +209,6 @@ class MessageInput extends StatefulWidget { /// Message to edit final Message? editMessage; - /// Video quality to use when compressing the videos - final VideoQuality compressedVideoQuality; - - /// Frame rate to use when compressing the videos - final int compressedVideoFrameRate; - /// Max attachment size in bytes /// Defaults to 20 MB /// do not set it if you're using our default CDN @@ -339,6 +329,7 @@ class MessageInput extends StatefulWidget { } /// State of [MessageInput] +@Deprecated("Use 'StreamMessageInput' instead") class MessageInputState extends State { final _attachments = {}; final List _mentionedUsers = []; @@ -363,7 +354,7 @@ class MessageInputState extends State { widget.textEditingController ?? TextEditingController(); late StreamChatThemeData _streamChatTheme; - late MessageInputThemeData _messageInputTheme; + late StreamMessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => widget.quotedMessage != null; @@ -487,7 +478,7 @@ class MessageInputState extends State { ); } - return MultiOverlay( + return StreamMultiOverlay( childAnchor: Alignment.topCenter, overlayAnchor: Alignment.bottomCenter, overlayOptions: [ @@ -932,7 +923,7 @@ class MessageInputState extends State { if (renderObject == null) { return const Offstage(); } - return CommandsOverlay( + return StreamCommandsOverlay( channel: StreamChannel.of(context).channel, size: Size(renderObject.size.width - 16, 400), text: text, @@ -1142,41 +1133,18 @@ class MessageInputState extends State { if (mediaFile == null) return; - var file = AttachmentFile( + final file = AttachmentFile( path: mediaFile.path, size: await mediaFile.length(), bytes: mediaFile.readAsBytesSync(), ); if (file.size! > widget.maxAttachmentSize) { - if (medium.type == AssetType.video && file.path != null) { - final mediaInfo = await VideoService.compressVideo( - file.path!, - frameRate: widget.compressedVideoFrameRate, - quality: widget.compressedVideoQuality, - ); - - if (mediaInfo == null || - mediaInfo.filesize! > widget.maxAttachmentSize) { - _showErrorAlert( - context.translations.fileTooLargeAfterCompressionError( - widget.maxAttachmentSize / (1024 * 1024), - ), - ); - return; - } - file = AttachmentFile( - name: file.name, - size: mediaInfo.filesize, - bytes: await mediaInfo.file?.readAsBytes(), - path: mediaInfo.path, - ); - } else { - _showErrorAlert(context.translations.fileTooLargeError( + return _showErrorAlert( + context.translations.fileTooLargeError( widget.maxAttachmentSize / (1024 * 1024), - )); - return; - } + ), + ); } setState(() { @@ -1218,7 +1186,7 @@ class MessageInputState extends State { } return LayoutBuilder( - builder: (context, snapshot) => UserMentionsOverlay( + builder: (context, snapshot) => StreamUserMentionsOverlay( query: query, mentionAllAppUsers: widget.mentionAllAppUsers, client: StreamChat.of(context).client, @@ -1263,7 +1231,7 @@ class MessageInputState extends State { // ignore: cast_nullable_to_non_nullable final renderObject = context.findRenderObject() as RenderBox; - return EmojiOverlay( + return StreamEmojiOverlay( size: Size(renderObject.size.width - 16, 200), query: query, onEmojiResult: (emoji) { @@ -1298,7 +1266,7 @@ class MessageInputState extends State { if (!_hasQuotedMessage) return const Offstage(); final containsUrl = widget.quotedMessage!.attachments .any((element) => element.ogScrapeUrl != null); - return QuotedMessageWidget( + return StreamQuotedMessageWidget( reverse: true, showBorder: !containsUrl, message: widget.quotedMessage!, @@ -1329,10 +1297,8 @@ class MessageInputState extends State { .map( (e) => ClipRRect( borderRadius: BorderRadius.circular(10), - child: FileAttachment( - message: Message( - status: MessageSendingStatus.sending, - ), // dummy message + child: StreamFileAttachment( + message: Message(), // dummy message attachment: e, size: Size( MediaQuery.of(context).size.width * 0.65, @@ -1453,7 +1419,7 @@ class MessageInputState extends State { case 'video': return Stack( children: [ - VideoThumbnailImage( + StreamVideoThumbnailImage( height: 104, width: 104, video: (attachment.file?.path ?? attachment.assetUrl)!, @@ -1703,33 +1669,11 @@ class MessageInputState extends State { ); if (file.size! > widget.maxAttachmentSize) { - if (attachmentType == 'video' && file.path != null) { - final mediaInfo = await (VideoService.compressVideo( - file.path!, - frameRate: widget.compressedVideoFrameRate, - quality: widget.compressedVideoQuality, - ) as FutureOr); - - if (mediaInfo.filesize! > widget.maxAttachmentSize) { - _showErrorAlert( - context.translations.fileTooLargeAfterCompressionError( - widget.maxAttachmentSize / (1024 * 1024), - ), - ); - return; - } - file = AttachmentFile( - name: file.name, - size: mediaInfo.filesize, - bytes: await mediaInfo.file!.readAsBytes(), - path: mediaInfo.path, - ); - } else { - _showErrorAlert(context.translations.fileTooLargeError( + return _showErrorAlert( + context.translations.fileTooLargeError( widget.maxAttachmentSize / (1024 * 1024), - )); - return; - } + ), + ); } setState(() { @@ -1968,7 +1912,7 @@ class MessageInputState extends State { @override void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); - _messageInputTheme = MessageInputTheme.of(context); + _messageInputTheme = StreamMessageInputTheme.of(context); if (widget.editMessage == null) _startSlowMode(); if ((widget.editMessage != null || widget.initialMessage != null) && @@ -2047,7 +1991,7 @@ class _PickerWidgetState extends State<_PickerWidget> { ); } - return MediaListView( + 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 ed1ea56ea..7d77b2eae 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -10,22 +10,22 @@ import 'package:stream_chat_flutter/src/swipeable.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget builder for message -/// [defaultMessageWidget] is the default [MessageWidget] configuration +/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration /// Use [defaultMessageWidget.copyWith] to easily customize it typedef MessageBuilder = Widget Function( BuildContext, MessageDetails, List, - MessageWidget defaultMessageWidget, + StreamMessageWidget defaultMessageWidget, ); /// Widget builder for parent message -/// [defaultMessageWidget] is the default [MessageWidget] configuration +/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration /// Use [defaultMessageWidget.copyWith] to easily customize it typedef ParentMessageBuilder = Widget Function( BuildContext, Message?, - MessageWidget defaultMessageWidget, + StreamMessageWidget defaultMessageWidget, ); /// Widget builder for system message @@ -122,6 +122,11 @@ class MessageDetails { final int index; } +/// {@macro message_list_view} +@Deprecated("Use 'StreamMessageListView' instead") +typedef MessageListView = StreamMessageListView; + +/// {@template message_list_view} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png) /// @@ -130,29 +135,25 @@ class MessageDetails { /// ```dart /// class ChannelPage extends StatelessWidget { /// const ChannelPage({ -/// Key key, +/// Key? key, /// }) : super(key: key); /// /// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// appBar: ChannelHeader(), -/// body: Column( -/// children: [ -/// Expanded( -/// child: MessageListView( -/// threadBuilder: (_, parentMessage) { -/// return ThreadPage( +/// Widget build(BuildContext context) => Scaffold( +/// appBar: const StreamChannelHeader(), +/// body: Column( +/// children: [ +/// Expanded( +/// child: StreamMessageListView( +/// threadBuilder: (_, parentMessage) => ThreadPage( /// parent: parentMessage, -/// ); -/// }, +/// ), +/// ), /// ), -/// ), -/// MessageInput(), -/// ], -/// ), -/// ); -/// } +/// const StreamMessageInput(), +/// ], +/// ), +/// ); /// } /// ``` /// @@ -164,11 +165,13 @@ class MessageDetails { /// The widget components render the ui based on the first /// ancestor of type [StreamChatTheme]. /// Modify it to change the widget appearance. -class MessageListView extends StatefulWidget { - /// Instantiate a new MessageListView - const MessageListView({ +/// {@endtemplate} +class StreamMessageListView extends StatefulWidget { + /// Instantiate a new StreamMessageListView. + const StreamMessageListView({ Key? key, this.showScrollToBottom = true, + this.scrollToBottomBuilder, this.messageBuilder, this.parentMessageBuilder, this.parentMessage, @@ -195,7 +198,6 @@ class MessageListView extends StatefulWidget { this.messageFilter, this.onMessageTap, this.onSystemMessageTap, - this.pinPermissions = const [], this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, @@ -241,6 +243,25 @@ class MessageListView extends StatefulWidget { /// messages and the scroll offset is not zero final bool showScrollToBottom; + /// Function used to build a custom scroll to bottom widget + /// + /// Provides the current unread messages count and a reference + /// to the function that is executed on tap of this widget by default + /// + /// As an example: + /// MessageListView( + /// scrollToBottomBuilder: (unreadCount, defaultTapAction) { + /// return InkWell( + /// onTap: () => defaultTapAction(unreadCount), + /// child: Text('Scroll To Bottom'), + /// ); + /// }, + /// ), + final Widget Function( + int unreadCount, + Future Function(int) scrollToBottomDefaultTapAction, + )? scrollToBottomBuilder; + /// Parent message in case of a thread final Message? parentMessage; @@ -313,9 +334,6 @@ class MessageListView extends StatefulWidget { /// Called when system message is tapped final OnMessageTap? onSystemMessageTap; - /// A List of user types that have permission to pin messages - final List pinPermissions; - /// Builder used to build the thread separator in case it's a thread view final WidgetBuilder? threadSeparatorBuilder; @@ -333,10 +351,10 @@ class MessageListView extends StatefulWidget { final SpacingWidgetBuilder? spacingWidgetBuilder; @override - _MessageListViewState createState() => _MessageListViewState(); + _StreamMessageListViewState createState() => _StreamMessageListViewState(); } -class _MessageListViewState extends State { +class _StreamMessageListViewState extends State { ItemScrollController? _scrollController; void Function(Message)? _onThreadTap; final ValueNotifier _showScrollToBottom = ValueNotifier(false); @@ -344,6 +362,7 @@ class _MessageListViewState extends State { int? _messageListLength; StreamChannelState? streamChannel; late StreamChatThemeData _streamTheme; + late List _userPermissions; int get _initialIndex { final initialScrollIndex = widget.initialScrollIndex; @@ -463,7 +482,7 @@ class _MessageListViewState extends State { final child = Stack( alignment: Alignment.center, children: [ - ConnectionStatusBuilder( + StreamConnectionStatusBuilder( statusBuilder: (context, status) { var statusString = ''; var showStatus = true; @@ -480,7 +499,7 @@ class _MessageListViewState extends State { break; } - return InfoTile( + return StreamInfoTile( showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -726,8 +745,10 @@ class _MessageListViewState extends State { ], ); - final backgroundColor = MessageListViewTheme.of(context).backgroundColor; - final backgroundImage = MessageListViewTheme.of(context).backgroundImage; + final backgroundColor = + StreamMessageListViewTheme.of(context).backgroundColor; + final backgroundImage = + StreamMessageListViewTheme.of(context).backgroundImage; if (backgroundColor != null || backgroundImage != null) { return Container( @@ -749,7 +770,7 @@ class _MessageListViewState extends State { ) : Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: DateDivider( + child: StreamDateDivider( dateTime: message.createdAt.toLocal(), ), ); @@ -771,7 +792,7 @@ class _MessageListViewState extends State { child: Text( context.translations.threadSeparatorText(replyCount), textAlign: TextAlign.center, - style: ChannelHeaderTheme.of(context).subtitleStyle, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, ), ), ); @@ -829,7 +850,7 @@ class _MessageListViewState extends State { final message = messages[index - 2]; return widget.dateDividerBuilder != null ? widget.dateDividerBuilder!(message.createdAt.toLocal()) - : DateDivider(dateTime: message.createdAt.toLocal()); + : StreamDateDivider(dateTime: message.createdAt.toLocal()); }, ), ); @@ -858,6 +879,28 @@ class _MessageListViewState extends State { .index; } + Future scrollToBottomDefaultTapAction(int unreadCount) async { + if (unreadCount > 0) { + streamChannel!.channel.markRead(); + } + if (!_upToDate) { + _bottomPaginationActive = false; + initialAlignment = 0; + initialIndex = 0; + await streamChannel!.reloadChannel(); + + WidgetsBinding.instance?.addPostFrameCallback((_) { + _scrollController!.jumpTo(index: 0); + }); + } else { + _scrollController!.scrollTo( + index: 0, + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + ); + } + } + Widget _buildScrollToBottom() => StreamBuilder( stream: streamChannel!.channel.state!.unreadCountStream, builder: (_, snapshot) { @@ -867,6 +910,12 @@ class _MessageListViewState extends State { return const Offstage(); } final unreadCount = snapshot.data!; + if (widget.scrollToBottomBuilder != null) { + return widget.scrollToBottomBuilder!( + unreadCount, + scrollToBottomDefaultTapAction, + ); + } final showUnreadCount = unreadCount > 0 && streamChannel!.channel.state!.members.any((e) => e.userId == @@ -881,27 +930,7 @@ class _MessageListViewState extends State { children: [ FloatingActionButton( backgroundColor: _streamTheme.colorTheme.barsBg, - onPressed: () async { - if (unreadCount > 0) { - streamChannel!.channel.markRead(); - } - if (!_upToDate) { - _bottomPaginationActive = false; - initialAlignment = 0; - initialIndex = 0; - await streamChannel!.reloadChannel(); - - WidgetsBinding.instance?.addPostFrameCallback((_) { - _scrollController!.jumpTo(index: 0); - }); - } else { - _scrollController!.scrollTo( - index: 0, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ); - } - }, + onPressed: () => scrollToBottomDefaultTapAction(unreadCount), child: widget.reverse ? StreamSvgIcon.down( color: _streamTheme.colorTheme.textHighEmphasis, @@ -970,7 +999,7 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); - final defaultMessageWidget = MessageWidget( + final defaultMessageWidget = StreamMessageWidget( showReplyMessage: false, showResendMessage: false, showThreadReplyMessage: false, @@ -1016,7 +1045,7 @@ class _MessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - widget.pinPermissions.contains(currentUserMember.role), + _userPermissions.contains(PermissionType.pinMessage), ); if (widget.parentMessageBuilder != null) { @@ -1034,7 +1063,7 @@ class _MessageListViewState extends State { if ((message.type == 'system' || message.type == 'error') && message.text?.isNotEmpty == true) { return widget.systemMessageBuilder?.call(context, message) ?? - SystemMessage( + StreamSystemMessage( message: message, onMessageTap: (message) { if (widget.onSystemMessageTap != null) { @@ -1104,7 +1133,7 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); - Widget messageWidget = MessageWidget( + Widget messageWidget = StreamMessageWidget( message: message, reverse: isMyMessage, showReactions: !message.isDeleted, @@ -1135,7 +1164,10 @@ class _MessageListViewState extends State { }, showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, - showThreadReplyMessage: !isThreadMessage, + showThreadReplyMessage: !isThreadMessage && + streamChannel?.channel.ownCapabilities + .contains(PermissionType.sendReply) == + true, showFlagButton: !isMyMessage, borderSide: borderSide, onThreadTap: _onThreadTap, @@ -1204,7 +1236,7 @@ class _MessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - widget.pinPermissions.contains(currentUserMember.role), + _userPermissions.contains(PermissionType.pinMessage), ); if (widget.messageBuilder != null) { @@ -1217,7 +1249,7 @@ class _MessageListViewState extends State { index, ), messages, - messageWidget as MessageWidget, + messageWidget as StreamMessageWidget, ); } @@ -1286,6 +1318,7 @@ class _MessageListViewState extends State { void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); _streamTheme = StreamChatTheme.of(context); + _userPermissions = newStreamChannel.channel.ownCapabilities; if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; 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 2127a98da..f862048b9 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -5,15 +5,21 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_reactions_modal} +@Deprecated("Use 'StreamMessageReactionsModal' instead") +typedef MessageReactionsModal = StreamMessageReactionsModal; + +/// {@template message_reactions_modal} /// Modal widget for displaying message reactions -class MessageReactionsModal extends StatelessWidget { - /// Constructor for creating a [MessageReactionsModal] reactions - const MessageReactionsModal({ +/// {@endtemplate} +class StreamMessageReactionsModal extends StatelessWidget { + /// Constructor for creating a [StreamMessageReactionsModal] reactions + const StreamMessageReactionsModal({ Key? key, required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions = true, + this.showReactions, this.reverse = false, this.onUserAvatarTap, }) : super(key: key); @@ -24,14 +30,14 @@ class MessageReactionsModal extends StatelessWidget { /// Message to display reactions of final Message message; - /// [MessageThemeData] to apply to [message] - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] to apply to [message] + final StreamMessageThemeData messageTheme; /// Flag to reverse message final bool reverse; /// Flag to show reactions on message - final bool showReactions; + final bool? showReactions; /// Callback when user avatar is tapped final void Function(User)? onUserAvatarTap; @@ -40,6 +46,10 @@ class MessageReactionsModal extends StatelessWidget { Widget build(BuildContext context) { final size = MediaQuery.of(context).size; final user = StreamChat.of(context).currentUser; + final _userPermissions = StreamChannel.of(context).channel.ownCapabilities; + + final hasReactionPermission = + _userPermissions.contains(PermissionType.sendReaction); final roughMaxSize = size.width * 2 / 3; var messageTextLength = message.text!.length; @@ -71,7 +81,7 @@ class MessageReactionsModal extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (showReactions && + if ((showReactions ?? hasReactionPermission) && (message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( @@ -84,7 +94,7 @@ class MessageReactionsModal extends StatelessWidget { : -(1.2 - divFactor)), 0, ), - child: ReactionPicker( + child: StreamReactionPicker( message: message, ), ), @@ -196,7 +206,7 @@ class MessageReactionsModal extends StatelessWidget { Stack( clipBehavior: Clip.none, children: [ - UserAvatar( + StreamUserAvatar( onTap: onUserAvatarTap, user: reaction.user!, constraints: const BoxConstraints.tightFor( @@ -216,7 +226,7 @@ class MessageReactionsModal extends StatelessWidget { child: Align( alignment: reverse ? Alignment.centerRight : Alignment.centerLeft, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [reaction], flipTail: !reverse, borderColor: diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 022ec7cee..f8721d131 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@template message_search_item} /// It shows the current [Message] preview. /// /// Usually you don't use this widget as it's the default item used by @@ -10,6 +11,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. +/// {@endtemplate} +@Deprecated("Use 'StreamMessageSearchItem' instead") class MessageSearchItem extends StatelessWidget { /// Instantiate a new MessageSearchItem const MessageSearchItem({ @@ -34,10 +37,10 @@ class MessageSearchItem extends StatelessWidget { final channel = getMessageResponse.channel; final channelName = channel?.extraData['name']; final user = message.user!; - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); return ListTile( onTap: onTap, - leading: UserAvatar( + leading: StreamUserAvatar( user: user, showOnlineStatus: showOnlineStatus, constraints: const BoxConstraints.tightFor( @@ -120,7 +123,7 @@ class MessageSearchItem extends StatelessWidget { text = parts.join(' '); } - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text!, 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 76ae0e98d..4cc436f44 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'; @@ -17,7 +20,7 @@ typedef EmptyMessageSearchBuilder = Widget Function( String searchQuery, ); -/// +/// {@template message_search_list_view} /// It shows the list of searched messages. /// /// ```dart @@ -47,6 +50,8 @@ typedef EmptyMessageSearchBuilder = Widget Function( /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. +/// {@endtemplate} +@Deprecated("Use 'StreamMessageSearchListView' instead") class MessageSearchListView extends StatefulWidget { /// Instantiate a new MessageSearchListView const MessageSearchListView({ @@ -168,7 +173,7 @@ class _MessageSearchListViewState extends State { if (error is Error) { print(error.stackTrace); } - return InfoTile( + return StreamInfoTile( showMessage: widget.showErrorTile, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -195,7 +200,7 @@ class _MessageSearchListViewState extends State { ); final backgroundColor = - MessageSearchListViewTheme.of(context).backgroundColor; + StreamMessageSearchListViewTheme.of(context).backgroundColor; if (backgroundColor != null) { return ColoredBox( diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 4787bb9ef..b6fee8018 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -1,12 +1,19 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_text} +@Deprecated("Use 'StreamMessageText' instead") +typedef MessageText = StreamMessageText; + +/// {@template message_text} /// Text widget to display in message -class MessageText extends StatelessWidget { - /// Constructor for creating a [MessageText] widget - const MessageText({ +/// {@endtemplate} +class StreamMessageText extends StatelessWidget { + /// Constructor for creating a [StreamMessageText] widget + const StreamMessageText({ Key? key, required this.message, required this.messageTheme, @@ -23,8 +30,8 @@ class MessageText extends StatelessWidget { /// Callback for when link is tapped final void Function(String)? onLinkTap; - /// [MessageThemeData] whose text theme is to be applied - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] whose text theme is to be applied + final StreamMessageThemeData messageTheme; @override Widget build(BuildContext context) { @@ -34,13 +41,14 @@ class MessageText extends StatelessWidget { stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'), initialData: streamChat.currentUser!.language ?? 'en', builder: (context, language) { - final translatedText = - message.i18n?['${language}_text'] ?? message.text; - final messageText = - _replaceMentions(translatedText ?? '').replaceAll('\n', '\n\n'); + final messageText = message + .translate(language) + .replaceMentions() + .text + ?.replaceAll('\n', '\n\n'); final themeData = Theme.of(context); return MarkdownBody( - data: messageText, + data: messageText ?? '', onTapLink: ( String link, String? href, @@ -80,17 +88,4 @@ class MessageText extends StatelessWidget { }, ); } - - String _replaceMentions(String text) { - var messageTextToRender = text; - for (final user in message.mentionedUsers.toSet()) { - final userId = user.id; - final userName = user.name; - messageTextToRender = messageTextToRender.replaceAll( - '@$userId', - '[@$userName](@${userName.replaceAll(' ', '')})', - ); - } - return messageTextToRender; - } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 812a7595d..beabe77f9 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -32,20 +32,26 @@ enum DisplayWidget { show, } +/// {@macro message_widget} +@Deprecated("Use 'StreamMessageWidget' instead") +typedef MessageWidget = StreamMessageWidget; + +/// {@template message_widget} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png) /// /// It shows a message with reactions, replies and user avatar. /// /// Usually you don't use this widget as it's the default message widget used by -/// [MessageListView]. +/// [StreamMessageListView]. /// /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class MessageWidget extends StatefulWidget { - /// - MessageWidget({ +/// {@endtemplate} +class StreamMessageWidget extends StatefulWidget { + /// Creates a new instance of the message widget. + StreamMessageWidget({ Key? key, required this.message, required this.messageTheme, @@ -95,14 +101,6 @@ class MessageWidget extends StatefulWidget { vertical: 8, ), this.attachmentPadding = EdgeInsets.zero, - @Deprecated(''' - allRead is now deprecated and it will be removed in future releases. - The MessageWidget now listens for read events on its own. - ''') this.allRead = false, - @Deprecated(''' - readList is now deprecated and it will be removed in future releases. - The MessageWidget now listens for read events on its own. - ''') this.readList, this.onQuotedMessageTap, this.customActions = const [], this.onAttachmentTap, @@ -121,7 +119,7 @@ class MessageWidget extends StatefulWidget { context, Material( color: messageTheme.messageBackgroundColor, - child: ImageGroup( + child: StreamImageGroup( size: Size( mediaQueryData.size.width * 0.8, mediaQueryData.size.height * 0.3, @@ -142,7 +140,7 @@ class MessageWidget extends StatefulWidget { return wrapAttachmentWidget( context, - ImageAttachment( + StreamImageAttachment( attachment: attachments[0], message: message, messageTheme: messageTheme, @@ -172,7 +170,7 @@ class MessageWidget extends StatefulWidget { Column( children: attachments.map((attachment) { final mediaQueryData = MediaQuery.of(context); - return VideoAttachment( + return StreamVideoAttachment( attachment: attachment, messageTheme: messageTheme, size: Size( @@ -204,7 +202,7 @@ class MessageWidget extends StatefulWidget { Column( children: attachments.map((attachment) { final mediaQueryData = MediaQuery.of(context); - return GiphyAttachment( + return StreamGiphyAttachment( attachment: attachment, message: message, size: Size( @@ -240,7 +238,7 @@ class MessageWidget extends StatefulWidget { final mediaQueryData = MediaQuery.of(context); return wrapAttachmentWidget( context, - FileAttachment( + StreamFileAttachment( message: message, attachment: attachment, size: Size( @@ -300,7 +298,7 @@ class MessageWidget extends StatefulWidget { final Message message; /// The message theme - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -341,9 +339,6 @@ class MessageWidget extends StatefulWidget { /// If true the widget will show the reactions final bool showReactions; - /// - final bool allRead; - /// If true the widget will show the thread reply indicator final bool showThreadReplyIndicator; @@ -356,12 +351,9 @@ class MessageWidget extends StatefulWidget { /// The function called when tapping on a link final void Function(String)? onLinkTap; - /// Used in [MessageReactionsModal] and [MessageActionsModal] + /// Used in [StreamMessageReactionsModal] and [StreamMessageActionsModal] final bool showReactionPickerIndicator; - /// List of users who read - final List? readList; - /// Callback when show message is tapped final ShowMessageCallback? onShowMessage; @@ -417,13 +409,14 @@ class MessageWidget extends StatefulWidget { final void Function(Message)? onMessageTap; /// List of custom actions shown on message long tap - final List customActions; + final List customActions; /// Customize onTap on attachment final void Function(Message message, Attachment attachment)? onAttachmentTap; - /// Creates a copy of [MessageWidget] with specified attributes overridden. - MessageWidget copyWith({ + /// Creates a copy of [StreamMessageWidget] with + /// specified attributes overridden. + StreamMessageWidget copyWith({ Key? key, void Function(User)? onMentionTap, void Function(Message)? onThreadTap, @@ -435,7 +428,7 @@ class MessageWidget extends StatefulWidget { Widget Function(BuildContext, Message)? deletedBottomRowBuilder, void Function(BuildContext, Message)? onMessageActions, Message? message, - MessageThemeData? messageTheme, + StreamMessageThemeData? messageTheme, bool? reverse, ShapeBorder? shape, ShapeBorder? attachmentShape, @@ -473,11 +466,11 @@ class MessageWidget extends StatefulWidget { bool? translateUserAvatar, OnQuotedMessageTap? onQuotedMessageTap, void Function(Message)? onMessageTap, - List? customActions, + List? customActions, void Function(Message message, Attachment attachment)? onAttachmentTap, Widget Function(BuildContext, User)? userAvatarBuilder, }) => - MessageWidget( + StreamMessageWidget( key: key ?? this.key, onMentionTap: onMentionTap ?? this.onMentionTap, onThreadTap: onThreadTap ?? this.onThreadTap, @@ -539,11 +532,11 @@ class MessageWidget extends StatefulWidget { ); @override - _MessageWidgetState createState() => _MessageWidgetState(); + _StreamMessageWidgetState createState() => _StreamMessageWidgetState(); } -class _MessageWidgetState extends State - with AutomaticKeepAliveClientMixin { +class _StreamMessageWidgetState extends State + with AutomaticKeepAliveClientMixin { bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; bool get showSendingIndicator => widget.showSendingIndicator; @@ -717,7 +710,7 @@ class _MessageWidgetState extends State ? 0 : 4.0, ), - child: DeletedMessage( + child: StreamDeletedMessage( borderRadiusGeometry: widget .borderRadiusGeometry, borderSide: @@ -854,7 +847,7 @@ class _MessageWidgetState extends State ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) : null; final chatThemeData = _streamChatTheme; - return QuotedMessageWidget( + return StreamQuotedMessageWidget( onTap: onTap, message: widget.message.quotedMessage!, messageTheme: isMyMessage @@ -1008,7 +1001,7 @@ class _MessageWidgetState extends State getWebsiteName(hostName.toLowerCase()) ?? hostName.capitalize(); - return UrlAttachment( + return StreamUrlAttachment( urlAttachment: urlAttachment, hostDisplayName: hostDisplayName, textPadding: widget.textPadding, @@ -1042,7 +1035,7 @@ class _MessageWidgetState extends State child: _shouldShowReactions ? GestureDetector( onTap: () => _showMessageReactionsModalBottomSheet(context), - child: ReactionBubble( + child: StreamReactionBubble( key: ValueKey('${widget.message.id}.reactions'), reverse: widget.reverse, flipTail: widget.reverse, @@ -1073,7 +1066,7 @@ class _MessageWidgetState extends State barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: widget.copyWith( key: const Key('MessageWidget'), message: widget.message.copyWith( @@ -1088,7 +1081,8 @@ class _MessageWidgetState extends State showSendingIndicator: false, padding: const EdgeInsets.all(0), showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent), + (widget.message.status == MessageSendingStatus.sent) && + channel.ownCapabilities.contains(PermissionType.sendReaction), showPinHighlight: false, showUserAvatar: widget.message.user!.id == channel.client.state.currentUser!.id @@ -1099,7 +1093,6 @@ class _MessageWidgetState extends State Clipboard.setData(ClipboardData(text: message.text)), messageTheme: widget.messageTheme, reverse: widget.reverse, - showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onReplyTap: widget.onReplyTap, @@ -1109,11 +1102,6 @@ class _MessageWidgetState extends State showCopyMessage: widget.showCopyMessage && !isFailedState && widget.message.text?.trim().isNotEmpty == true, - showEditMessage: widget.showEditMessage && - !isDeleteFailed && - !widget.message.attachments - .any((element) => element.type == 'giphy'), - showReactions: widget.showReactions, showReplyMessage: widget.showReplyMessage && !isFailedState && widget.onReplyTap != null, @@ -1121,7 +1109,6 @@ class _MessageWidgetState extends State !isFailedState && widget.onThreadTap != null, showFlagButton: widget.showFlagButton, - showPinButton: widget.showPinButton, customActions: widget.customActions, ), ), @@ -1136,7 +1123,7 @@ class _MessageWidgetState extends State barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, - child: MessageReactionsModal( + child: StreamMessageReactionsModal( messageWidget: widget.copyWith( key: const Key('MessageWidget'), message: widget.message.copyWith( @@ -1151,7 +1138,8 @@ class _MessageWidgetState extends State showSendingIndicator: false, padding: const EdgeInsets.all(0), showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent), + (widget.message.status == MessageSendingStatus.sent) && + channel.ownCapabilities.contains(PermissionType.sendReaction), showPinHighlight: false, showUserAvatar: widget.message.user!.id == channel.client.state.currentUser!.id @@ -1162,7 +1150,8 @@ class _MessageWidgetState extends State messageTheme: widget.messageTheme, reverse: widget.reverse, message: widget.message, - showReactions: widget.showReactions, + showReactions: widget.showReactions && + channel.ownCapabilities.contains(PermissionType.sendReaction), ), ), ); @@ -1250,6 +1239,13 @@ class _MessageWidgetState extends State final channel = StreamChannel.of(context).channel; + if (!channel.ownCapabilities.contains(PermissionType.readEvents)) { + return StreamSendingIndicator( + message: message, + size: style!.fontSize, + ); + } + return BetterStreamBuilder>( stream: channel.state?.readStream, initialData: channel.state?.read, @@ -1259,7 +1255,7 @@ class _MessageWidgetState extends State (it.lastRead.isAfter(message.createdAt) || it.lastRead.isAtSameMomentAs(message.createdAt))); final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; - Widget child = SendingIndicator( + Widget child = StreamSendingIndicator( message: message, isMessageRead: isMessageRead, size: style!.fontSize, @@ -1293,7 +1289,7 @@ class _MessageWidgetState extends State : 0, ), child: widget.userAvatarBuilder?.call(context, widget.message.user!) ?? - UserAvatar( + StreamUserAvatar( user: widget.message.user!, onTap: widget.onUserAvatarTap, constraints: widget.messageTheme.avatarTheme!.constraints, @@ -1311,7 +1307,7 @@ class _MessageWidgetState extends State padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding, child: widget.textBuilder != null ? widget.textBuilder!(context, widget.message) - : MessageText( + : StreamMessageText( onLinkTap: widget.onLinkTap, message: widget.message, onMentionTap: widget.onMentionTap, @@ -1428,7 +1424,7 @@ class _ThreadParticipants extends StatelessWidget { color: _streamChatTheme.colorTheme.barsBg, ), padding: const EdgeInsets.all(1), - child: UserAvatar( + child: StreamUserAvatar( user: user, constraints: BoxConstraints.loose(const Size.fromRadius(7)), showOnlineStatus: false, diff --git a/packages/stream_chat_flutter/lib/src/multi_overlay.dart b/packages/stream_chat_flutter/lib/src/multi_overlay.dart index 31b9d5e93..bd1ed3878 100644 --- a/packages/stream_chat_flutter/lib/src/multi_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/multi_overlay.dart @@ -2,15 +2,21 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_portal/flutter_portal.dart'; +/// {@macro multi_overlay} +@Deprecated("Use 'StreamMultiOverlay' instead") +typedef MultiOverlay = StreamMultiOverlay; + +/// {@template multi_overlay} /// Widget that renders a single overlay widget from a list of [overlayOptions] /// It shows the first one that is visible -class MultiOverlay extends StatelessWidget { +/// {@endtemplate} +class StreamMultiOverlay extends StatelessWidget { /// Constructs a new MultiOverlay widget /// [overlayOptions] - the list of overlay options /// [overlayAnchor] - the anchor relative to the overlay /// [childAnchor] - the anchor relative to the child /// [child] - the child widget - const MultiOverlay({ + const StreamMultiOverlay({ Key? key, required this.overlayOptions, required this.child, diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/option_list_tile.dart index 16bf9e971..185d40f89 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/option_list_tile.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro option_list_tile} +@Deprecated("Use 'StreamOptionListTile' instead") +typedef OptionListTile = StreamOptionListTile; + +/// {@template option_list_tile} /// List tile for [ChannelBottomSheet] -class OptionListTile extends StatelessWidget { - /// Constructor for creating [OptionListTile] - const OptionListTile({ +/// {@endtemplate} +class StreamOptionListTile extends StatelessWidget { + /// Constructor for creating [StreamOptionListTile] + const StreamOptionListTile({ Key? key, - this.title, + required this.title, this.leading, this.trailing, this.onTap, @@ -17,7 +23,7 @@ class OptionListTile extends StatelessWidget { }) : super(key: key); /// Title for tile - final String? title; + final String title; /// Leading widget (start) final Widget? leading; @@ -46,8 +52,8 @@ class OptionListTile extends StatelessWidget { return Column( children: [ Container( - color: separatorColor ?? chatThemeData.colorTheme.disabled, height: 1, + color: separatorColor ?? chatThemeData.colorTheme.disabled, ), Material( color: tileColor ?? chatThemeData.colorTheme.barsBg, @@ -57,15 +63,14 @@ class OptionListTile extends StatelessWidget { onTap: onTap, child: Row( children: [ - if (leading != null) Center(child: leading), - if (leading == null) - const SizedBox( - width: 16, - ), + if (leading != null) + Center(child: leading) + else + const SizedBox(width: 16), Expanded( flex: 4, child: Text( - title!, + title, style: titleTextStyle ?? (titleColor == null ? chatThemeData.textTheme.bodyBold 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 8994eece4..1a7bb2d08 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -10,54 +10,14 @@ typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( Attachment, ); -class _VideoAttachmentThumbnail extends StatefulWidget { - const _VideoAttachmentThumbnail({ - Key? key, - required this.attachment, - this.size = const Size(32, 32), - }) : super(key: key); - - final Size size; - final Attachment attachment; - - @override - _VideoAttachmentThumbnailState createState() => - _VideoAttachmentThumbnailState(); -} - -class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> { - late VideoPlayerController _controller; - - @override - void initState() { - super.initState(); - _controller = VideoPlayerController.network(widget.attachment.assetUrl!) - ..initialize().then((_) { - // ignore: no-empty-block - setState(() {}); //when your thumbnail will show. - }); - } - - @override - void dispose() { - super.dispose(); - _controller.dispose(); - } +/// Widget for the quoted message. +@Deprecated("Use 'StreamQuotedMessageWidget' instead") +typedef QuotedMessageWidget = StreamQuotedMessageWidget; - @override - Widget build(BuildContext context) => SizedBox( - height: widget.size.height, - width: widget.size.width, - child: _controller.value.isInitialized - ? VideoPlayer(_controller) - : const CircularProgressIndicator(), - ); -} - -/// -class QuotedMessageWidget extends StatelessWidget { - /// - const QuotedMessageWidget({ +/// Widget for the quoted message. +class StreamQuotedMessageWidget extends StatelessWidget { + /// Creates a new instance of the widget. + const StreamQuotedMessageWidget({ Key? key, required this.message, required this.messageTheme, @@ -73,7 +33,7 @@ class QuotedMessageWidget extends StatelessWidget { final Message message; /// The message theme - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -134,7 +94,7 @@ class QuotedMessageWidget extends StatelessWidget { if (_hasAttachments) _parseAttachments(context), if (msg.text!.isNotEmpty) Flexible( - child: MessageText( + child: StreamMessageText( message: msg, messageTheme: isOnlyEmoji && _containsText ? messageTheme.copyWith( @@ -231,7 +191,7 @@ class QuotedMessageWidget extends StatelessWidget { borderRadius: BorderRadius.circular(8), ); - Widget _buildUserAvatar() => UserAvatar( + Widget _buildUserAvatar() => StreamUserAvatar( user: message.user!, constraints: const BoxConstraints.tightFor( height: 24, @@ -242,7 +202,7 @@ class QuotedMessageWidget extends StatelessWidget { Map get _defaultAttachmentBuilder => { - 'image': (_, attachment) => ImageAttachment( + 'image': (_, attachment) => StreamImageAttachment( attachment: attachment, message: message, messageTheme: messageTheme, @@ -288,3 +248,47 @@ class QuotedMessageWidget extends StatelessWidget { return messageTheme.messageBackgroundColor; } } + +class _VideoAttachmentThumbnail extends StatefulWidget { + const _VideoAttachmentThumbnail({ + Key? key, + required this.attachment, + this.size = const Size(32, 32), + }) : super(key: key); + + final Size size; + final Attachment attachment; + + @override + _VideoAttachmentThumbnailState createState() => + _VideoAttachmentThumbnailState(); +} + +class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network(widget.attachment.assetUrl!) + ..initialize().then((_) { + // ignore: no-empty-block + setState(() {}); //when your thumbnail will show. + }); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + @override + Widget build(BuildContext context) => SizedBox( + height: widget.size.height, + width: widget.size.width, + child: _controller.value.isInitialized + ? VideoPlayer(_controller) + : const CircularProgressIndicator(), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index dd3d5e27a..995850011 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -4,10 +4,16 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro reaction_bubble} +@Deprecated("Use 'StreamReactionBubble' instead") +typedef ReactionBubble = StreamReactionBubble; + +/// {@template reaction_bubble} /// Creates reaction bubble widget for displaying over messages -class ReactionBubble extends StatelessWidget { - /// Constructor for creating a [ReactionBubble] - const ReactionBubble({ +/// {@endtemplate} +class StreamReactionBubble extends StatelessWidget { + /// Constructor for creating a [StreamReactionBubble] + const StreamReactionBubble({ Key? key, required this.reactions, required this.borderColor, @@ -111,7 +117,7 @@ class ReactionBubble extends StatelessWidget { } Widget _buildReaction( - List reactionIcons, + List reactionIcons, Reaction reaction, BuildContext context, ) { diff --git a/packages/stream_chat_flutter/lib/src/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/reaction_icon.dart index e675128b3..cf2cef81a 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_icon.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; /// Reaction icon data -class ReactionIcon { - /// Constructor for creating [ReactionIcon] - ReactionIcon({ +@Deprecated("Use 'StreamReactionIcon' instead") +typedef ReactionIcon = StreamReactionIcon; + +/// Reaction icon data +class StreamReactionIcon { + /// Constructor for creating [StreamReactionIcon] + StreamReactionIcon({ required this.type, required this.builder, }); diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 2991a1bb5..28053c01a 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -3,16 +3,22 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro reaction_picker} +@Deprecated("Use 'StreamReactionPicker' instead") +typedef ReactionPicker = StreamReactionPicker; + +/// {@template reaction_picker} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png) /// /// It shows a reaction picker /// /// Usually you don't use this widget as it's one of the default widgets used -/// by [MessageWidget.onMessageActions]. -class ReactionPicker extends StatefulWidget { - /// Constructor for creating a [ReactionPicker] widget - const ReactionPicker({ +/// by [StreamMessageWidget.onMessageActions]. +/// {@endtemplate} +class StreamReactionPicker extends StatefulWidget { + /// Constructor for creating a [StreamReactionPicker] widget + const StreamReactionPicker({ Key? key, required this.message, }) : super(key: key); @@ -21,10 +27,10 @@ class ReactionPicker extends StatefulWidget { final Message message; @override - _ReactionPickerState createState() => _ReactionPickerState(); + _StreamReactionPickerState createState() => _StreamReactionPickerState(); } -class _ReactionPickerState extends State +class _StreamReactionPickerState extends State with TickerProviderStateMixin { List animations = []; diff --git a/packages/stream_chat_flutter/lib/src/sending_indicator.dart b/packages/stream_chat_flutter/lib/src/sending_indicator.dart index 7f44e5b89..908c734d0 100644 --- a/packages/stream_chat_flutter/lib/src/sending_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/sending_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro sending_indicator} +@Deprecated("Use 'StreamSendingIndicator' instead") +typedef SendingIndicator = StreamSendingIndicator; + +/// {@template sending_indicator} /// Used to show the sending status of the message -class SendingIndicator extends StatelessWidget { - /// Constructor for creating a [SendingIndicator] widget - const SendingIndicator({ +/// {@endtemplate} +class StreamSendingIndicator extends StatelessWidget { + /// Constructor for creating a [StreamSendingIndicator] widget + const StreamSendingIndicator({ Key? key, required this.message, this.isMessageRead = false, diff --git a/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart b/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart new file mode 100644 index 000000000..0ede51421 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart @@ -0,0 +1,18 @@ +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// The [StreamAttachmentPackage] class is basically meant to wrap +/// individual attachments with their corresponding message +class StreamAttachmentPackage { + /// Default constructor to prepare an [StreamAttachmentPackage] object + StreamAttachmentPackage({ + required this.attachment, + required this.message, + }); + + /// This is the individual attachment + final Attachment attachment; + + /// This is the message that the attachment belongs to + /// The message object may have attachemnt(s) other than the one packaged + final Message message; +} diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index cf18e2825..acc829e28 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -132,20 +132,6 @@ class StreamChatState extends State { return defaultTheme.merge(themeData); } - // coverage:ignore-start - - /// The current user - @Deprecated('Use `.currentUser` instead, Will be removed in future releases') - User? get user => widget.client.state.currentUser; - - /// The current user as a stream - @Deprecated( - 'Use `.currentUserStream` instead, Will be removed in future releases', - ) - Stream get userStream => widget.client.state.currentUserStream; - - // coverage:ignore-end - /// The current user User? get currentUser => widget.client.state.currentUser; 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 563f3c507..32b4830f4 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -38,29 +38,29 @@ class StreamChatThemeData { /// Create a theme from scratch factory StreamChatThemeData({ Brightness? brightness, - TextTheme? textTheme, - ColorTheme? colorTheme, - ChannelListHeaderThemeData? channelListHeaderTheme, - ChannelPreviewThemeData? channelPreviewTheme, - ChannelHeaderThemeData? channelHeaderTheme, - MessageThemeData? otherMessageTheme, - MessageThemeData? ownMessageTheme, - MessageInputThemeData? messageInputTheme, + StreamTextTheme? textTheme, + StreamColorTheme? colorTheme, + StreamChannelListHeaderThemeData? channelListHeaderTheme, + StreamChannelPreviewThemeData? channelPreviewTheme, + StreamChannelHeaderThemeData? channelHeaderTheme, + StreamMessageThemeData? otherMessageTheme, + StreamMessageThemeData? ownMessageTheme, + StreamMessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? placeholderUserImage, IconThemeData? primaryIconTheme, - List? reactionIcons, - GalleryHeaderThemeData? imageHeaderTheme, - GalleryFooterThemeData? imageFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, + List? reactionIcons, + StreamGalleryHeaderThemeData? imageHeaderTheme, + StreamGalleryFooterThemeData? imageFooterTheme, + StreamMessageListViewThemeData? messageListViewTheme, + StreamChannelListViewThemeData? channelListViewTheme, + StreamUserListViewThemeData? userListViewTheme, + StreamMessageSearchListViewThemeData? messageSearchListViewTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; - textTheme ??= isDark ? TextTheme.dark() : TextTheme.light(); - colorTheme ??= isDark ? ColorTheme.dark() : ColorTheme.light(); + textTheme ??= isDark ? StreamTextTheme.dark() : StreamTextTheme.light(); + colorTheme ??= isDark ? StreamColorTheme.dark() : StreamColorTheme.light(); final defaultData = StreamChatThemeData.fromColorAndTextTheme( colorTheme, @@ -133,14 +133,14 @@ class StreamChatThemeData { /// Create theme from color and text theme factory StreamChatThemeData.fromColorAndTextTheme( - ColorTheme colorTheme, - TextTheme textTheme, + StreamColorTheme colorTheme, + StreamTextTheme textTheme, ) { final accentColor = colorTheme.accentPrimary; final iconTheme = IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5)); - final channelHeaderTheme = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( + final channelHeaderTheme = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -153,9 +153,9 @@ class StreamChatThemeData { color: const Color(0xff7A7A7A), ), ); - final channelPreviewTheme = ChannelPreviewThemeData( + final channelPreviewTheme = StreamChannelPreviewThemeData( unreadCounterColor: colorTheme.accentError, - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -176,14 +176,14 @@ class StreamChatThemeData { colorTheme: colorTheme, primaryIconTheme: iconTheme, defaultUserImage: (context, user) => Center( - child: GradientAvatar( + child: StreamGradientAvatar( name: user.name, userId: user.id, ), ), channelPreviewTheme: channelPreviewTheme, - channelListHeaderTheme: ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( + channelListHeaderTheme: StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -194,7 +194,7 @@ class StreamChatThemeData { titleStyle: textTheme.headlineBold, ), channelHeaderTheme: channelHeaderTheme, - ownMessageTheme: MessageThemeData( + ownMessageTheme: StreamMessageThemeData( messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), messageTextStyle: textTheme.body, @@ -206,7 +206,7 @@ class StreamChatThemeData { reactionsBorderColor: colorTheme.borders, reactionsMaskColor: colorTheme.appBg, messageBorderColor: colorTheme.disabled, - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -218,7 +218,7 @@ class StreamChatThemeData { ), linkBackgroundColor: colorTheme.linkBg, ), - otherMessageTheme: MessageThemeData( + otherMessageTheme: StreamMessageThemeData( reactionsBackgroundColor: colorTheme.disabled, reactionsBorderColor: colorTheme.barsBg, reactionsMaskColor: colorTheme.appBg, @@ -233,7 +233,7 @@ class StreamChatThemeData { ), messageBackgroundColor: colorTheme.barsBg, messageBorderColor: colorTheme.borders, - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -242,7 +242,7 @@ class StreamChatThemeData { ), linkBackgroundColor: colorTheme.linkBg, ), - messageInputTheme: MessageInputThemeData( + messageInputTheme: StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), actionButtonColor: colorTheme.accentPrimary, @@ -252,6 +252,7 @@ class StreamChatThemeData { sendButtonIdleColor: colorTheme.disabled, inputBackgroundColor: colorTheme.barsBg, inputTextStyle: textTheme.body, + linkHighlightColor: colorTheme.accentPrimary, idleBorderGradient: LinearGradient( colors: [ colorTheme.disabled, @@ -266,7 +267,7 @@ class StreamChatThemeData { ), ), reactionIcons: [ - ReactionIcon( + StreamReactionIcon( type: 'love', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -278,7 +279,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'like', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -290,7 +291,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'sad', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -302,7 +303,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'haha', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -314,7 +315,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'wow', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -327,7 +328,7 @@ class StreamChatThemeData { }, ), ], - galleryHeaderTheme: GalleryHeaderThemeData( + galleryHeaderTheme: StreamGalleryHeaderThemeData( closeButtonColor: colorTheme.textHighEmphasis, backgroundColor: channelHeaderTheme.color, iconMenuPointColor: colorTheme.textHighEmphasis, @@ -335,7 +336,7 @@ class StreamChatThemeData { subtitleTextStyle: channelPreviewTheme.subtitleStyle, bottomSheetBarrierColor: colorTheme.overlay, ), - galleryFooterTheme: GalleryFooterThemeData( + galleryFooterTheme: StreamGalleryFooterThemeData( backgroundColor: colorTheme.barsBg, shareIconColor: colorTheme.textHighEmphasis, titleTextStyle: textTheme.headlineBold, @@ -345,52 +346,52 @@ class StreamChatThemeData { bottomSheetPhotosTextStyle: textTheme.headlineBold, bottomSheetCloseIconColor: colorTheme.textHighEmphasis, ), - messageListViewTheme: MessageListViewThemeData( + messageListViewTheme: StreamMessageListViewThemeData( backgroundColor: colorTheme.barsBg, ), - channelListViewTheme: ChannelListViewThemeData( + channelListViewTheme: StreamChannelListViewThemeData( backgroundColor: colorTheme.appBg, ), - userListViewTheme: UserListViewThemeData( + userListViewTheme: StreamUserListViewThemeData( backgroundColor: colorTheme.appBg, ), - messageSearchListViewTheme: MessageSearchListViewThemeData( + messageSearchListViewTheme: StreamMessageSearchListViewThemeData( backgroundColor: colorTheme.appBg, ), ); } /// The text themes used in the widgets - final TextTheme textTheme; + final StreamTextTheme textTheme; /// The color themes used in the widgets - final ColorTheme colorTheme; + final StreamColorTheme colorTheme; - /// Theme of the [ChannelPreview] - final ChannelPreviewThemeData channelPreviewTheme; + /// Theme of the [StreamChannelPreview] + final StreamChannelPreviewThemeData channelPreviewTheme; - /// Theme of the [ChannelListHeader] - final ChannelListHeaderThemeData channelListHeaderTheme; + /// Theme of the [StreamChannelListHeader] + final StreamChannelListHeaderThemeData channelListHeaderTheme; /// Theme of the chat widgets dedicated to a channel header - final ChannelHeaderThemeData channelHeaderTheme; + final StreamChannelHeaderThemeData channelHeaderTheme; - /// The default style for [GalleryHeader]s below the overall + /// The default style for [StreamGalleryHeader]s below the overall /// [StreamChatTheme]. - final GalleryHeaderThemeData galleryHeaderTheme; + final StreamGalleryHeaderThemeData galleryHeaderTheme; - /// The default style for [GalleryFooter]s below the overall + /// The default style for [StreamGalleryFooter]s below the overall /// [StreamChatTheme]. - final GalleryFooterThemeData galleryFooterTheme; + final StreamGalleryFooterThemeData galleryFooterTheme; /// Theme of the current user messages - final MessageThemeData ownMessageTheme; + final StreamMessageThemeData ownMessageTheme; /// Theme of other users messages - final MessageThemeData otherMessageTheme; + final StreamMessageThemeData otherMessageTheme; - /// Theme dedicated to the [MessageInput] widget - final MessageInputThemeData messageInputTheme; + /// Theme dedicated to the [StreamMessageInput] widget + final StreamMessageInputThemeData messageInputTheme; /// The widget that will be built when the user image is unavailable final Widget Function(BuildContext, User) defaultUserImage; @@ -402,41 +403,41 @@ class StreamChatThemeData { final IconThemeData primaryIconTheme; /// Assets used for rendering reactions - final List reactionIcons; + final List reactionIcons; - /// Theme configuration for the [MessageListView] widget. - final MessageListViewThemeData messageListViewTheme; + /// Theme configuration for the [StreamMessageListView] widget. + final StreamMessageListViewThemeData messageListViewTheme; - /// Theme configuration for the [ChannelListView] widget. - final ChannelListViewThemeData channelListViewTheme; + /// Theme configuration for the [StreamChannelListView] widget. + final StreamChannelListViewThemeData channelListViewTheme; - /// Theme configuration for the [UserListView] widget. - final UserListViewThemeData userListViewTheme; + /// Theme configuration for the [StreamUserListView] widget. + final StreamUserListViewThemeData userListViewTheme; - /// Theme configuration for the [MessageSearchListView] widget. - final MessageSearchListViewThemeData messageSearchListViewTheme; + /// Theme configuration for the [StreamMessageSearchListView] widget. + final StreamMessageSearchListViewThemeData messageSearchListViewTheme; /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ - TextTheme? textTheme, - ColorTheme? colorTheme, - ChannelPreviewThemeData? channelPreviewTheme, - ChannelHeaderThemeData? channelHeaderTheme, - MessageThemeData? ownMessageTheme, - MessageThemeData? otherMessageTheme, - MessageInputThemeData? messageInputTheme, + StreamTextTheme? textTheme, + StreamColorTheme? colorTheme, + StreamChannelPreviewThemeData? channelPreviewTheme, + StreamChannelHeaderThemeData? channelHeaderTheme, + StreamMessageThemeData? ownMessageTheme, + StreamMessageThemeData? otherMessageTheme, + StreamMessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? placeholderUserImage, IconThemeData? primaryIconTheme, - ChannelListHeaderThemeData? channelListHeaderTheme, - List? reactionIcons, - GalleryHeaderThemeData? galleryHeaderTheme, - GalleryFooterThemeData? galleryFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, + StreamChannelListHeaderThemeData? channelListHeaderTheme, + List? reactionIcons, + StreamGalleryHeaderThemeData? galleryHeaderTheme, + StreamGalleryFooterThemeData? galleryFooterTheme, + StreamMessageListViewThemeData? messageListViewTheme, + StreamChannelListViewThemeData? channelListViewTheme, + StreamUserListViewThemeData? userListViewTheme, + StreamMessageSearchListViewThemeData? messageSearchListViewTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index 297810b36..4ec10aa46 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// It shows a date divider depending on the date difference -class SystemMessage extends StatelessWidget { - /// Constructor for creating a [SystemMessage] - const SystemMessage({ +/// {@macro system_message} +@Deprecated("Use 'StreamSystemMessage' instead") +typedef SystemMessage = StreamSystemMessage; + +/// {@template system_message} +/// It shows a widget for the message with a system message type. +/// {@endtemplate} +class StreamSystemMessage extends StatelessWidget { + /// Constructor for creating a [StreamSystemMessage] + const StreamSystemMessage({ Key? key, required this.message, this.onMessageTap, diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index 738fa0de6..eb2d79bfc 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -1,11 +1,17 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +/// {@macro avatar_theme_data} +@Deprecated("Use 'StreamAvatarThemeData' instead") +typedef AvatarThemeData = StreamAvatarThemeData; + +/// {@template avatar_theme_data} /// A style that overrides the default appearance of various avatar widgets. +/// {@endtemplate} // ignore: prefer-match-file-name -class AvatarThemeData with Diagnosticable { - /// Creates an [AvatarThemeData]. - const AvatarThemeData({ +class StreamAvatarThemeData with Diagnosticable { + /// Creates an [StreamAvatarThemeData]. + const StreamAvatarThemeData({ BoxConstraints? constraints, BorderRadius? borderRadius, }) : _constraints = constraints, @@ -25,12 +31,12 @@ class AvatarThemeData with Diagnosticable { /// Get border radius BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); - /// Copy this [AvatarThemeData] to another. - AvatarThemeData copyWith({ + /// Copy this [StreamAvatarThemeData] to another. + StreamAvatarThemeData copyWith({ BoxConstraints? constraints, BorderRadius? borderRadius, }) => - AvatarThemeData( + StreamAvatarThemeData( constraints: constraints ?? _constraints, borderRadius: borderRadius ?? _borderRadius, ); @@ -38,12 +44,12 @@ class AvatarThemeData with Diagnosticable { /// Linearly interpolate between two [UserAvatar] themes. /// /// All the properties must be non-null. - AvatarThemeData lerp( - AvatarThemeData a, - AvatarThemeData b, + StreamAvatarThemeData lerp( + StreamAvatarThemeData a, + StreamAvatarThemeData b, double t, ) => - AvatarThemeData( + StreamAvatarThemeData( borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), constraints: BoxConstraints.lerp(a.constraints, b.constraints, t), ); @@ -51,7 +57,7 @@ class AvatarThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is AvatarThemeData && + other is StreamAvatarThemeData && runtimeType == other.runtimeType && _constraints == other._constraints && _borderRadius == other._borderRadius; @@ -59,8 +65,8 @@ class AvatarThemeData with Diagnosticable { @override int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode; - /// Merges one [AvatarThemeData] with the another - AvatarThemeData merge(AvatarThemeData? other) { + /// Merges one [StreamAvatarThemeData] with the another + StreamAvatarThemeData merge(StreamAvatarThemeData? other) { if (other == null) return this; return copyWith( constraints: other._constraints, diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 63e208e5f..aa988c0f4 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -4,27 +4,33 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; +/// {@macro channel_header_theme} +@Deprecated("Use 'StreamChannelHeaderTheme' instead") +typedef ChannelHeaderTheme = StreamChannelHeaderTheme; + +/// {@template channel_header_theme} /// Overrides the default style of [ChannelHeader] descendants. /// /// See also: /// -/// * [ChannelHeaderThemeData], which is used to configure this theme. -class ChannelHeaderTheme extends InheritedTheme { - /// Creates a [ChannelHeaderTheme]. +/// * [StreamChannelHeaderThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamChannelHeaderTheme extends InheritedTheme { + /// Creates a [StreamChannelHeaderTheme]. /// /// The [data] parameter must not be null. - const ChannelHeaderTheme({ + const StreamChannelHeaderTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelHeaderThemeData data; + final StreamChannelHeaderThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelHeaderTheme] widget, then + /// If there is no enclosing [StreamChannelHeaderTheme] widget, then /// [StreamChatThemeData.channelTheme.channelHeaderTheme] is used. /// /// Typical usage is as follows: @@ -32,34 +38,40 @@ class ChannelHeaderTheme extends InheritedTheme { /// ```dart /// final theme = ChannelHeaderTheme.of(context); /// ``` - static ChannelHeaderThemeData of(BuildContext context) { + static StreamChannelHeaderThemeData of(BuildContext context) { final channelHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return channelHeaderTheme?.data ?? StreamChatTheme.of(context).channelHeaderTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelHeaderTheme(data: data, child: child); + StreamChannelHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelHeaderTheme oldWidget) => + bool updateShouldNotify(StreamChannelHeaderTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_header_theme_data} +@Deprecated("Use 'StreamChannelHeaderThemeData' instead") +typedef ChannelHeaderThemeData = StreamChannelHeaderThemeData; + +/// {@template channel_header_theme_data} /// A style that overrides the default appearance of [ChannelHeader]s when used -/// with [ChannelHeaderTheme] or with the overall [StreamChatTheme]'s +/// with [StreamChannelHeaderTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.channelHeaderTheme]. /// /// See also: /// -/// * [ChannelHeaderTheme], the theme which is configured with this class. +/// * [StreamChannelHeaderTheme], the theme which is configured with this class. /// * [StreamChatThemeData.channelHeaderTheme], which can be used to override /// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. -class ChannelHeaderThemeData with Diagnosticable { - /// Creates a [ChannelHeaderThemeData] - const ChannelHeaderThemeData({ +/// {@endtemplate} +class StreamChannelHeaderThemeData with Diagnosticable { + /// Creates a [StreamChannelHeaderThemeData] + const StreamChannelHeaderThemeData({ this.titleStyle, this.subtitleStyle, this.avatarTheme, @@ -73,43 +85,43 @@ class ChannelHeaderThemeData with Diagnosticable { final TextStyle? subtitleStyle; /// Theme for avatar - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; - /// Color for [ChannelHeaderThemeData] + /// Color for [StreamChannelHeaderThemeData] final Color? color; /// Copy with theme - ChannelHeaderThemeData copyWith({ + StreamChannelHeaderThemeData copyWith({ TextStyle? titleStyle, TextStyle? subtitleStyle, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? color, }) => - ChannelHeaderThemeData( + StreamChannelHeaderThemeData( titleStyle: titleStyle ?? this.titleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle, avatarTheme: avatarTheme ?? this.avatarTheme, color: color ?? this.color, ); - /// Linearly interpolate between two [ChannelHeaderThemeData]. + /// Linearly interpolate between two [StreamChannelHeaderThemeData]. /// /// All the properties must be non-null. - ChannelHeaderThemeData lerp( - ChannelHeaderThemeData a, - ChannelHeaderThemeData b, + StreamChannelHeaderThemeData lerp( + StreamChannelHeaderThemeData a, + StreamChannelHeaderThemeData b, double t, ) => - ChannelHeaderThemeData( + StreamChannelHeaderThemeData( titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), color: Color.lerp(a.color, b.color, t), ); - /// Merge with other [ChannelHeaderThemeData] - ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) { + /// Merge with other [StreamChannelHeaderThemeData] + StreamChannelHeaderThemeData merge(StreamChannelHeaderThemeData? other) { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, @@ -123,7 +135,7 @@ class ChannelHeaderThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelHeaderThemeData && + other is StreamChannelHeaderThemeData && runtimeType == other.runtimeType && titleStyle == other.titleStyle && subtitleStyle == other.subtitleStyle && diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index 3846232e5..87ba01276 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -3,27 +3,34 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +/// {@macro channel_list_header_theme} +@Deprecated("Use 'StreamChannelListHeaderTheme' instead") +typedef ChannelListHeaderTheme = StreamChannelListHeaderTheme; + +/// {@template channel_list_header_theme} /// Overrides the default style of [ChannelListHeader] descendants. /// /// See also: /// -/// * [ChannelListHeaderThemeData], which is used to configure this theme. -class ChannelListHeaderTheme extends InheritedTheme { - /// Creates a [ChannelListHeaderTheme]. +/// * [StreamChannelListHeaderThemeData], which is used +/// to configure this theme. +/// {@endtemplate} +class StreamChannelListHeaderTheme extends InheritedTheme { + /// Creates a [StreamChannelListHeaderTheme]. /// /// The [data] parameter must not be null. - const ChannelListHeaderTheme({ + const StreamChannelListHeaderTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelListHeaderThemeData data; + final StreamChannelListHeaderThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelListHeaderTheme] widget, then + /// If there is no enclosing [StreamChannelListHeaderTheme] widget, then /// [StreamChatThemeData.channelListHeaderTheme] is used. /// /// Typical usage is as follows: @@ -31,26 +38,32 @@ class ChannelListHeaderTheme extends InheritedTheme { /// ```dart /// final theme = ChannelListHeaderTheme.of(context); /// ``` - static ChannelListHeaderThemeData of(BuildContext context) { - final channelListHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); + static StreamChannelListHeaderThemeData of(BuildContext context) { + final channelListHeaderTheme = context + .dependOnInheritedWidgetOfExactType(); return channelListHeaderTheme?.data ?? StreamChatTheme.of(context).channelListHeaderTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelListHeaderTheme(data: data, child: child); + StreamChannelListHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelListHeaderTheme oldWidget) => + bool updateShouldNotify(StreamChannelListHeaderTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_list_header_theme_data} +@Deprecated("Use ''StreamChannelListHeaderThemeData' instead") +typedef ChannelListHeaderThemeData = StreamChannelListHeaderThemeData; + +/// {@template channel_list_header_theme_data} /// Theme dedicated to the [ChannelListHeader] -class ChannelListHeaderThemeData with Diagnosticable { - /// Returns a new [ChannelListHeaderThemeData] - const ChannelListHeaderThemeData({ +/// {@endtemplate} +class StreamChannelListHeaderThemeData with Diagnosticable { + /// Returns a new [StreamChannelListHeaderThemeData] + const StreamChannelListHeaderThemeData({ this.titleStyle, this.avatarTheme, this.color, @@ -60,39 +73,42 @@ class ChannelListHeaderThemeData with Diagnosticable { final TextStyle? titleStyle; /// Theme dedicated to the userAvatar - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; /// Background color of the appbar final Color? color; - /// Returns a new [ChannelListHeaderThemeData] replacing some of its + /// Returns a new [StreamChannelListHeaderThemeData] replacing some of its /// properties - ChannelListHeaderThemeData copyWith({ + StreamChannelListHeaderThemeData copyWith({ TextStyle? titleStyle, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? color, }) => - ChannelListHeaderThemeData( + StreamChannelListHeaderThemeData( titleStyle: titleStyle ?? this.titleStyle, avatarTheme: avatarTheme ?? this.avatarTheme, color: color ?? this.color, ); - /// Linearly interpolate from one [ChannelListHeaderThemeData] to another. - ChannelListHeaderThemeData lerp( - ChannelListHeaderThemeData a, - ChannelListHeaderThemeData b, + /// Linearly interpolate from one [StreamChannelListHeaderThemeData] + /// to another. + StreamChannelListHeaderThemeData lerp( + StreamChannelListHeaderThemeData a, + StreamChannelListHeaderThemeData b, double t, ) => - ChannelListHeaderThemeData( - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + StreamChannelListHeaderThemeData( + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), color: Color.lerp(a.color, b.color, t), titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), ); - /// Merges [this] [ChannelListHeaderThemeData] with the [other] - ChannelListHeaderThemeData merge(ChannelListHeaderThemeData? other) { + /// Merges [this] [StreamChannelListHeaderThemeData] with the [other] + StreamChannelListHeaderThemeData merge( + StreamChannelListHeaderThemeData? other, + ) { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, @@ -104,7 +120,7 @@ class ChannelListHeaderThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelListHeaderThemeData && + other is StreamChannelListHeaderThemeData && runtimeType == other.runtimeType && titleStyle == other.titleStyle && avatarTheme == other.avatarTheme && diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart index d310e2bc9..d3ed33eb0 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro channel_list_view_theme} +@Deprecated("Use 'StreamChannelListViewTheme' instead") +typedef ChannelListViewTheme = StreamChannelListViewTheme; + +/// {@template channel_list_view_theme} /// Overrides the default style of [ChannelListView] descendants. /// /// See also: /// -/// * [ChannelListViewThemeData], which is used to configure this theme. -class ChannelListViewTheme extends InheritedTheme { - /// Creates a [ChannelListViewTheme]. +/// * [StreamChannelListViewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamChannelListViewTheme extends InheritedTheme { + /// Creates a [StreamChannelListViewTheme]. /// /// The [data] parameter must not be null. - const ChannelListViewTheme({ + const StreamChannelListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelListViewThemeData data; + final StreamChannelListViewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelListViewTheme] widget, then + /// If there is no enclosing [StreamChannelListViewTheme] widget, then /// [StreamChatThemeData.channelListViewTheme] is used. /// /// Typical usage is as follows: @@ -30,63 +36,71 @@ class ChannelListViewTheme extends InheritedTheme { /// ```dart /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); /// ``` - static ChannelListViewThemeData of(BuildContext context) { - final channelListViewTheme = - context.dependOnInheritedWidgetOfExactType(); + static StreamChannelListViewThemeData of(BuildContext context) { + final channelListViewTheme = context + .dependOnInheritedWidgetOfExactType(); return channelListViewTheme?.data ?? StreamChatTheme.of(context).channelListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelListViewTheme(data: data, child: child); + StreamChannelListViewTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelListViewTheme oldWidget) => + bool updateShouldNotify(StreamChannelListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_list_view_theme_data} +@Deprecated("Use 'StreamChannelListViewThemeData' instead") +typedef ChannelListViewThemeData = StreamChannelListViewThemeData; + +/// {@template channel_list_view_theme_data} /// A style that overrides the default appearance of [ChannelListView]s when -/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s +/// used with [StreamChannelListViewTheme] +/// or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.channelListViewTheme]. /// /// See also: /// -/// * [ChannelListViewTheme], the theme which is configured with this class. +/// * [StreamChannelListViewTheme], the theme +/// which is configured with this class. /// * [StreamChatThemeData.channelListViewTheme], which can be used to override /// the default style for [ChannelListView]s below the overall /// [StreamChatTheme]. -class ChannelListViewThemeData with Diagnosticable { - /// Creates a [ChannelListViewThemeData]. - const ChannelListViewThemeData({ +/// {@endtemplate} +class StreamChannelListViewThemeData with Diagnosticable { + /// Creates a [StreamChannelListViewThemeData]. + const StreamChannelListViewThemeData({ this.backgroundColor, }); /// The color of the [ChannelListView] background. final Color? backgroundColor; - /// Copies this [ChannelListViewThemeData] to another. - ChannelListViewThemeData copyWith({ + /// Copies this [StreamChannelListViewThemeData] to another. + StreamChannelListViewThemeData copyWith({ Color? backgroundColor, }) => - ChannelListViewThemeData( + StreamChannelListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, ); - /// Linearly interpolate between two [ChannelListViewThemeData] themes. + /// Linearly interpolate between two [StreamChannelListViewThemeData] themes. /// /// All the properties must be non-null. - ChannelListViewThemeData lerp( - ChannelListViewThemeData a, - ChannelListViewThemeData b, + StreamChannelListViewThemeData lerp( + StreamChannelListViewThemeData a, + StreamChannelListViewThemeData b, double t, ) => - ChannelListViewThemeData( + StreamChannelListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), ); - /// Merges one [ChannelListViewThemeData] with another. - ChannelListViewThemeData merge(ChannelListViewThemeData? other) { + /// Merges one [StreamChannelListViewThemeData] with another. + StreamChannelListViewThemeData merge(StreamChannelListViewThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -96,7 +110,7 @@ class ChannelListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelListViewThemeData && + other is StreamChannelListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index ff9ef70c3..e89ce4a6e 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -3,27 +3,33 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +/// {@macro channel_preview_theme} +@Deprecated("Use 'StreamChannelPreviewTheme' instead") +typedef ChannelPreviewTheme = StreamChannelPreviewTheme; + +/// {@template channel_preview_theme} /// Overrides the default style of [ChannelPreview] descendants. /// /// See also: /// -/// * [ChannelPreviewThemeData], which is used to configure this theme. -class ChannelPreviewTheme extends InheritedTheme { - /// Creates a [ChannelPreviewTheme]. +/// * [StreamChannelPreviewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamChannelPreviewTheme extends InheritedTheme { + /// Creates a [StreamChannelPreviewTheme]. /// /// The [data] parameter must not be null. - const ChannelPreviewTheme({ + const StreamChannelPreviewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelPreviewThemeData data; + final StreamChannelPreviewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelPreviewTheme] widget, then + /// If there is no enclosing [StreamChannelPreviewTheme] widget, then /// [StreamChatThemeData.channelPreviewTheme] is used. /// /// Typical usage is as follows: @@ -31,34 +37,41 @@ class ChannelPreviewTheme extends InheritedTheme { /// ```dart /// final theme = ChannelPreviewTheme.of(context); /// ``` - static ChannelPreviewThemeData of(BuildContext context) { + static StreamChannelPreviewThemeData of(BuildContext context) { final channelPreviewTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return channelPreviewTheme?.data ?? StreamChatTheme.of(context).channelPreviewTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelPreviewTheme(data: data, child: child); + StreamChannelPreviewTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelPreviewTheme oldWidget) => + bool updateShouldNotify(StreamChannelPreviewTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_preview_theme_data} +@Deprecated("Use 'StreamChannelPreviewThemeData' instead") +typedef ChannelPreviewThemeData = StreamChannelPreviewThemeData; + +/// {@template channel_preview_theme_data} /// A style that overrides the default appearance of [ChannelPreview]s when used -/// with [ChannelPreviewTheme] or with the overall [StreamChatTheme]'s +/// with [StreamChannelPreviewTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.channelPreviewTheme]. /// /// See also: /// -/// * [ChannelPreviewTheme], the theme which is configured with this class. +/// * [StreamChannelPreviewTheme], the theme +/// which is configured with this class. /// * [StreamChatThemeData.channelPreviewTheme], which can be used to override /// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. -class ChannelPreviewThemeData with Diagnosticable { - /// Creates a [ChannelPreviewThemeData]. - const ChannelPreviewThemeData({ +/// {@endtemplate} +class StreamChannelPreviewThemeData with Diagnosticable { + /// Creates a [StreamChannelPreviewThemeData]. + const StreamChannelPreviewThemeData({ this.titleStyle, this.subtitleStyle, this.lastMessageAtStyle, @@ -77,7 +90,7 @@ class ChannelPreviewThemeData with Diagnosticable { final TextStyle? lastMessageAtStyle; /// Avatar theme - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; /// Unread counter color final Color? unreadCounterColor; @@ -86,15 +99,15 @@ class ChannelPreviewThemeData with Diagnosticable { final double? indicatorIconSize; /// Copy with theme - ChannelPreviewThemeData copyWith({ + StreamChannelPreviewThemeData copyWith({ TextStyle? titleStyle, TextStyle? subtitleStyle, TextStyle? lastMessageAtStyle, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? unreadCounterColor, double? indicatorIconSize, }) => - ChannelPreviewThemeData( + StreamChannelPreviewThemeData( titleStyle: titleStyle ?? this.titleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle, lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, @@ -103,15 +116,15 @@ class ChannelPreviewThemeData with Diagnosticable { indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, ); - /// Linearly interpolate one [ChannelPreviewThemeData] to another. - ChannelPreviewThemeData lerp( - ChannelPreviewThemeData a, - ChannelPreviewThemeData b, + /// Linearly interpolate one [StreamChannelPreviewThemeData] to another. + StreamChannelPreviewThemeData lerp( + StreamChannelPreviewThemeData a, + StreamChannelPreviewThemeData b, double t, ) => - ChannelPreviewThemeData( - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + StreamChannelPreviewThemeData( + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), indicatorIconSize: a.indicatorIconSize, lastMessageAtStyle: TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), @@ -122,7 +135,7 @@ class ChannelPreviewThemeData with Diagnosticable { ); /// Merge with theme - ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) { + StreamChannelPreviewThemeData merge(StreamChannelPreviewThemeData? other) { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, @@ -138,7 +151,7 @@ class ChannelPreviewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelPreviewThemeData && + other is StreamChannelPreviewThemeData && runtimeType == other.runtimeType && titleStyle == other.titleStyle && subtitleStyle == other.subtitleStyle && diff --git a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart index b32c023fb..e0068d592 100644 --- a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; +/// {@macro color_theme} +@Deprecated("Use 'StreamColorTheme' instead") +typedef ColorTheme = StreamColorTheme; + +/// {@template color_theme} /// Theme that holds colors -class ColorTheme { +/// {@endtemplate} +class StreamColorTheme { /// Initialise with light theme - ColorTheme.light({ + StreamColorTheme.light({ this.textHighEmphasis = const Color(0xff000000), this.textLowEmphasis = const Color(0xff7a7a7a), this.disabled = const Color(0xffdbdbdb), @@ -55,7 +61,7 @@ class ColorTheme { }) : brightness = Brightness.light; /// Initialise with dark theme - ColorTheme.dark({ + StreamColorTheme.dark({ this.textHighEmphasis = const Color(0xffffffff), this.textLowEmphasis = const Color(0xff7a7a7a), this.disabled = const Color(0xff2d2f2f), @@ -169,7 +175,7 @@ class ColorTheme { final Brightness brightness; /// Copy with theme - ColorTheme copyWith({ + StreamColorTheme copyWith({ Brightness brightness = Brightness.light, Color? textHighEmphasis, Color? textLowEmphasis, @@ -192,7 +198,7 @@ class ColorTheme { Gradient? bgGradient, }) => brightness == Brightness.light - ? ColorTheme.light( + ? StreamColorTheme.light( textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, disabled: disabled ?? this.disabled, @@ -213,7 +219,7 @@ class ColorTheme { overlayDark: overlayDark ?? this.overlayDark, bgGradient: bgGradient ?? this.bgGradient, ) - : ColorTheme.dark( + : StreamColorTheme.dark( textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, disabled: disabled ?? this.disabled, @@ -236,7 +242,7 @@ class ColorTheme { ); /// Merge color theme - ColorTheme merge(ColorTheme? other) { + StreamColorTheme merge(StreamColorTheme? other) { if (other == null) return this; return copyWith( textHighEmphasis: other.textHighEmphasis, diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart index a227b8301..f68ceb450 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro gallery_footer_theme} +@Deprecated("Use 'StreamGalleryFooterTheme' instead") +typedef GalleryFooterTheme = StreamGalleryFooterTheme; + +/// {@template gallery_footer_theme} /// Overrides the default style of [GalleryFooter] descendants. /// /// See also: /// -/// * [GalleryFooterThemeData], which is used to configure this theme. -class GalleryFooterTheme extends InheritedTheme { - /// Creates an [GalleryFooterTheme]. +/// * [StreamGalleryFooterThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamGalleryFooterTheme extends InheritedTheme { + /// Creates an [StreamGalleryFooterTheme]. /// /// The [data] parameter must not be null. - const GalleryFooterTheme({ + const StreamGalleryFooterTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final GalleryFooterThemeData data; + final StreamGalleryFooterThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [GalleryFooterTheme] widget, then + /// If there is no enclosing [StreamGalleryFooterTheme] widget, then /// [StreamChatThemeData.galleryFooterTheme] is used. /// /// Typical usage is as follows: @@ -30,34 +36,40 @@ class GalleryFooterTheme extends InheritedTheme { /// ```dart /// ImageFooterTheme theme = ImageFooterTheme.of(context); /// ``` - static GalleryFooterThemeData of(BuildContext context) { + static StreamGalleryFooterThemeData of(BuildContext context) { final imageFooterTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return imageFooterTheme?.data ?? StreamChatTheme.of(context).galleryFooterTheme; } @override Widget wrap(BuildContext context, Widget child) => - GalleryFooterTheme(data: data, child: child); + StreamGalleryFooterTheme(data: data, child: child); @override - bool updateShouldNotify(GalleryFooterTheme oldWidget) => + bool updateShouldNotify(StreamGalleryFooterTheme oldWidget) => data != oldWidget.data; } +/// {@macro gallery_footer_theme_data} +@Deprecated("Use 'StreamGalleryFooterThemeData' instead") +typedef GalleryFooterThemeData = StreamGalleryFooterThemeData; + +/// {@template gallery_footer_theme_data} /// A style that overrides the default appearance of [GalleryFooter]s when used -/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s +/// with [StreamGalleryFooterTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.galleryFooterTheme]. /// /// See also: /// -/// * [GalleryFooterTheme], the theme which is configured with this class. +/// * [StreamGalleryFooterTheme], the theme which is configured with this class. /// * [StreamChatThemeData.galleryFooterTheme], which can be used to override /// the default style for [GalleryFooter]s below the overall [StreamChatTheme]. -class GalleryFooterThemeData with Diagnosticable { - /// Creates an [GalleryFooterThemeData]. - const GalleryFooterThemeData({ +/// {@endtemplate} +class StreamGalleryFooterThemeData with Diagnosticable { + /// Creates an [StreamGalleryFooterThemeData]. + const StreamGalleryFooterThemeData({ this.backgroundColor, this.shareIconColor, this.titleTextStyle, @@ -108,8 +120,8 @@ class GalleryFooterThemeData with Diagnosticable { /// Defaults to [ColorTheme.textHighEmphasis]. final Color? bottomSheetCloseIconColor; - /// Copies this [GalleryFooterThemeData] to another. - GalleryFooterThemeData copyWith({ + /// Copies this [StreamGalleryFooterThemeData] to another. + StreamGalleryFooterThemeData copyWith({ Color? backgroundColor, Color? shareIconColor, TextStyle? titleTextStyle, @@ -119,7 +131,7 @@ class GalleryFooterThemeData with Diagnosticable { TextStyle? bottomSheetPhotosTextStyle, Color? bottomSheetCloseIconColor, }) => - GalleryFooterThemeData( + StreamGalleryFooterThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, shareIconColor: shareIconColor ?? this.shareIconColor, titleTextStyle: titleTextStyle ?? this.titleTextStyle, @@ -137,12 +149,12 @@ class GalleryFooterThemeData with Diagnosticable { /// Linearly interpolate between two [GalleryFooter] themes. /// /// All the properties must be non-null. - GalleryFooterThemeData lerp( - GalleryFooterThemeData a, - GalleryFooterThemeData b, + StreamGalleryFooterThemeData lerp( + StreamGalleryFooterThemeData a, + StreamGalleryFooterThemeData b, double t, ) => - GalleryFooterThemeData( + StreamGalleryFooterThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), @@ -167,8 +179,8 @@ class GalleryFooterThemeData with Diagnosticable { ), ); - /// Merges one [GalleryFooterThemeData] with another. - GalleryFooterThemeData merge(GalleryFooterThemeData? other) { + /// Merges one [StreamGalleryFooterThemeData] with another. + StreamGalleryFooterThemeData merge(StreamGalleryFooterThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -185,7 +197,7 @@ class GalleryFooterThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is GalleryFooterThemeData && + other is StreamGalleryFooterThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor && shareIconColor == other.shareIconColor && diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index 23402c570..f03ebe9aa 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro gallery_header_them} +@Deprecated("Use 'StreamGalleryHeaderTheme' instead") +typedef GalleryHeaderTheme = StreamGalleryHeaderTheme; + +/// {@template gallery_header_theme} /// Overrides the default style of [GalleryHeader] descendants. /// /// See also: /// -/// * [GalleryHeaderThemeData], which is used to configure this theme. -class GalleryHeaderTheme extends InheritedTheme { - /// Creates a [GalleryHeaderTheme]. +/// * [StreamGalleryHeaderThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamGalleryHeaderTheme extends InheritedTheme { + /// Creates a [StreamGalleryHeaderTheme]. /// /// The [data] parameter must not be null. - const GalleryHeaderTheme({ + const StreamGalleryHeaderTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final GalleryHeaderThemeData data; + final StreamGalleryHeaderThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [GalleryHeaderTheme] widget, then + /// If there is no enclosing [StreamGalleryHeaderTheme] widget, then /// [StreamChatThemeData.galleryHeaderTheme] is used. /// /// Typical usage is as follows: @@ -30,34 +36,40 @@ class GalleryHeaderTheme extends InheritedTheme { /// ```dart /// ImageHeaderTheme theme = ImageHeaderTheme.of(context); /// ``` - static GalleryHeaderThemeData of(BuildContext context) { + static StreamGalleryHeaderThemeData of(BuildContext context) { final galleryHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return galleryHeaderTheme?.data ?? StreamChatTheme.of(context).galleryHeaderTheme; } @override Widget wrap(BuildContext context, Widget child) => - GalleryHeaderTheme(data: data, child: child); + StreamGalleryHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(GalleryHeaderTheme oldWidget) => + bool updateShouldNotify(StreamGalleryHeaderTheme oldWidget) => data != oldWidget.data; } +/// {@macro gallery_header_theme_data} +@Deprecated("Use 'StreamGalleryHeaderThemeData' instead") +typedef GalleryHeaderThemeData = StreamGalleryHeaderThemeData; + +/// {@template gallery_header_theme_data} /// A style that overrides the default appearance of [GalleryHeader]s when used -/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s +/// with [StreamGalleryHeaderTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.galleryHeaderTheme]. /// /// See also: /// -/// * [GalleryHeaderTheme], the theme which is configured with this class. +/// * [StreamGalleryHeaderTheme], the theme which is configured with this class. /// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override /// the default style for [GalleryHeader]s below the overall [StreamChatTheme]. -class GalleryHeaderThemeData with Diagnosticable { - /// Creates an [GalleryHeaderThemeData]. - const GalleryHeaderThemeData({ +/// {@endtemplate} +class StreamGalleryHeaderThemeData with Diagnosticable { + /// Creates an [StreamGalleryHeaderThemeData]. + const StreamGalleryHeaderThemeData({ this.closeButtonColor, this.backgroundColor, this.iconMenuPointColor, @@ -92,8 +104,8 @@ class GalleryHeaderThemeData with Diagnosticable { /// final Color? bottomSheetBarrierColor; - /// Copies this [GalleryHeaderThemeData] to another. - GalleryHeaderThemeData copyWith({ + /// Copies this [StreamGalleryHeaderThemeData] to another. + StreamGalleryHeaderThemeData copyWith({ Color? closeButtonColor, Color? backgroundColor, Color? iconMenuPointColor, @@ -101,7 +113,7 @@ class GalleryHeaderThemeData with Diagnosticable { TextStyle? subtitleTextStyle, Color? bottomSheetBarrierColor, }) => - GalleryHeaderThemeData( + StreamGalleryHeaderThemeData( closeButtonColor: closeButtonColor ?? this.closeButtonColor, backgroundColor: backgroundColor ?? this.backgroundColor, iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, @@ -114,12 +126,12 @@ class GalleryHeaderThemeData with Diagnosticable { /// Linearly interpolate between two [GalleryHeader] themes. /// /// All the properties must be non-null. - GalleryHeaderThemeData lerp( - GalleryHeaderThemeData a, - GalleryHeaderThemeData b, + StreamGalleryHeaderThemeData lerp( + StreamGalleryHeaderThemeData a, + StreamGalleryHeaderThemeData b, double t, ) => - GalleryHeaderThemeData( + StreamGalleryHeaderThemeData( closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), iconMenuPointColor: @@ -131,8 +143,8 @@ class GalleryHeaderThemeData with Diagnosticable { Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), ); - /// Merges one [GalleryHeaderThemeData] with the another - GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) { + /// Merges one [StreamGalleryHeaderThemeData] with the another + StreamGalleryHeaderThemeData merge(StreamGalleryHeaderThemeData? other) { if (other == null) return this; return copyWith( closeButtonColor: other.closeButtonColor, @@ -147,7 +159,7 @@ class GalleryHeaderThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is GalleryHeaderThemeData && + other is StreamGalleryHeaderThemeData && runtimeType == other.runtimeType && closeButtonColor == other.closeButtonColor && backgroundColor == other.backgroundColor && diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index 26a995a9c..bd78bd1b1 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -1,29 +1,37 @@ +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro message_input_theme} +@Deprecated("Use 'StreamMessageInputTheme' instead") +typedef MessageInputTheme = StreamMessageInputTheme; + +/// {@template message_input_theme} /// Overrides the default style of [MessageInput] descendants. /// /// See also: /// -/// * [MessageInputThemeData], which is used to configure this theme. -class MessageInputTheme extends InheritedTheme { - /// Creates a [MessageInputTheme]. +/// * [StreamMessageInputThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamMessageInputTheme extends InheritedTheme { + /// Creates a [StreamMessageInputTheme]. /// /// The [data] parameter must not be null. - const MessageInputTheme({ + const StreamMessageInputTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final MessageInputThemeData data; + final StreamMessageInputThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [MessageInputTheme] widget, then + /// If there is no enclosing [StreamMessageInputTheme] widget, then /// [StreamChatThemeData.messageInputTheme] is used. /// /// Typical usage is as follows: @@ -31,28 +39,35 @@ class MessageInputTheme extends InheritedTheme { /// ```dart /// final theme = MessageInputTheme.of(context); /// ``` - static MessageInputThemeData of(BuildContext context) { + static StreamMessageInputThemeData of(BuildContext context) { final messageInputTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return messageInputTheme?.data ?? StreamChatTheme.of(context).messageInputTheme; } @override Widget wrap(BuildContext context, Widget child) => - MessageInputTheme(data: data, child: child); + StreamMessageInputTheme(data: data, child: child); @override - bool updateShouldNotify(MessageInputTheme oldWidget) => + bool updateShouldNotify(StreamMessageInputTheme oldWidget) => data != oldWidget.data; } +/// {@macro message_input_theme_data} +@Deprecated("Use 'StreamMessageInputThemeData' instead") +typedef MessageInputThemeData = StreamMessageInputThemeData; + +/// {@template message_input_theme_data} /// A style that overrides the default appearance of [MessageInput] widgets -/// when used with [MessageInputTheme] or with the overall [StreamChatTheme]'s +/// when used with [StreamMessageInputTheme] +/// or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.messageInputTheme]. -class MessageInputThemeData with Diagnosticable { - /// Creates a [MessageInputThemeData]. - const MessageInputThemeData({ +/// {@endtemplate} +class StreamMessageInputThemeData with Diagnosticable { + /// Creates a [StreamMessageInputThemeData]. + const StreamMessageInputThemeData({ this.sendAnimationDuration, this.actionButtonColor, this.sendButtonColor, @@ -65,6 +80,10 @@ class MessageInputThemeData with Diagnosticable { this.idleBorderGradient, this.borderRadius, this.expandButtonColor, + this.linkHighlightColor, + this.enableSafeArea, + this.elevation, + this.shadow, }); /// Duration of the [MessageInput] send button animation @@ -73,6 +92,9 @@ class MessageInputThemeData with Diagnosticable { /// Background color of [MessageInput] send button final Color? sendButtonColor; + /// Color of a link + final Color? linkHighlightColor; + /// Background color of [MessageInput] action buttons final Color? actionButtonColor; @@ -103,13 +125,24 @@ class MessageInputThemeData with Diagnosticable { /// Border radius of [MessageInput] final BorderRadius? borderRadius; - /// Returns a new [MessageInputThemeData] replacing some of its properties - MessageInputThemeData copyWith({ + /// Wrap [MessageInput] with a [SafeArea widget] + final bool? enableSafeArea; + + /// Elevation of the [MessageInput] + final double? elevation; + + /// Shadow for the [MessageInput] widget + final BoxShadow? shadow; + + /// Returns a new [StreamMessageInputThemeData] + /// replacing some of its properties + StreamMessageInputThemeData copyWith({ Duration? sendAnimationDuration, Color? inputBackgroundColor, Color? actionButtonColor, Color? sendButtonColor, Color? actionButtonIdleColor, + Color? linkHighlightColor, Color? sendButtonIdleColor, Color? expandButtonColor, TextStyle? inputTextStyle, @@ -117,8 +150,11 @@ class MessageInputThemeData with Diagnosticable { Gradient? activeBorderGradient, Gradient? idleBorderGradient, BorderRadius? borderRadius, + bool? enableSafeArea, + double? elevation, + BoxShadow? shadow, }) => - MessageInputThemeData( + StreamMessageInputThemeData( sendAnimationDuration: sendAnimationDuration ?? this.sendAnimationDuration, inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, @@ -133,15 +169,19 @@ class MessageInputThemeData with Diagnosticable { activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, borderRadius: borderRadius ?? this.borderRadius, + linkHighlightColor: linkHighlightColor ?? this.linkHighlightColor, + enableSafeArea: enableSafeArea ?? this.enableSafeArea, + elevation: elevation ?? this.elevation, + shadow: shadow ?? this.shadow, ); - /// Linearly interpolate from one [MessageInputThemeData] to another. - MessageInputThemeData lerp( - MessageInputThemeData a, - MessageInputThemeData b, + /// Linearly interpolate from one [StreamMessageInputThemeData] to another. + StreamMessageInputThemeData lerp( + StreamMessageInputThemeData a, + StreamMessageInputThemeData b, double t, ) => - MessageInputThemeData( + StreamMessageInputThemeData( actionButtonColor: Color.lerp(a.actionButtonColor, b.actionButtonColor, t), actionButtonIdleColor: @@ -161,10 +201,15 @@ class MessageInputThemeData with Diagnosticable { Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), sendAnimationDuration: a.sendAnimationDuration, inputDecoration: a.inputDecoration, + linkHighlightColor: + Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), + enableSafeArea: a.enableSafeArea, + elevation: lerpDouble(a.elevation, b.elevation, t), + shadow: BoxShadow.lerp(a.shadow, b.shadow, t), ); - /// Merges [this] [MessageInputThemeData] with the [other] - MessageInputThemeData merge(MessageInputThemeData? other) { + /// Merges [this] [StreamMessageInputThemeData] with the [other] + StreamMessageInputThemeData merge(StreamMessageInputThemeData? other) { if (other == null) return this; return copyWith( sendAnimationDuration: other.sendAnimationDuration, @@ -181,13 +226,17 @@ class MessageInputThemeData with Diagnosticable { idleBorderGradient: other.idleBorderGradient, borderRadius: other.borderRadius, expandButtonColor: other.expandButtonColor, + linkHighlightColor: other.linkHighlightColor, + enableSafeArea: other.enableSafeArea, + elevation: other.elevation, + shadow: other.shadow, ); } @override bool operator ==(Object other) => identical(this, other) || - other is MessageInputThemeData && + other is StreamMessageInputThemeData && runtimeType == other.runtimeType && sendAnimationDuration == other.sendAnimationDuration && sendButtonColor == other.sendButtonColor && @@ -200,7 +249,11 @@ class MessageInputThemeData with Diagnosticable { inputDecoration == other.inputDecoration && idleBorderGradient == other.idleBorderGradient && activeBorderGradient == other.activeBorderGradient && - borderRadius == other.borderRadius; + borderRadius == other.borderRadius && + linkHighlightColor == other.linkHighlightColor && + enableSafeArea == other.enableSafeArea && + elevation == other.elevation && + shadow == other.shadow; @override int get hashCode => @@ -215,7 +268,11 @@ class MessageInputThemeData with Diagnosticable { inputDecoration.hashCode ^ idleBorderGradient.hashCode ^ activeBorderGradient.hashCode ^ - borderRadius.hashCode; + borderRadius.hashCode ^ + linkHighlightColor.hashCode ^ + elevation.hashCode ^ + shadow.hashCode ^ + enableSafeArea.hashCode; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -232,6 +289,10 @@ class MessageInputThemeData with Diagnosticable { ..add(DiagnosticsProperty('activeBorderGradient', activeBorderGradient)) ..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient)) ..add(DiagnosticsProperty('borderRadius', borderRadius)) - ..add(ColorProperty('expandButtonColor', expandButtonColor)); + ..add(ColorProperty('expandButtonColor', expandButtonColor)) + ..add(ColorProperty('linkHighlightColor', linkHighlightColor)) + ..add(DiagnosticsProperty('elevation', elevation)) + ..add(DiagnosticsProperty('shadow', shadow)) + ..add(DiagnosticsProperty('enableSafeArea', enableSafeArea)); } } diff --git a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart index e4857a799..e882a9f69 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro message_list_view_theme} +@Deprecated("Use 'StreamMessageListViewTheme' instead") +typedef MessageListViewTheme = StreamMessageListViewTheme; + +/// {@template message_list_view_theme} /// Overrides the default style of [MessageListView] descendants. /// /// See also: /// -/// * [MessageListViewThemeData], which is used to configure this theme. -class MessageListViewTheme extends InheritedTheme { - /// Creates a [MessageListViewTheme]. +/// * [StreamMessageListViewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamMessageListViewTheme extends InheritedTheme { + /// Creates a [StreamMessageListViewTheme]. /// /// The [data] parameter must not be null. - const MessageListViewTheme({ + const StreamMessageListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final MessageListViewThemeData data; + final StreamMessageListViewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [MessageListViewTheme] widget, then + /// If there is no enclosing [StreamMessageListViewTheme] widget, then /// [StreamChatThemeData.messageListViewTheme] is used. /// /// Typical usage is as follows: @@ -30,35 +36,43 @@ class MessageListViewTheme extends InheritedTheme { /// ```dart /// MessageListViewTheme theme = MessageListViewTheme.of(context); /// ``` - static MessageListViewThemeData of(BuildContext context) { - final messageListViewTheme = - context.dependOnInheritedWidgetOfExactType(); + static StreamMessageListViewThemeData of(BuildContext context) { + final messageListViewTheme = context + .dependOnInheritedWidgetOfExactType(); return messageListViewTheme?.data ?? StreamChatTheme.of(context).messageListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - MessageListViewTheme(data: data, child: child); + StreamMessageListViewTheme(data: data, child: child); @override - bool updateShouldNotify(MessageListViewTheme oldWidget) => + bool updateShouldNotify(StreamMessageListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro message_list_view_theme_data} +@Deprecated("Use 'StreamMessageListViewThemeData' instead") +typedef MessageListViewThemeData = StreamMessageListViewThemeData; + +/// {@template message_list_view_theme_data} /// A style that overrides the default appearance of [MessageListView]s when -/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s +/// used with [StreamMessageListViewTheme] or with +/// the overall [StreamChatTheme]'s /// [StreamChatThemeData.messageListViewTheme]. /// /// See also: /// -/// * [MessageListViewTheme], the theme which is configured with this class. +/// * [StreamMessageListViewTheme], the theme +/// which is configured with this class. /// * [StreamChatThemeData.messageListViewTheme], which can be used to override /// the default style for [MessageListView]s below the overall /// [StreamChatTheme]. -class MessageListViewThemeData with Diagnosticable { - /// Creates a [MessageListViewThemeData]. - const MessageListViewThemeData({ +/// {@endtemplate} +class StreamMessageListViewThemeData with Diagnosticable { + /// Creates a [StreamMessageListViewThemeData]. + const StreamMessageListViewThemeData({ this.backgroundColor, this.backgroundImage, }); @@ -69,12 +83,12 @@ class MessageListViewThemeData with Diagnosticable { /// The image of the [MessageListView] background. final DecorationImage? backgroundImage; - /// Copies this [MessageListViewThemeData] to another. - MessageListViewThemeData copyWith({ + /// Copies this [StreamMessageListViewThemeData] to another. + StreamMessageListViewThemeData copyWith({ Color? backgroundColor, DecorationImage? backgroundImage, }) => - MessageListViewThemeData( + StreamMessageListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, backgroundImage: backgroundImage ?? this.backgroundImage, ); @@ -82,18 +96,18 @@ class MessageListViewThemeData with Diagnosticable { /// Linearly interpolate between two [MessageListView] themes. /// /// All the properties must be non-null. - MessageListViewThemeData lerp( - MessageListViewThemeData a, - MessageListViewThemeData b, + StreamMessageListViewThemeData lerp( + StreamMessageListViewThemeData a, + StreamMessageListViewThemeData b, double t, ) => - MessageListViewThemeData( + StreamMessageListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage, ); - /// Merges one [MessageListViewThemeData] with another. - MessageListViewThemeData merge(MessageListViewThemeData? other) { + /// Merges one [StreamMessageListViewThemeData] with another. + StreamMessageListViewThemeData merge(StreamMessageListViewThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -104,7 +118,7 @@ class MessageListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageListViewThemeData && + other is StreamMessageListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor && backgroundImage == other.backgroundImage; diff --git a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart index 670e05e74..535846aef 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart @@ -2,23 +2,29 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro message_search_list_view_theme} +@Deprecated("Use 'StreamMessageSearchListViewTheme' instead") +typedef MessageSearchListViewTheme = StreamMessageSearchListViewTheme; + +/// {@template message_search_list_view_theme} /// Overrides the default style of [MessageSearchListView] descendants. /// /// See also: /// /// * [UserListViewThemeData], which is used to configure this theme. -class MessageSearchListViewTheme extends InheritedTheme { +/// {@endtemplate} +class StreamMessageSearchListViewTheme extends InheritedTheme { /// Creates a [UserListViewTheme]. /// /// The [data] parameter must not be null. - const MessageSearchListViewTheme({ + const StreamMessageSearchListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final MessageSearchListViewThemeData data; + final StreamMessageSearchListViewThemeData data; /// The closest instance of this class that encloses the given context. /// @@ -30,64 +36,72 @@ class MessageSearchListViewTheme extends InheritedTheme { /// ```dart /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); /// ``` - static MessageSearchListViewThemeData of(BuildContext context) { + static StreamMessageSearchListViewThemeData of(BuildContext context) { final messageSearchListViewTheme = context - .dependOnInheritedWidgetOfExactType(); + .dependOnInheritedWidgetOfExactType(); return messageSearchListViewTheme?.data ?? StreamChatTheme.of(context).messageSearchListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - MessageSearchListViewTheme(data: data, child: child); + StreamMessageSearchListViewTheme(data: data, child: child); @override - bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => + bool updateShouldNotify(StreamMessageSearchListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro message_search_list_view_theme_data} +@Deprecated("Use 'StreamMessageSearchListViewThemeData' instead") +typedef MessageSearchListViewThemeData = StreamMessageSearchListViewThemeData; + +/// {@macro message_search_list_view_theme_data} /// A style that overrides the default appearance of [MessageSearchListView]s /// when used with [MessageSearchListView] or with the overall /// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme]. /// /// See also: /// -/// * [MessageSearchListViewTheme], the theme which is configured with this -/// class. +/// * [StreamMessageSearchListViewTheme], the theme +/// which is configured with this class. /// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to /// override the default style for [UserListView]s below the overall /// [StreamChatTheme]. -class MessageSearchListViewThemeData with Diagnosticable { - /// Creates a [MessageSearchListViewThemeData]. - const MessageSearchListViewThemeData({ +/// {@endtemplate} +class StreamMessageSearchListViewThemeData with Diagnosticable { + /// Creates a [StreamMessageSearchListViewThemeData]. + const StreamMessageSearchListViewThemeData({ this.backgroundColor, }); /// The color of the [MessageSearchListView] background. final Color? backgroundColor; - /// Copies this [MessageSearchListViewThemeData] to another. - MessageSearchListViewThemeData copyWith({ + /// Copies this [StreamMessageSearchListViewThemeData] to another. + StreamMessageSearchListViewThemeData copyWith({ Color? backgroundColor, }) => - MessageSearchListViewThemeData( + StreamMessageSearchListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, ); /// Linearly interpolate between two [UserListViewThemeData] themes. /// /// All the properties must be non-null. - MessageSearchListViewThemeData lerp( - MessageSearchListViewThemeData a, - MessageSearchListViewThemeData b, + StreamMessageSearchListViewThemeData lerp( + StreamMessageSearchListViewThemeData a, + StreamMessageSearchListViewThemeData b, double t, ) => - MessageSearchListViewThemeData( + StreamMessageSearchListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), ); - /// Merges one [MessageSearchListViewThemeData] with another. - MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { + /// Merges one [StreamMessageSearchListViewThemeData] with another. + StreamMessageSearchListViewThemeData merge( + StreamMessageSearchListViewThemeData? other, + ) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -97,7 +111,7 @@ class MessageSearchListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageSearchListViewThemeData && + other is StreamMessageSearchListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index 386a312f6..c94c8159f 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -2,11 +2,17 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +/// {@macro message_theme_data} +@Deprecated("Use 'StreamMessageThemeData' instead") +typedef MessageThemeData = StreamMessageThemeData; + +/// {@template message_theme_data} /// Class for getting message theme +/// {@endtemplate} // ignore: prefer-match-file-name -class MessageThemeData with Diagnosticable { - /// Creates a [MessageThemeData]. - const MessageThemeData({ +class StreamMessageThemeData with Diagnosticable { + /// Creates a [StreamMessageThemeData]. + const StreamMessageThemeData({ this.repliesStyle, this.messageTextStyle, this.messageAuthorStyle, @@ -52,13 +58,13 @@ class MessageThemeData with Diagnosticable { final Color? reactionsMaskColor; /// Theme of the avatar - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; /// Background color for messages with url attachments. final Color? linkBackgroundColor; /// Copy with a theme - MessageThemeData copyWith({ + StreamMessageThemeData copyWith({ TextStyle? messageTextStyle, TextStyle? messageAuthorStyle, TextStyle? messageLinksStyle, @@ -66,13 +72,13 @@ class MessageThemeData with Diagnosticable { TextStyle? repliesStyle, Color? messageBackgroundColor, Color? messageBorderColor, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? reactionsBackgroundColor, Color? reactionsBorderColor, Color? reactionsMaskColor, Color? linkBackgroundColor, }) => - MessageThemeData( + StreamMessageThemeData( messageTextStyle: messageTextStyle ?? this.messageTextStyle, messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle, messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle, @@ -89,11 +95,15 @@ class MessageThemeData with Diagnosticable { linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor, ); - /// Linearly interpolate from one [MessageThemeData] to another. - MessageThemeData lerp(MessageThemeData a, MessageThemeData b, double t) => - MessageThemeData( - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + /// Linearly interpolate from one [StreamMessageThemeData] to another. + StreamMessageThemeData lerp( + StreamMessageThemeData a, + StreamMessageThemeData b, + double t, + ) => + StreamMessageThemeData( + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), messageAuthorStyle: TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), @@ -120,7 +130,7 @@ class MessageThemeData with Diagnosticable { ); /// Merge with a theme - MessageThemeData merge(MessageThemeData? other) { + StreamMessageThemeData merge(StreamMessageThemeData? other) { if (other == null) return this; return copyWith( messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ?? @@ -146,7 +156,7 @@ class MessageThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageThemeData && + other is StreamMessageThemeData && runtimeType == other.runtimeType && messageTextStyle == other.messageTextStyle && messageAuthorStyle == other.messageAuthorStyle && diff --git a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart index ec70cf837..06ddf2d95 100644 --- a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; +/// {@macro text_theme} +@Deprecated("Use 'StreamTextTheme' instead") +typedef TextTheme = StreamTextTheme; + +/// {@template text_theme} /// Class for holding text theme -class TextTheme { +/// {@endtemplate} +class StreamTextTheme { /// Initialise light text theme - TextTheme.light({ + StreamTextTheme.light({ this.title = const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, @@ -46,7 +52,7 @@ class TextTheme { }); /// Initialise with dark theme - TextTheme.dark({ + StreamTextTheme.dark({ this.title = const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, @@ -113,7 +119,7 @@ class TextTheme { final TextStyle captionBold; /// Copy with theme - TextTheme copyWith({ + StreamTextTheme copyWith({ Brightness brightness = Brightness.light, TextStyle? body, TextStyle? title, @@ -125,7 +131,7 @@ class TextTheme { TextStyle? captionBold, }) => brightness == Brightness.light - ? TextTheme.light( + ? StreamTextTheme.light( body: body ?? this.body, title: title ?? this.title, headlineBold: headlineBold ?? this.headlineBold, @@ -135,7 +141,7 @@ class TextTheme { footnote: footnote ?? this.footnote, captionBold: captionBold ?? this.captionBold, ) - : TextTheme.dark( + : StreamTextTheme.dark( body: body ?? this.body, title: title ?? this.title, headlineBold: headlineBold ?? this.headlineBold, @@ -147,7 +153,7 @@ class TextTheme { ); /// Merge text theme - TextTheme merge(TextTheme? other) { + StreamTextTheme merge(StreamTextTheme? other) { if (other == null) return this; return copyWith( body: body.merge(other.body), diff --git a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart index 0d99ccbd2..fb57438bb 100644 --- a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro user_list_view_theme} +@Deprecated("Use 'StreamUserListViewTheme' instead") +typedef UserListViewTheme = StreamUserListViewTheme; + +/// {@template user_list_view_theme} /// Overrides the default style of [UserListView] descendants. /// /// See also: /// -/// * [UserListViewThemeData], which is used to configure this theme. -class UserListViewTheme extends InheritedTheme { - /// Creates a [UserListViewTheme]. +/// * [StreamUserListViewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamUserListViewTheme extends InheritedTheme { + /// Creates a [StreamUserListViewTheme]. /// /// The [data] parameter must not be null. - const UserListViewTheme({ + const StreamUserListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final UserListViewThemeData data; + final StreamUserListViewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [UserListViewTheme] widget, then + /// If there is no enclosing [StreamUserListViewTheme] widget, then /// [StreamChatThemeData.userListViewTheme] is used. /// /// Typical usage is as follows: @@ -30,35 +36,41 @@ class UserListViewTheme extends InheritedTheme { /// ```dart /// UserListViewTheme theme = UserListViewTheme.of(context); /// ``` - static UserListViewThemeData of(BuildContext context) { + static StreamUserListViewThemeData of(BuildContext context) { final userListViewTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return userListViewTheme?.data ?? StreamChatTheme.of(context).userListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - UserListViewTheme(data: data, child: child); + StreamUserListViewTheme(data: data, child: child); @override - bool updateShouldNotify(UserListViewTheme oldWidget) => + bool updateShouldNotify(StreamUserListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro user_list_view_theme_data} +@Deprecated("Use 'StreamUserListViewThemeData' instead") +typedef UserListViewThemeData = StreamUserListViewThemeData; + +/// {@template user_list_view_theme_data} /// A style that overrides the default appearance of [UserListView]s when -/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s +/// used with [StreamUserListViewTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.userListViewTheme]. /// /// See also: /// -/// * [UserListViewTheme], the theme which is configured with this class. +/// * [StreamUserListViewTheme], the theme which is configured with this class. /// * [StreamChatThemeData.userListViewTheme], which can be used to override /// the default style for [UserListView]s below the overall /// [StreamChatTheme]. -class UserListViewThemeData with Diagnosticable { - /// Creates a [UserListViewThemeData]. - const UserListViewThemeData({ +/// {@endtemplate} +class StreamUserListViewThemeData with Diagnosticable { + /// Creates a [StreamUserListViewThemeData]. + const StreamUserListViewThemeData({ this.backgroundColor, }); @@ -66,27 +78,27 @@ class UserListViewThemeData with Diagnosticable { final Color? backgroundColor; /// Copies this [ChannelListViewThemeData] to another. - UserListViewThemeData copyWith({ + StreamUserListViewThemeData copyWith({ Color? backgroundColor, }) => - UserListViewThemeData( + StreamUserListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, ); - /// Linearly interpolate between two [UserListViewThemeData] themes. + /// Linearly interpolate between two [StreamUserListViewThemeData] themes. /// /// All the properties must be non-null. - UserListViewThemeData lerp( - UserListViewThemeData a, - UserListViewThemeData b, + StreamUserListViewThemeData lerp( + StreamUserListViewThemeData a, + StreamUserListViewThemeData b, double t, ) => - UserListViewThemeData( + StreamUserListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), ); - /// Merges one [UserListViewThemeData] with another. - UserListViewThemeData merge(UserListViewThemeData? other) { + /// Merges one [StreamUserListViewThemeData] with another. + StreamUserListViewThemeData merge(StreamUserListViewThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -96,7 +108,7 @@ class UserListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is UserListViewThemeData && + other is StreamUserListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 199b8a44c..f6f77a32c 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -3,6 +3,11 @@ import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro thread_header} +@Deprecated("Use 'StreamThreadHeader' instead") +typedef ThreadHeader = StreamThreadHeader; + +/// {@template thread_header} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png) /// @@ -56,9 +61,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// Modify it to change the widget appearance. -class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamThreadHeader extends StatelessWidget + implements PreferredSizeWidget { /// Instantiate a new ThreadHeader - const ThreadHeader({ + const StreamThreadHeader({ Key? key, required this.parent, this.showBackButton = true, @@ -107,10 +114,10 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// if a user is typing in this thread final bool showTypingIndicator; - /// The background color of this [ThreadHeader]. + /// The background color of this [StreamThreadHeader]. final Color? backgroundColor; - /// The elevation for this [ThreadHeader]. + /// The elevation for this [StreamThreadHeader]. final double elevation; @override @@ -121,7 +128,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { centerTitle: centerTitle, ); - final channelHeaderTheme = ChannelHeaderTheme.of(context); + final channelHeaderTheme = StreamChannelHeaderTheme.of(context); final defaultSubtitle = subtitle ?? Row( @@ -133,7 +140,8 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { style: channelHeaderTheme.subtitleStyle, ), Flexible( - child: ChannelName( + child: StreamChannelName( + channel: StreamChannel.of(context).channel, textStyle: channelHeaderTheme.subtitleStyle, ), ), @@ -178,12 +186,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { ), const SizedBox(height: 2), if (showTypingIndicator) - TypingIndicator( - alignment: Alignment.center, - channel: StreamChannel.of(context).channel, - style: channelHeaderTheme.subtitleStyle, - parentId: parent.id, - alternativeWidget: defaultSubtitle, + Align( + child: StreamTypingIndicator( + channel: StreamChannel.of(context).channel, + style: channelHeaderTheme.subtitleStyle, + parentId: parent.id, + alternativeWidget: defaultSubtitle, + ), ) else defaultSubtitle, diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 5bf7358a4..1ac99785a 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -3,15 +3,20 @@ import 'package:lottie/lottie.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +/// {@macro typing_indicator} +@Deprecated("Use 'StreamTypingIndicator' instead") +typedef TypingIndicator = StreamTypingIndicator; + +/// {@template typing_indicator} /// Widget to show the current list of typing users -class TypingIndicator extends StatelessWidget { +/// {@endtemplate} +class StreamTypingIndicator extends StatelessWidget { /// Instantiate a new TypingIndicator - const TypingIndicator({ + const StreamTypingIndicator({ Key? key, this.channel, this.alternativeWidget, this.style, - this.alignment = Alignment.centerLeft, this.padding = const EdgeInsets.all(0), this.parentId, }) : super(key: key); @@ -28,9 +33,6 @@ class TypingIndicator extends StatelessWidget { /// The padding of this widget final EdgeInsets padding; - /// Alignment of the typing indicator - final Alignment alignment; - /// Id of the parent message in case of a thread final String? parentId; @@ -46,7 +48,7 @@ class TypingIndicator extends StatelessWidget { stream: channelState.typingEventsStream.map((typings) => typings.entries .where((element) => element.value.parentId == parentId) .map((e) => e.key)), - builder: (context, data) => AnimatedSwitcher( + builder: (context, users) => AnimatedSwitcher( layoutBuilder: (currentChild, previousChildren) => Stack( children: [ ...previousChildren, @@ -54,28 +56,23 @@ class TypingIndicator extends StatelessWidget { ], ), duration: const Duration(milliseconds: 300), - child: data.isNotEmpty + child: users.isNotEmpty ? Padding( - key: const Key('main'), padding: padding, - child: Align( - key: const Key('typings'), - alignment: alignment, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Lottie.asset( - 'animations/typing_dots.json', - package: 'stream_chat_flutter', - height: 4, - ), - Text( - context.translations.userTypingText(data), - maxLines: 1, - style: style, - ), - ], - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Lottie.asset( + 'animations/typing_dots.json', + package: 'stream_chat_flutter', + height: 4, + ), + Text( + context.translations.userTypingText(users), + maxLines: 1, + style: style, + ), + ], ), ) : altWidget, diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index 958386d15..bf23db1ad 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro unread_indicator} +@Deprecated("Use 'StreamUnreadIndicator' instead") +typedef UnreadIndicator = StreamUnreadIndicator; + +/// {@template unread_indicator} /// Widget for showing an unread indicator -class UnreadIndicator extends StatelessWidget { - /// Constructor for creating an [UnreadIndicator] - const UnreadIndicator({ +/// {@endtemplate} +class StreamUnreadIndicator extends StatelessWidget { + /// Constructor for creating an [StreamUnreadIndicator] + const StreamUnreadIndicator({ Key? key, this.cid, }) : super(key: key); diff --git a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart index b3fe510d0..d11e9609e 100644 --- a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro upload_progress_indicator} +@Deprecated("Use 'StreamUploadProgressIndicator' instead") +typedef UploadProgressIndicator = StreamUploadProgressIndicator; + +/// {@template upload_progress_indicator} /// Widget for showing upload progress -class UploadProgressIndicator extends StatelessWidget { - /// Constructor for creating an [UploadProgressIndicator] - const UploadProgressIndicator({ +/// {@endtemplate} +class StreamUploadProgressIndicator extends StatelessWidget { + /// Constructor for creating an [StreamUploadProgressIndicator] + const StreamUploadProgressIndicator({ Key? key, required this.uploaded, required this.total, diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index e638685cb..8d6340396 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -2,10 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro user_avatar} +@Deprecated("Use 'StreamUserAvatar' instead") +typedef UserAvatar = StreamUserAvatar; + +/// {@template user_avatar} /// Widget that displays a user avatar -class UserAvatar extends StatelessWidget { - /// Constructor to create a [UserAvatar] - const UserAvatar({ +/// {@endtemplate} +class StreamUserAvatar extends StatelessWidget { + /// Constructor to create a [StreamUserAvatar] + const StreamUserAvatar({ Key? key, required this.user, this.constraints, diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index c0e3482eb..74ccacc28 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -2,21 +2,26 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// +/// {@macro user_item} +@Deprecated("Use 'StreamUserItem' instead") +typedef UserItem = StreamUserItem; + +/// {@template user_item} /// It shows the current [User] preview. /// /// The widget uses a [StreamBuilder] to render the user information /// image as soon as it updates. /// /// Usually you don't use this widget as it's the default user preview used -/// by [UserListView]. +/// by [StreamUserListView]. /// /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class UserItem extends StatelessWidget { +/// {@endtemplate} +class StreamUserItem extends StatelessWidget { /// Instantiate a new UserItem - const UserItem({ + const StreamUserItem({ Key? key, required this.user, this.onTap, @@ -38,10 +43,10 @@ class UserItem extends StatelessWidget { /// The function called when the image is tapped final void Function(User)? onImageTap; - /// If true the [UserItem] will show a trailing checkmark + /// If true the [StreamUserItem] will show a trailing checkmark final bool selected; - /// If true the [UserItem] will show the last seen + /// If true the [StreamUserItem] will show the last seen final bool showLastOnline; @override @@ -58,7 +63,7 @@ class UserItem extends StatelessWidget { onLongPress!(user); } }, - leading: UserAvatar( + leading: StreamUserAvatar( user: user, onTap: (user) { if (onImageTap != null) { 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 58af30a7b..2b5b868e8 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'; @@ -8,7 +11,7 @@ typedef UserTapCallback = void Function(User, Widget?); /// Builder used to create a custom [ListUserItem] from a [User] typedef UserItemBuilder = Widget Function(BuildContext, User, bool); -/// +/// {@template user_list_view} /// It shows the list of current users. /// /// ```dart @@ -42,6 +45,8 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool); /// The widget components render the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. +/// {@endtemplate} +@Deprecated("Use 'StreamUserListView' instead") class UserListView extends StatefulWidget { /// Instantiate a new UserListView UserListView({ @@ -203,7 +208,7 @@ class _UserListViewState extends State userListController: _userListController, ); - final backgroundColor = UserListViewTheme.of(context).backgroundColor; + final backgroundColor = StreamUserListViewTheme.of(context).backgroundColor; Widget child; @@ -332,7 +337,7 @@ class _UserListViewState extends State key: ValueKey('USER-${user.id}'), child: widget.userItemBuilder != null ? widget.userItemBuilder!(context, user, selected) - : UserItem( + : StreamUserItem( user: user, onTap: (user) => widget.onUserTap!(user, widget.userWidget), onLongPress: widget.onUserLongPress, @@ -362,7 +367,7 @@ class _UserListViewState extends State : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - UserAvatar( + StreamUserAvatar( user: user, borderRadius: BorderRadius.circular(32), selected: selected, diff --git a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart index ff9e145b8..b944d912a 100644 --- a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro user_mention_tile} +@Deprecated("Use 'StreamUserMentionTile' instead") +typedef UserMentionTile = StreamUserMentionTile; + +/// {@template user_mention_tile} /// This widget is used for showing user tiles for mentions /// Use [title], [subtitle], [leading], [trailing] for /// substituting widgets in respective positions -class UserMentionTile extends StatelessWidget { - /// Constructor for creating a [UserMentionTile] widget - const UserMentionTile( +/// {@endtemplate} +class StreamUserMentionTile extends StatelessWidget { + /// Constructor for creating a [StreamUserMentionTile] widget + const StreamUserMentionTile( this.user, { Key? key, this.title, @@ -41,7 +47,7 @@ class UserMentionTile extends StatelessWidget { width: 16, ), leading ?? - UserAvatar( + StreamUserAvatar( user: user, constraints: BoxConstraints.tight(const Size(40, 40)), ), diff --git a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart index e3830231f..59c867910 100644 --- a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart @@ -6,16 +6,22 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Builder function for building a mention tile. /// -/// Use [UserMentionTile] for the default implementation. +/// Use [StreamUserMentionTile] for the default implementation. typedef MentionTileBuilder = Widget Function( BuildContext context, User user, ); +/// {@macro user_mention_tile} +@Deprecated("Use 'StreamUserMentionsOverlay' instead") +typedef UserMentionsOverlay = StreamUserMentionsOverlay; + +/// {@template user_mentions_overlay} /// Overlay for displaying users that can be mentioned. -class UserMentionsOverlay extends StatefulWidget { - /// Constructor for creating a [UserMentionsOverlay]. - UserMentionsOverlay({ +/// {@endtemplate} +class StreamUserMentionsOverlay extends StatefulWidget { + /// Constructor for creating a [StreamUserMentionsOverlay]. + StreamUserMentionsOverlay({ Key? key, required this.query, required this.channel, @@ -62,10 +68,11 @@ class UserMentionsOverlay extends StatefulWidget { final void Function(User user)? onMentionUserTap; @override - _UserMentionsOverlayState createState() => _UserMentionsOverlayState(); + _StreamUserMentionsOverlayState createState() => + _StreamUserMentionsOverlayState(); } -class _UserMentionsOverlayState extends State { +class _StreamUserMentionsOverlayState extends State { late Future> userMentionsFuture; @override @@ -75,7 +82,7 @@ class _UserMentionsOverlayState extends State { } @override - void didUpdateWidget(covariant UserMentionsOverlay oldWidget) { + void didUpdateWidget(covariant StreamUserMentionsOverlay oldWidget) { super.didUpdateWidget(oldWidget); if (widget.channel != oldWidget.channel || widget.query != oldWidget.query || @@ -116,7 +123,7 @@ class _UserMentionsOverlayState extends State { child: InkWell( onTap: () => widget.onMentionUserTap?.call(user), child: widget.mentionsTileBuilder?.call(context, user) ?? - UserMentionTile(user), + StreamUserMentionTile(user), ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 3d0121947..86357803a 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; /// Launch URL Future launchURL(BuildContext context, String url) async { try { - await launch(Uri.parse(url).withScheme.toString()); + await launchUrl(Uri.parse(url).withScheme); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.translations.launchUrlError)), @@ -451,3 +451,19 @@ int levenshtein(String s, String t, {bool caseSensitive = true}) { return v1[t.length]; } + +/// 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 + List getAttachmentPackageList() { + final _attachmentPackages = List.generate( + attachments.length, + (index) => StreamAttachmentPackage( + attachment: attachments[index], + message: this, + ), + ); + return _attachmentPackages; + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/countdown_button.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/countdown_button.dart new file mode 100644 index 000000000..aaf016a34 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/countdown_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Button for showing visual component of slow mode. +class StreamCountdownButton extends StatelessWidget { + /// Constructor for creating [StreamCountdownButton]. + const StreamCountdownButton({ + Key? key, + required this.count, + }) : super(key: key); + + /// Count of time remaining to show to the user. + final int count; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(8), + child: DecoratedBox( + decoration: BoxDecoration( + color: StreamChatTheme.of(context).colorTheme.disabled, + shape: BoxShape.circle, + ), + child: SizedBox( + height: 24, + width: 24, + child: Center( + child: Text('$count'), + ), + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/simple_safe_area.dart new file mode 100644 index 000000000..5f3d7391f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/simple_safe_area.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +/// A [SafeArea] with an enabled toggle +class SimpleSafeArea extends StatelessWidget { + /// Constructor for [SimpleSafeArea] + const SimpleSafeArea({ + Key? key, + this.enabled = true, + required this.child, + }) : super(key: key); + + /// Wrap [child] with [SafeArea] + final bool enabled; + + /// Child widget to wrap + final Widget child; + + @override + Widget build(BuildContext context) => SafeArea( + left: enabled, + top: enabled, + right: enabled, + bottom: enabled, + child: child, + ); +} 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 new file mode 100644 index 000000000..c6a3581b9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart @@ -0,0 +1,564 @@ +import 'dart:async'; + +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/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. +typedef FilePickerCallback = void Function( + DefaultAttachmentTypes fileType, { + bool camera, +}); + +/// Callback for building an icon for a custom attachment type. +typedef CustomAttachmentIconBuilder = Widget Function( + BuildContext context, + bool active, +); + +/// A widget that allows to pick an attachment. +class StreamAttachmentPicker extends StatefulWidget { + /// Default constructor for [StreamAttachmentPicker] which creates the Stream + /// attachment picker widget. + const StreamAttachmentPicker({ + Key? key, + required this.messageInputController, + required this.onFilePicked, + this.isOpen = false, + this.pickerSize = 360.0, + this.attachmentLimit = 10, + this.onAttachmentLimitExceeded, + this.maxAttachmentSize = 20971520, + this.onError, + this.allowedAttachmentTypes = const [ + DefaultAttachmentTypes.image, + DefaultAttachmentTypes.file, + DefaultAttachmentTypes.video, + ], + this.customAttachmentTypes = const [], + }) : super(key: key); + + /// True if the picker is open. + final bool isOpen; + + /// The picker size in height. + final double pickerSize; + + /// The [StreamMessageInputController] linked to this picker. + final StreamMessageInputController messageInputController; + + /// The limit of attachments that can be picked. + final int attachmentLimit; + + /// The callback for when the attachment limit is exceeded. + final AttachmentLimitExceedListener? onAttachmentLimitExceeded; + + /// Callback for when an error occurs in the attachment picker. + final ValueChanged? onError; + + /// Callback for when file is picked. + final FilePickerCallback onFilePicked; + + /// Max attachment size in bytes: + /// - Defaults to 20 MB + /// - Do not set it if you're using our default CDN + final int maxAttachmentSize; + + /// The list of attachment types that can be picked. + final List allowedAttachmentTypes; + + /// The list of custom attachment types that can be picked. + final List customAttachmentTypes; + + /// Used to create a new copy of [StreamAttachmentPicker] with modified + /// properties. + StreamAttachmentPicker copyWith({ + Key? key, + StreamMessageInputController? messageInputController, + FilePickerCallback? onFilePicked, + bool? isOpen, + double? pickerSize, + int? attachmentLimit, + AttachmentLimitExceedListener? onAttachmentLimitExceeded, + int? maxAttachmentSize, + ValueChanged? onChangeInputState, + ValueChanged? onError, + List? allowedAttachmentTypes, + List? customAttachmentTypes = const [], + }) => + StreamAttachmentPicker( + key: key ?? this.key, + messageInputController: + messageInputController ?? this.messageInputController, + onFilePicked: onFilePicked ?? this.onFilePicked, + isOpen: isOpen ?? this.isOpen, + pickerSize: pickerSize ?? this.pickerSize, + attachmentLimit: attachmentLimit ?? this.attachmentLimit, + onAttachmentLimitExceeded: + onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded, + maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize, + onError: onError ?? this.onError, + allowedAttachmentTypes: + allowedAttachmentTypes ?? this.allowedAttachmentTypes, + customAttachmentTypes: + customAttachmentTypes ?? this.customAttachmentTypes, + ); + + @override + State createState() => _StreamAttachmentPickerState(); +} + +class _StreamAttachmentPickerState extends State { + int _filePickerIndex = 0; + final _mediaListViewController = MediaListViewController(); + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + final messageInputController = widget.messageInputController; + + final _attachmentContainsImage = + messageInputController.attachments.any((it) => it.type == 'image'); + + final _attachmentContainsFile = + messageInputController.attachments.any((it) => it.type == 'file'); + + final _attachmentContainsVideo = + messageInputController.attachments.any((it) => it.type == 'video'); + + final attachmentLimitCrossed = + messageInputController.attachments.length >= widget.attachmentLimit; + + Color _getIconColor(int index) { + final streamChatThemeData = _streamChatTheme; + switch (index) { + case 0: + return _filePickerIndex == 0 || _attachmentContainsImage + ? streamChatThemeData.colorTheme.accentPrimary + : (_attachmentContainsImage + ? streamChatThemeData.colorTheme.accentPrimary + : streamChatThemeData.colorTheme.textHighEmphasis.withOpacity( + messageInputController.attachments.isEmpty ? 0.5 : 0.2, + )); + case 1: + return _attachmentContainsFile + ? streamChatThemeData.colorTheme.accentPrimary + : (messageInputController.attachments.isEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); + case 2: + return widget.messageInputController.attachments.isNotEmpty && + (!_attachmentContainsImage || attachmentLimitCrossed) + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : _attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); + case 3: + return widget.messageInputController.attachments.isNotEmpty && + (!_attachmentContainsVideo || attachmentLimitCrossed) + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : _attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); + default: + return Colors.black; + } + } + + return AnimatedContainer( + duration: + widget.isOpen ? const Duration(milliseconds: 300) : const Duration(), + curve: Curves.easeOut, + height: widget.isOpen ? widget.pickerSize : 0, + child: SingleChildScrollView( + child: SizedBox( + height: widget.pickerSize, + child: Material( + color: _streamChatTheme.colorTheme.inputBg, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image)) + IconButton( + icon: StreamSvgIcon.pictures( + color: _getIconColor(0), + ), + onPressed: + messageInputController.attachments.isNotEmpty && + !_attachmentContainsImage + ? null + : () { + setState(() { + _filePickerIndex = 0; + }); + }, + ), + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.file)) + IconButton( + iconSize: 32, + icon: StreamSvgIcon.files( + color: _getIconColor(1), + ), + onPressed: messageInputController + .attachments.isNotEmpty && + !_attachmentContainsFile + ? null + : () { + widget + .onFilePicked(DefaultAttachmentTypes.file); + }, + ), + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image)) + IconButton( + icon: StreamSvgIcon.camera( + color: _getIconColor(2), + ), + onPressed: attachmentLimitCrossed || + (messageInputController + .attachments.isNotEmpty && + !_attachmentContainsVideo) + ? null + : () { + widget.onFilePicked( + DefaultAttachmentTypes.image, + camera: true, + ); + }, + ), + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.video)) + IconButton( + padding: const EdgeInsets.all(0), + icon: StreamSvgIcon.record( + color: _getIconColor(3), + ), + onPressed: attachmentLimitCrossed || + (messageInputController + .attachments.isNotEmpty && + !_attachmentContainsVideo) + ? null + : () { + widget.onFilePicked( + DefaultAttachmentTypes.video, + camera: true, + ); + }, + ), + for (int i = 0; + i < widget.customAttachmentTypes.length; + i++) + IconButton( + onPressed: () { + if (messageInputController.attachments.isNotEmpty) { + if (!messageInputController.attachments.any((e) => + e.type == + widget.customAttachmentTypes[i].type)) { + return; + } + } + + setState(() { + _filePickerIndex = i + 1; + }); + }, + icon: widget.customAttachmentTypes[i] + .iconBuilder(context, _filePickerIndex == i + 1), + ), + ], + ), + 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, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.inputBg, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ), + if (widget.isOpen && + (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image) || + (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.file)))) + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + child: _PickerWidget( + mediaListViewController: _mediaListViewController, + filePickerIndex: _filePickerIndex, + streamChatTheme: _streamChatTheme, + containsFile: _attachmentContainsFile, + selectedMedias: messageInputController.attachments + .map((e) => e.id) + .toList(), + onAddMoreFilesClick: widget.onFilePicked, + onMediaSelected: (media) { + if (messageInputController.attachments + .any((e) => e.id == media.id)) { + messageInputController + .removeAttachmentById(media.id); + } else { + _addAssetAttachment(media); + } + }, + allowedAttachmentTypes: widget.allowedAttachmentTypes, + customAttachmentTypes: widget.customAttachmentTypes, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _addAssetAttachment(AssetEntity medium) async { + final mediaFile = await medium.originFile.timeout( + const Duration(seconds: 5), + onTimeout: () => medium.originFile, + ); + + if (mediaFile == null) return; + + final file = AttachmentFile( + path: mediaFile.path, + size: await mediaFile.length(), + bytes: mediaFile.readAsBytesSync(), + ); + + if (file.size! > widget.maxAttachmentSize) { + return widget.onError?.call( + context.translations.fileTooLargeError( + widget.maxAttachmentSize / (1024 * 1024), + ), + ); + } + + setState(() { + final attachment = Attachment( + id: medium.id, + file: file, + type: medium.type == AssetType.image ? 'image' : 'video', + ); + _addAttachments([attachment]); + }); + } + + /// Adds an attachment to the [messageInputController.attachments] map + void _addAttachments(Iterable attachments) { + final limit = widget.attachmentLimit; + final length = + widget.messageInputController.attachments.length + attachments.length; + if (length > limit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceeded; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + widget.attachmentLimit, + context.translations.attachmentLimitExceedError(limit), + ); + } + return widget.onError?.call( + context.translations.attachmentLimitExceedError(limit), + ); + } + for (final attachment in attachments) { + widget.messageInputController.addAttachment(attachment); + } + } +} + +class _PickerWidget extends StatefulWidget { + const _PickerWidget({ + Key? key, + required this.filePickerIndex, + required this.containsFile, + required this.selectedMedias, + required this.onAddMoreFilesClick, + required this.onMediaSelected, + required this.streamChatTheme, + required this.allowedAttachmentTypes, + required this.customAttachmentTypes, + required this.mediaListViewController, + }) : super(key: key); + + final int filePickerIndex; + final bool containsFile; + final List selectedMedias; + final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; + final void Function(AssetEntity) onMediaSelected; + final StreamChatThemeData streamChatTheme; + final List allowedAttachmentTypes; + final List customAttachmentTypes; + final MediaListViewController mediaListViewController; + + @override + _PickerWidgetState createState() => _PickerWidgetState(); +} + +class _PickerWidgetState extends State<_PickerWidget> { + Future? requestPermission; + + @override + void initState() { + super.initState(); + requestPermission = PhotoManager.requestPermissionExtend(); + } + + @override + Widget build(BuildContext context) { + if (widget.filePickerIndex != 0) { + return widget.customAttachmentTypes[widget.filePickerIndex - 1] + .pickerBuilder(context); + } + return FutureBuilder( + future: requestPermission, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Offstage(); + } + + if ([PermissionState.authorized, PermissionState.limited] + .contains(snapshot.data)) { + if (widget.containsFile || + !widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image)) { + return GestureDetector( + onTap: () { + widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); + }, + child: Container( + constraints: const BoxConstraints.expand(), + color: widget.streamChatTheme.colorTheme.inputBg, + alignment: Alignment.center, + child: Text( + context.translations.addMoreFilesLabel, + style: TextStyle( + color: widget.streamChatTheme.colorTheme.accentPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + return StreamMediaListView( + selectedIds: widget.selectedMedias, + onSelect: widget.onMediaSelected, + controller: widget.mediaListViewController, + ); + } + + return InkWell( + onTap: () async { + PhotoManager.openSetting(); + }, + child: Container( + color: widget.streamChatTheme.colorTheme.inputBg, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SvgPicture.asset( + 'svgs/icon_picture_empty_state.svg', + package: 'stream_chat_flutter', + height: 140, + color: widget.streamChatTheme.colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: widget.streamChatTheme.textTheme.body.copyWith( + color: widget.streamChatTheme.colorTheme.textLowEmphasis, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Center( + child: Text( + context.translations.allowGalleryAccessMessage, + style: widget.streamChatTheme.textTheme.bodyBold.copyWith( + color: widget.streamChatTheme.colorTheme.accentPrimary, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +/// Class which holds data for a custom attachment type in the attachment picker +class CustomAttachmentType { + /// Default constructor for creating a custom attachment for the attachment + /// picker. + CustomAttachmentType({ + required this.type, + required this.iconBuilder, + required this.pickerBuilder, + }); + + /// Type name. + String type; + + /// Builds the icon in the attachment picker top row. + CustomAttachmentIconBuilder iconBuilder; + + /// Builds content in the attachment builder when icon is selected. + WidgetBuilder pickerBuilder; +} 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 new file mode 100644 index 000000000..786ba22e5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart @@ -0,0 +1,1873 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:shimmer/shimmer.dart'; +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/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'; +import 'package:stream_chat_flutter/src/v4/message_input/tld.dart'; +import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A function that returns true if the message is valid and can be sent. +typedef MessageValidator = bool Function(Message message); + +/// A callback that can be passed to [StreamMessageInput.onError]. +/// +/// This callback should not throw. +/// +/// It exists merely for error reporting, and should not be used otherwise. +typedef ErrorListener = void Function( + Object error, + StackTrace? stackTrace, +); + +/// A callback that can be passed to +/// [StreamMessageInput.onAttachmentLimitExceed]. +/// +/// This callback should not throw. +/// +/// It exists merely for showing a custom error, and should not be used +/// otherwise. +typedef AttachmentLimitExceedListener = void Function( + int limit, + String error, +); + +/// Builder for attachment thumbnails. +typedef AttachmentThumbnailBuilder = Widget Function( + BuildContext, + Attachment, +); + +/// Builder function for building a mention tile. +typedef MentionTileBuilder = Widget Function( + BuildContext context, + Member member, +); + +/// Builder function for building a user mention tile. +/// +/// Use [StreamUserMentionTile] for the default implementation. +typedef UserMentionTileBuilder = Widget Function( + BuildContext context, + User user, +); + +/// Widget builder for action button. +/// +/// [defaultActionButton] is the default [IconButton] configuration, +/// use [defaultActionButton.copyWith] to easily customize it. +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, +); + +/// Widget builder for widgets that may require data from the +/// [StreamMessageInputController]. +typedef MessageRelatedBuilder = Widget Function( + BuildContext context, + StreamMessageInputController messageInputController, +); + +/// Widget builder for a custom attachment picker. +typedef AttachmentsPickerBuilder = Widget Function( + BuildContext context, + StreamMessageInputController messageInputController, + StreamAttachmentPicker defaultPicker, +); + +/// Location for actions on the [StreamMessageInput]. +enum ActionsLocation { + /// Align to left + left, + + /// Align to right + right, + + /// Align to left but inside the [TextField] + leftInside, + + /// Align to right but inside the [TextField] + rightInside, +} + +/// Default attachments for widget. +enum DefaultAttachmentTypes { + /// Image Attachment + image, + + /// Video Attachment + video, + + /// File Attachment + file, +} + +/// Available locations for the `sendMessage` button relative to the textField. +enum SendButtonLocation { + /// inside the textField + inside, + + /// outside the textField + outside, +} + +const _kMinMediaPickerSize = 360.0; + +const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes + +/// Inactive state: +/// +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input_paint.png) +/// +/// Focused state: +/// +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2_paint.png) +/// +/// Widget used to enter a message and add attachments: +/// +/// ```dart +/// class ChannelPage extends StatelessWidget { +/// const ChannelPage({ +/// Key? key, +/// }) : super(key: key); +/// +/// @override +/// Widget build(BuildContext context) => Scaffold( +/// appBar: const StreamChannelHeader(), +/// body: Column( +/// children: [ +/// Expanded( +/// child: StreamMessageListView( +/// threadBuilder: (_, parentMessage) => ThreadPage( +/// parent: parentMessage, +/// ), +/// ), +/// ), +/// const StreamMessageInput(), +/// ], +/// ), +/// ); +/// } +/// ``` +/// +/// You usually put this widget in the same page of a [StreamMessageListView] +/// as the bottom widget. +/// +/// The widget renders the ui based on the first ancestor of +/// type [StreamChatTheme]. Modify it to change the widget appearance. +class StreamMessageInput extends StatefulWidget { + /// Instantiate a new MessageInput + const StreamMessageInput({ + Key? key, + this.onMessageSent, + this.preMessageSending, + this.maxHeight = 150, + this.keyboardType = TextInputType.multiline, + this.disableAttachments = false, + this.messageInputController, + this.actions = const [], + this.actionsLocation = ActionsLocation.left, + this.attachmentThumbnailBuilders, + this.focusNode, + this.sendButtonLocation = SendButtonLocation.outside, + this.autofocus = false, + this.hideSendAsDm = false, + this.idleSendButton, + this.activeSendButton, + this.showCommandsButton = true, + this.userMentionsTileBuilder, + this.maxAttachmentSize = _kDefaultMaxAttachmentSize, + this.onError, + this.attachmentLimit = 10, + this.onAttachmentLimitExceed, + this.attachmentButtonBuilder, + this.commandButtonBuilder, + this.customOverlays = const [], + this.mentionAllAppUsers = false, + this.attachmentsPickerBuilder, + this.sendButtonBuilder, + this.shouldKeepFocusAfterMessage, + this.validator = _defaultValidator, + this.restorationId, + this.enableSafeArea, + this.elevation, + this.shadow, + this.autoCorrect, + this.disableEmojiSuggestionsOverlay, + }) : super(key: key); + + /// List of options for showing overlays. + final List customOverlays; + + /// Max attachment size in bytes: + /// - Defaults to 20 MB + /// - Do not set it if you're using our default CDN + final int maxAttachmentSize; + + /// Function called after sending the message. + final void Function(Message)? onMessageSent; + + /// Function called right before sending the message. + /// + /// Use this to transform the message. + final FutureOr Function(Message)? preMessageSending; + + /// Maximum Height for the TextField to grow before it starts scrolling. + final double maxHeight; + + /// The keyboard type assigned to the TextField. + final TextInputType keyboardType; + + /// If true the attachments button will not be displayed. + final bool disableAttachments; + + /// Use this property to hide/show the commands button. + final bool showCommandsButton; + + /// Hide send as dm checkbox. + final bool hideSendAsDm; + + /// The text controller of the TextField. + final StreamMessageInputController? messageInputController; + + /// List of action widgets. + final List actions; + + /// The location of the custom actions. + final ActionsLocation actionsLocation; + + /// Map that defines a thumbnail builder for an attachment type. + final Map? attachmentThumbnailBuilders; + + /// The focus node associated to the TextField. + final FocusNode? focusNode; + + /// The location of the send button + final SendButtonLocation sendButtonLocation; + + /// Autofocus property passed to the TextField + final bool autofocus; + + /// Send button widget in an idle state + final Widget? idleSendButton; + + /// Send button widget in an active state + final Widget? activeSendButton; + + /// Customize the tile for the mentions overlay. + final UserMentionTileBuilder? userMentionsTileBuilder; + + /// A callback for error reporting + final ErrorListener? onError; + + /// A limit for the no. of attachments that can be sent with a single message. + final int attachmentLimit; + + /// A callback for when the [attachmentLimit] is exceeded. + /// + /// This will override the default error alert behaviour. + final AttachmentLimitExceedListener? onAttachmentLimitExceed; + + /// Builder for customizing the attachment button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. + final ActionButtonBuilder? attachmentButtonBuilder; + + /// Builder for customizing the command button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. + final ActionButtonBuilder? commandButtonBuilder; + + /// When enabled mentions search users across the entire app. + /// + /// Defaults to false. + final bool mentionAllAppUsers; + + /// Builds bottom sheet when attachment picker is opened. + final AttachmentsPickerBuilder? attachmentsPickerBuilder; + + /// Builder for creating send button + final MessageRelatedBuilder? sendButtonBuilder; + + /// Defines if the [StreamMessageInput] loses focuses after a message is sent. + /// The default behaviour keeps focus until a command is enabled. + final bool? shouldKeepFocusAfterMessage; + + /// A callback function that validates the message. + final MessageValidator validator; + + /// Restoration ID to save and restore the state of the MessageInput. + final String? restorationId; + + /// Wrap [StreamMessageInput] with a [SafeArea widget] + final bool? enableSafeArea; + + /// Elevation of the [StreamMessageInput] + final double? elevation; + + /// Shadow for the [StreamMessageInput] widget + final BoxShadow? shadow; + + /// Disable autoCorrect by passing false + /// autoCorrect is enabled by default + final bool? autoCorrect; + + /// Disable the default emoji suggestions + /// Enabled by default + final bool? disableEmojiSuggestionsOverlay; + + static bool _defaultValidator(Message message) => + message.text?.isNotEmpty == true || message.attachments.isNotEmpty; + + @override + StreamMessageInputState createState() => StreamMessageInputState(); +} + +/// State of [StreamMessageInput] +class StreamMessageInputState extends State + with RestorationMixin { + final _imagePicker = ImagePicker(); + late FocusNode _focusNode = widget.focusNode ?? FocusNode(); + late final _isInternalFocusNode = widget.focusNode == null; + bool _inputEnabled = true; + + bool get _commandEnabled => _effectiveController.value.command != null; + bool _showCommandsOverlay = false; + bool _showMentionsOverlay = false; + + bool _actionsShrunk = false; + bool _openFilePickerSection = false; + + late StreamChatThemeData _streamChatTheme; + late StreamMessageInputThemeData _messageInputTheme; + + bool get _hasQuotedMessage => + _effectiveController.value.quotedMessage != null; + + bool get _isEditing => + _effectiveController.value.status != MessageSendingStatus.sending; + + bool get _autoCorrect => widget.autoCorrect ?? true; + + bool get _disableEmojiSuggestionsOverlay => + widget.disableEmojiSuggestionsOverlay ?? false; + + StreamRestorableMessageInputController? _controller; + + StreamMessageInputController get _effectiveController => + widget.messageInputController ?? _controller!.value; + + void _createLocalController([Message? message]) { + assert(_controller == null, ''); + _controller = StreamRestorableMessageInputController(message: message); + } + + void _registerController() { + assert(_controller != null, ''); + + registerForRestoration( + _controller!, + widget.restorationId ?? 'messageInputController', + ); + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _effectiveController.textEditingController.addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); + } + + @override + void initState() { + super.initState(); + if (widget.messageInputController == null) { + _createLocalController(); + } else { + _initialiseEffectiveController(); + } + _focusNode.addListener(_focusNodeListener); + } + + @override + void didUpdateWidget(covariant StreamMessageInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.messageInputController == null && + oldWidget.messageInputController != null) { + _createLocalController(oldWidget.messageInputController!.value); + } else if (widget.messageInputController != null && + oldWidget.messageInputController == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + _initialiseEffectiveController(); + } + + // Update _focusNode + if (widget.focusNode != null && oldWidget.focusNode != widget.focusNode) { + _focusNode.removeListener(_focusNodeListener); + _focusNode = widget.focusNode!; + _focusNode.addListener(_focusNodeListener); + } + } + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (_controller != null) { + _registerController(); + } + } + + @override + String? get restorationId => widget.restorationId; + + void _focusNodeListener() { + if (_focusNode.hasFocus) { + _openFilePickerSection = false; + } + } + + int _timeOut = 0; + Timer? _slowModeTimer; + + void _initialiseEffectiveController() { + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _effectiveController.textEditingController.addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); + } + + void _startSlowMode() { + if (!mounted) { + return; + } + final channel = StreamChannel.of(context).channel; + final cooldownStartedAt = channel.cooldownStartedAt; + if (cooldownStartedAt != null) { + final diff = DateTime.now().difference(cooldownStartedAt).inSeconds; + if (diff < channel.cooldown) { + _timeOut = channel.cooldown - diff; + if (_timeOut > 0) { + _slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_timeOut == 0) { + timer.cancel(); + } else { + if (mounted) { + setState(() => _timeOut -= 1); + } + } + }); + } + } + } + } + + void _stopSlowMode() => _slowModeTimer?.cancel(); + + @override + Widget build(BuildContext context) { + final channel = StreamChannel.of(context).channel; + if (channel.state != null && + !channel.ownCapabilities.contains(PermissionType.sendMessage)) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 15, + ), + child: Text( + context.translations.sendMessagePermissionError, + style: _messageInputTheme.inputTextStyle, + ), + ), + ); + } + return StreamMessageValueListenableBuilder( + valueListenable: _effectiveController, + builder: (context, value, _) { + Widget child = DecoratedBox( + decoration: BoxDecoration( + color: _messageInputTheme.inputBackgroundColor, + boxShadow: widget.shadow == null + ? (_streamChatTheme.messageInputTheme.shadow == null + ? [] + : [_streamChatTheme.messageInputTheme.shadow!]) + : [widget.shadow!], + ), + child: SimpleSafeArea( + enabled: widget.enableSafeArea ?? + _streamChatTheme.messageInputTheme.enableSafeArea ?? + true, + child: GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy > 0) { + _focusNode.unfocus(); + if (_openFilePickerSection) { + setState(() { + _openFilePickerSection = false; + }); + } + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_hasQuotedMessage) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.reply( + color: _streamChatTheme.colorTheme.disabled, + ), + ), + Text( + context.translations.replyToMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: () { + _effectiveController.clearQuotedMessage(); + _focusNode.unfocus(); + }, + ), + ], + ), + ) + else if (_effectiveController.ogAttachment != null) + OGAttachmentPreview( + attachment: _effectiveController.ogAttachment!, + onDismissPreviewPressed: () { + _effectiveController.clearOGAttachment(); + _focusNode.unfocus(); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: _buildTextField(context), + ), + if (_effectiveController.value.parentId != null && + !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], + ), + ), + ), + ); + if (!_isEditing) { + child = Material( + elevation: widget.elevation ?? + _streamChatTheme.messageInputTheme.elevation ?? + 8, + color: _messageInputTheme.inputBackgroundColor, + child: child, + ); + } + return StreamMultiOverlay( + childAnchor: Alignment.topCenter, + overlayAnchor: Alignment.bottomCenter, + overlayOptions: [ + OverlayOptions( + visible: _showCommandsOverlay, + widget: _buildCommandsOverlayEntry(), + ), + if (!_disableEmojiSuggestionsOverlay) + OverlayOptions( + visible: _focusNode.hasFocus && + _effectiveController.text.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring( + 0, + _effectiveController.baseOffset, + ) + .contains(':'), + widget: _buildEmojiOverlay(), + ), + OverlayOptions( + visible: _showMentionsOverlay, + widget: _buildMentionsOverlayEntry(), + ), + ...widget.customOverlays, + ], + child: child, + ); + }, + ); + } + + Flex _buildTextField(BuildContext context) => Flex( + direction: Axis.horizontal, + children: [ + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.left) + _buildExpandActionsButton(context), + _buildTextInput(context), + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.right) + _buildExpandActionsButton(context), + if (widget.sendButtonLocation == SendButtonLocation.outside) + _buildSendButton(context), + ], + ); + + Widget _buildDmCheckbox() => Row( + children: [ + Container( + height: 16, + width: 16, + foregroundDecoration: BoxDecoration( + border: _effectiveController.showInChannel + ? null + : Border.all( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + child: Center( + child: Material( + borderRadius: BorderRadius.circular(3), + color: _effectiveController.showInChannel + ? _streamChatTheme.colorTheme.accentPrimary + : _streamChatTheme.colorTheme.barsBg, + child: InkWell( + onTap: () { + _effectiveController.showInChannel = + !_effectiveController.showInChannel; + }, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + crossFadeState: _effectiveController.showInChannel + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: StreamSvgIcon.check( + size: 16, + color: _streamChatTheme.colorTheme.barsBg, + ), + secondChild: const SizedBox( + height: 16, + width: 16, + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + context.translations.alsoSendAsDirectMessageLabel, + style: _streamChatTheme.textTheme.footnote.copyWith( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + ), + ), + ), + ], + ); + + Widget _buildSendButton(BuildContext context) { + if (widget.sendButtonBuilder != null) { + return widget.sendButtonBuilder!(context, _effectiveController); + } + + return StreamMessageSendButton( + onSendMessage: sendMessage, + timeOut: _timeOut, + isIdle: !widget.validator(_effectiveController.message), + isEditEnabled: _isEditing, + idleSendButton: widget.idleSendButton, + activeSendButton: widget.activeSendButton, + ); + } + + Widget _buildExpandActionsButton(BuildContext context) { + final channel = StreamChannel.of(context).channel; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: AnimatedCrossFade( + crossFadeState: _actionsShrunk + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstCurve: Curves.easeOut, + secondCurve: Curves.easeIn, + firstChild: IconButton( + onPressed: () { + if (_actionsShrunk) { + setState(() => _actionsShrunk = false); + } + }, + icon: Transform.rotate( + angle: (widget.actionsLocation == ActionsLocation.right || + widget.actionsLocation == ActionsLocation.rightInside) + ? pi + : 0, + child: StreamSvgIcon.emptyCircleLeft( + color: _messageInputTheme.expandButtonColor, + ), + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + ), + secondChild: widget.disableAttachments && + !widget.showCommandsButton && + !widget.actions.isNotEmpty + ? const Offstage() + : Wrap( + children: [ + if (!widget.disableAttachments && + channel.ownCapabilities + .contains(PermissionType.uploadFile)) + _buildAttachmentButton(context), + if (widget.showCommandsButton && + !_isEditing && + channel.state != null && + channel.config?.commands.isNotEmpty == true) + _buildCommandButton(context), + ...widget.actions, + ].insertBetween(const SizedBox(width: 8)), + ), + duration: const Duration(milliseconds: 300), + alignment: Alignment.center, + ), + ); + } + + Expanded _buildTextInput(BuildContext context) { + final margin = (widget.sendButtonLocation == SendButtonLocation.inside + ? const EdgeInsets.only(right: 8) + : EdgeInsets.zero) + + (widget.actionsLocation != ActionsLocation.left || _commandEnabled + ? const EdgeInsets.only(left: 8) + : EdgeInsets.zero); + return Expanded( + child: Container( + clipBehavior: Clip.hardEdge, + margin: margin, + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + gradient: _focusNode.hasFocus + ? _messageInputTheme.activeBorderGradient + : _messageInputTheme.idleBorderGradient, + color: _messageInputTheme.inputBackgroundColor, + ), + child: Padding( + padding: const EdgeInsets.all(1.5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + color: _messageInputTheme.inputBackgroundColor, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildReplyToMessage(), + _buildAttachments(), + LimitedBox( + maxHeight: widget.maxHeight, + child: StreamMessageTextField( + key: const Key('messageInputText'), + enabled: _inputEnabled, + maxLines: null, + onSubmitted: (_) => sendMessage(), + keyboardType: widget.keyboardType, + controller: _effectiveController, + focusNode: _focusNode, + style: _messageInputTheme.inputTextStyle, + autofocus: widget.autofocus, + textAlignVertical: TextAlignVertical.center, + decoration: _getInputDecoration(context), + textCapitalization: TextCapitalization.sentences, + autocorrect: _autoCorrect, + ), + ), + ], + ), + ), + ), + ), + ); + } + + InputDecoration _getInputDecoration(BuildContext context) { + final passedDecoration = _messageInputTheme.inputDecoration; + return InputDecoration( + isDense: true, + hintText: _getHint(context), + hintStyle: _messageInputTheme.inputTextStyle!.copyWith( + color: _streamChatTheme.colorTheme.textLowEmphasis, + ), + border: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + errorBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + disabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), + prefixIcon: _commandEnabled + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Container( + constraints: BoxConstraints.tight(const Size(64, 24)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: _streamChatTheme.colorTheme.accentPrimary, + ), + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.lightning( + color: Colors.white, + size: 16, + ), + Text( + _effectiveController.value.command!.toUpperCase(), + style: + _streamChatTheme.textTheme.footnoteBold.copyWith( + color: Colors.white, + ), + ), + ], + ), + ), + ), + ], + ) + : (widget.actionsLocation == ActionsLocation.leftInside + ? Row( + mainAxisSize: MainAxisSize.min, + children: [_buildExpandActionsButton(context)], + ) + : null), + suffixIconConstraints: const BoxConstraints.tightFor(height: 40), + prefixIconConstraints: const BoxConstraints.tightFor(height: 40), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_commandEnabled) + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: StreamSvgIcon.closeSmall(), + splashRadius: 24, + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: _effectiveController.clear, + ), + ), + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.rightInside) + _buildExpandActionsButton(context), + if (widget.sendButtonLocation == SendButtonLocation.inside) + _buildSendButton(context), + ], + ), + ).merge(passedDecoration); + } + + late final _onChangedDebounced = debounce( + () { + var value = _effectiveController.text; + if (!mounted) return; + value = value.trim(); + + final channel = StreamChannel.of(context).channel; + if (channel.ownCapabilities.contains(PermissionType.sendTypingEvents) && + value.isNotEmpty) { + channel + .keyStroke(_effectiveController.value.parentId) + // ignore: no-empty-block + .catchError((e) {}); + } + + var actionsLength = widget.actions.length; + if (widget.showCommandsButton) actionsLength += 1; + if (!widget.disableAttachments) actionsLength += 1; + + setState(() { + _actionsShrunk = value.isNotEmpty && actionsLength > 1; + }); + + _checkContainsUrl(value, context); + _checkCommands(value, context); + _checkMentions(value, context); + _checkEmoji(value, context); + }, + const Duration(milliseconds: 350), + leading: true, + ); + + String _getHint(BuildContext context) { + if (_commandEnabled && _effectiveController.value.command == 'giphy') { + return context.translations.searchGifLabel; + } + if (_effectiveController.attachments.isNotEmpty) { + return context.translations.addACommentOrSendLabel; + } + if (_timeOut != 0) { + return context.translations.slowModeOnLabel; + } + + return context.translations.writeAMessageLabel; + } + + String? _lastSearchedContainsUrlText; + CancelableOperation? _enrichUrlOperation; + final _urlRegex = RegExp( + r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', + ); + + void _checkContainsUrl(String value, BuildContext context) async { + // Cancel the previous operation if it's still running + _enrichUrlOperation?.cancel(); + + // If the text is same as the last time, don't do anything + if (_lastSearchedContainsUrlText == value) return; + _lastSearchedContainsUrlText = value; + + final matchedUrls = _urlRegex.allMatches(value).toList() + ..removeWhere((it) => it.group(0)?.split('.').last.isValidTLD() == false); + + // Reset the og attachment if the text doesn't contain any url + if (matchedUrls.isEmpty || + !StreamChannel.of(context) + .channel + .ownCapabilities + .contains(PermissionType.sendLinks)) { + _effectiveController.clearOGAttachment(); + return; + } + + final firstMatchedUrl = matchedUrls.first.group(0)!; + + // If the parsed url matches the ogAttachment url, don't do anything + if (_effectiveController.ogAttachment?.titleLink == firstMatchedUrl) { + return; + } + + final client = StreamChat.of(context).client; + + _enrichUrlOperation = CancelableOperation.fromFuture( + _enrichUrl(firstMatchedUrl, client), + ).then( + (ogAttachment) { + final attachment = Attachment.fromOGAttachment(ogAttachment); + _effectiveController.setOGAttachment(attachment); + }, + onError: (error, stackTrace) { + // Reset the ogAttachment if there was an error + _effectiveController.clearOGAttachment(); + widget.onError?.call(error, stackTrace); + }, + ); + } + + final _ogAttachmentCache = {}; + + Future _enrichUrl( + String url, + StreamChatClient client, + ) async { + var response = _ogAttachmentCache[url]; + if (response == null) { + final client = StreamChat.of(context).client; + response = await client.enrichUrl(url); + _ogAttachmentCache[url] = response; + } + return response; + } + + void _checkEmoji(String value, BuildContext context) { + if (value.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring(0, _effectiveController.baseOffset) + .contains(':')) { + final textToSelection = _effectiveController.text.substring( + 0, + _effectiveController.selectionStart, + ); + final splits = textToSelection.split(':'); + final query = splits[splits.length - 2].toLowerCase(); + final emoji = Emoji.byName(query); + + if (textToSelection.endsWith(':') && emoji != null) { + _chooseEmoji(splits.sublist(0, splits.length - 1), emoji); + } + } + } + + void _checkMentions(String value, BuildContext context) { + if (value.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring(0, _effectiveController.baseOffset) + .split(' ') + .last + .contains('@')) { + if (!_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = true; + }); + } + } else if (_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = false; + }); + } + } + + void _checkCommands(String value, BuildContext context) { + if (value.startsWith('/')) { + final allCommands = StreamChannel.of(context).channel.config?.commands; + final command = + allCommands?.firstWhereOrNull((it) => it.name == value.substring(1)); + if (command != null) { + return _setCommand(command); + } else if (!_showCommandsOverlay) { + setState(() { + _showCommandsOverlay = true; + }); + } + } else if (_showCommandsOverlay) { + setState(() { + _showCommandsOverlay = false; + }); + } + } + + Widget _buildCommandsOverlayEntry() { + final text = _effectiveController.text.trimLeft(); + + final renderObject = context.findRenderObject() as RenderBox?; + if (renderObject == null) { + return const Offstage(); + } + return StreamCommandsOverlay( + channel: StreamChannel.of(context).channel, + size: Size(renderObject.size.width - 16, 400), + text: text, + onCommandResult: _setCommand, + ); + } + + Widget _buildFilePickerSection() { + final picker = StreamAttachmentPicker( + messageInputController: _effectiveController, + onFilePicked: pickFile, + isOpen: _openFilePickerSection, + pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0, + attachmentLimit: widget.attachmentLimit, + onAttachmentLimitExceeded: widget.onAttachmentLimitExceed, + maxAttachmentSize: widget.maxAttachmentSize, + onError: _showErrorAlert, + ); + + if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { + return widget.attachmentsPickerBuilder!( + context, + _effectiveController, + picker, + ); + } + + return picker; + } + + Widget _buildMentionsOverlayEntry() { + final channel = StreamChannel.of(context).channel; + if (_effectiveController.selectionStart < 0 || channel.state == null) { + return const Offstage(); + } + + final splits = _effectiveController.text + .substring(0, _effectiveController.selectionStart) + .split('@'); + final query = splits.last.toLowerCase(); + + // ignore: cast_nullable_to_non_nullable + final renderObject = context.findRenderObject() as RenderBox; + + return LayoutBuilder( + builder: (context, snapshot) => StreamUserMentionsOverlay( + query: query, + mentionAllAppUsers: widget.mentionAllAppUsers, + client: StreamChat.of(context).client, + channel: channel, + size: Size( + renderObject.size.width - 16, + min(400, (snapshot.maxHeight - renderObject.size.height - 16).abs()), + ), + mentionsTileBuilder: widget.userMentionsTileBuilder, + onMentionUserTap: (user) { + _effectiveController.addMentionedUser(user); + splits[splits.length - 1] = user.name; + final rejoin = splits.join('@'); + + _effectiveController.text = + '$rejoin${_effectiveController.text.substring( + _effectiveController.selectionStart, + )}'; + + _onChangedDebounced.cancel(); + setState(() => _showMentionsOverlay = false); + }, + ), + ); + } + + Widget _buildEmojiOverlay() { + if (_effectiveController.baseOffset < 0) { + return const Offstage(); + } + + final splits = _effectiveController.text + .substring(0, _effectiveController.baseOffset) + .split(':'); + + final query = splits.last.toLowerCase(); + // ignore: cast_nullable_to_non_nullable + final renderObject = context.findRenderObject() as RenderBox; + + return StreamEmojiOverlay( + size: Size(renderObject.size.width - 16, 200), + query: query, + onEmojiResult: (emoji) { + _chooseEmoji(splits, emoji); + }, + ); + } + + void _chooseEmoji(List splits, Emoji emoji) { + final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; + + _effectiveController.text = rejoin + + _effectiveController.text.substring( + _effectiveController.selectionStart, + ); + } + + void _setCommand(Command c) { + _effectiveController + ..clear() + ..command = c; + setState(() { + _showCommandsOverlay = false; + }); + } + + Widget _buildReplyToMessage() { + if (!_hasQuotedMessage) return const Offstage(); + final containsUrl = _effectiveController.value.quotedMessage!.attachments + .any((element) => element.titleLink != null); + return StreamQuotedMessageWidget( + reverse: true, + showBorder: !containsUrl, + message: _effectiveController.value.quotedMessage!, + messageTheme: _streamChatTheme.otherMessageTheme, + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + ); + } + + Widget _buildAttachments() { + final nonOGAttachments = _effectiveController.attachments.where( + (it) => it.titleLink == null, + ); + if (nonOGAttachments.isEmpty) return const Offstage(); + final fileAttachments = nonOGAttachments + .where((it) => it.type == 'file') + .toList(growable: false); + final remainingAttachments = nonOGAttachments + .where((it) => it.type != 'file') + .toList(growable: false); + return Column( + children: [ + if (fileAttachments.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 136, + child: ListView( + reverse: true, + shrinkWrap: true, + children: fileAttachments.reversed + .map( + (e) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: StreamFileAttachment( + message: Message(), // dummy message + attachment: e, + size: Size( + MediaQuery.of(context).size.width * 0.65, + 56, + ), + trailing: Padding( + padding: const EdgeInsets.all(8), + child: _buildRemoveButton(e), + ), + ), + ), + ) + .insertBetween(const SizedBox(height: 8)), + ), + ), + ), + if (remainingAttachments.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 104, + child: ListView( + scrollDirection: Axis.horizontal, + children: remainingAttachments + .map( + (attachment) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1, + child: SizedBox( + height: 104, + width: 104, + child: _buildAttachment(attachment), + ), + ), + Positioned( + top: 8, + right: 8, + child: _buildRemoveButton(attachment), + ), + ], + ), + ), + ) + .insertBetween(const SizedBox(width: 8)), + ), + ), + ), + ], + ); + } + + Widget _buildRemoveButton(Attachment attachment) => SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: () { + _effectiveController.removeAttachmentById(attachment.id); + }, + fillColor: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), + child: Center( + child: StreamSvgIcon.close( + size: 24, + color: _streamChatTheme.colorTheme.barsBg, + ), + ), + ), + ); + + Widget _buildAttachment(Attachment attachment) { + if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == + true) { + return widget.attachmentThumbnailBuilders![attachment.type!]!( + context, + attachment, + ); + } + + switch (attachment.type) { + case 'image': + case 'giphy': + return attachment.file != null + ? Image.memory( + attachment.file!.bytes!, + fit: BoxFit.cover, + errorBuilder: (context, _, __) => Image.asset( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ), + ) + : CachedNetworkImage( + imageUrl: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl!, + fit: BoxFit.cover, + errorWidget: (_, obj, trace) => + getFileTypeImage(attachment.extraData['other'] as String?), + placeholder: (context, _) => Shimmer.fromColors( + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, + child: Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ), + ), + ); + case 'video': + return Stack( + children: [ + StreamVideoThumbnailImage( + height: 104, + width: 104, + video: (attachment.file?.path ?? attachment.assetUrl)!, + fit: BoxFit.cover, + ), + Positioned( + left: 8, + bottom: 10, + child: SvgPicture.asset( + 'svgs/video_call_icon.svg', + package: 'stream_chat_flutter', + ), + ), + ], + ); + default: + return Container( + color: Colors.black26, + child: const Icon(Icons.insert_drive_file), + ); + } + } + + Widget _buildCommandButton(BuildContext context) { + final s = _effectiveController.text.trim(); + final defaultButton = IconButton( + icon: StreamSvgIcon.lightning( + color: s.isNotEmpty + ? _streamChatTheme.colorTheme.disabled + : (_showCommandsOverlay + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor), + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + if (_openFilePickerSection) { + setState(() => _openFilePickerSection = false); + await Future.delayed(const Duration(milliseconds: 300)); + } + + setState(() { + _showCommandsOverlay = !_showCommandsOverlay; + }); + }, + ); + + return widget.commandButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } + + Widget _buildAttachmentButton(BuildContext context) { + final defaultButton = IconButton( + icon: StreamSvgIcon.attach( + color: _openFilePickerSection + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor, + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + _showCommandsOverlay = false; + _showMentionsOverlay = false; + + if (_openFilePickerSection) { + setState(() => _openFilePickerSection = false); + } else { + showAttachmentModal(); + } + }, + ); + + return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } + + /// Show the attachment modal, making the user choose where to + /// pick a media from + void showAttachmentModal() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + + if (!kIsWeb) { + setState(() { + _openFilePickerSection = true; + }); + } else { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + isScrollControlled: true, + builder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + context.translations.addAFileLabel, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + leading: const Icon(Icons.image), + title: Text(context.translations.uploadAPhotoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.image); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.video_library), + title: Text(context.translations.uploadAVideoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.video); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.insert_drive_file), + title: Text(context.translations.uploadAFileLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.file); + Navigator.pop(context); + }, + ), + ], + ), + ); + } + } + + /// Adds an attachment to the [messageInputController.attachments] map + void _addAttachments(Iterable attachments) { + final limit = widget.attachmentLimit; + final length = _effectiveController.attachments.length + attachments.length; + if (length > limit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + widget.attachmentLimit, + context.translations.attachmentLimitExceedError(limit), + ); + } + return _showErrorAlert( + context.translations.attachmentLimitExceedError(limit), + ); + } + for (final attachment in attachments) { + _effectiveController.addAttachment(attachment); + } + } + + /// Pick a file from the device + /// If [camera] is true then the camera will open + void pickFile( + DefaultAttachmentTypes fileType, { + bool camera = false, + }) async { + setState(() => _inputEnabled = false); + + AttachmentFile? file; + String? attachmentType; + + if (fileType == DefaultAttachmentTypes.image) { + attachmentType = 'image'; + } else if (fileType == DefaultAttachmentTypes.video) { + attachmentType = 'video'; + } else if (fileType == DefaultAttachmentTypes.file) { + attachmentType = 'file'; + } + + if (camera) { + XFile? pickedFile; + if (fileType == DefaultAttachmentTypes.image) { + pickedFile = await _imagePicker.pickImage(source: ImageSource.camera); + } else if (fileType == DefaultAttachmentTypes.video) { + pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); + } + if (pickedFile != null) { + final bytes = await pickedFile.readAsBytes(); + file = AttachmentFile( + size: bytes.length, + path: pickedFile.path, + bytes: bytes, + ); + } + } else { + late FileType type; + if (fileType == DefaultAttachmentTypes.image) { + type = FileType.image; + } else if (fileType == DefaultAttachmentTypes.video) { + type = FileType.video; + } else if (fileType == DefaultAttachmentTypes.file) { + type = FileType.any; + } + final res = await FilePicker.platform.pickFiles( + type: type, + ); + if (res?.files.isNotEmpty == true) { + file = res!.files.single.toAttachmentFile; + } + } + + setState(() => _inputEnabled = true); + + if (file == null) return; + + final mimeType = file.name?.mimeType ?? file.path!.split('/').last.mimeType; + + final extraDataMap = {}; + + if (mimeType?.subtype != null) { + extraDataMap['mime_type'] = mimeType!.subtype.toLowerCase(); + } + + extraDataMap['file_size'] = file.size!; + + final attachment = Attachment( + file: file, + type: attachmentType, + uploadState: const UploadState.preparing(), + extraData: extraDataMap, + ); + + if (file.size! > widget.maxAttachmentSize) { + return _showErrorAlert( + context.translations.fileTooLargeError( + widget.maxAttachmentSize / (1024 * 1024), + ), + ); + } + + _addAttachments([ + attachment.copyWith( + file: file, + extraData: {...attachment.extraData} + ..update('file_size', ((_) => file!.size!)), + ), + ]); + } + + /// Sends the current message + Future sendMessage() async { + final streamChannel = StreamChannel.of(context); + var message = _effectiveController.value; + if (!streamChannel.channel.ownCapabilities + .contains(PermissionType.sendLinks) && + _urlRegex.allMatches(message.text ?? '').any((element) => + element.group(0)?.split('.').last.isValidTLD() == true)) { + showInfoDialog( + context, + icon: StreamSvgIcon.error( + color: StreamChatTheme.of(context).colorTheme.accentError, + size: 24, + ), + title: 'Links are disabled', + details: 'Sending links is not allowed in this conversation.', + okText: context.translations.okLabel, + ); + return; + } + + final skipEnrichUrl = _effectiveController.ogAttachment == null; + + var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; + + shouldKeepFocus ??= !_commandEnabled; + + _effectiveController.reset(); + + if (widget.preMessageSending != null) { + message = await widget.preMessageSending!(message); + } + + final channel = streamChannel.channel; + if (!channel.state!.isUpToDate) { + await streamChannel.reloadChannel(); + } + + message = message.replaceMentionsWithId(); + + try { + Future sendingFuture; + if (_isEditing) { + sendingFuture = channel.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); + } else { + sendingFuture = channel.sendMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); + } + + if (shouldKeepFocus) { + FocusScope.of(context).requestFocus(_focusNode); + } else { + FocusScope.of(context).unfocus(); + } + + final resp = await sendingFuture; + if (resp.message?.type == 'error') { + _effectiveController.value = message; + } + _startSlowMode(); + widget.onMessageSent?.call(resp.message); + } catch (e, stk) { + if (widget.onError != null) { + widget.onError?.call(e, stk); + } else { + rethrow; + } + } + } + + void _showErrorAlert(String description) { + showModalBottomSheet( + backgroundColor: _streamChatTheme.colorTheme.barsBg, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + builder: (context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 26, + ), + StreamSvgIcon.error( + color: _streamChatTheme.colorTheme.accentError, + size: 24, + ), + const SizedBox( + height: 26, + ), + Text( + context.translations.somethingWentWrongError, + style: _streamChatTheme.textTheme.headlineBold, + ), + const SizedBox( + height: 7, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + description, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 36, + ), + Container( + color: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08), + height: 1, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.translations.okLabel, + style: _streamChatTheme.textTheme.bodyBold.copyWith( + color: _streamChatTheme.colorTheme.accentPrimary, + ), + ), + ), + ], + ), + ], + ), + ); + } + + @override + void dispose() { + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _controller?.dispose(); + _focusNode.removeListener(_focusNodeListener); + if (_isInternalFocusNode) _focusNode.dispose(); + _stopSlowMode(); + _onChangedDebounced.cancel(); + super.dispose(); + } + + @override + void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); + _messageInputTheme = StreamMessageInputTheme.of(context); + + super.didChangeDependencies(); + } +} + +/// Preview of an Open Graph attachment. +class OGAttachmentPreview extends StatelessWidget { + /// Returns a new instance of [OGAttachmentPreview] + const OGAttachmentPreview({ + Key? key, + required this.attachment, + this.onDismissPreviewPressed, + }) : super(key: key); + + /// The attachment to be rendered. + final Attachment attachment; + + /// Called when the dismiss button is pressed. + final VoidCallback? onDismissPreviewPressed; + + @override + Widget build(BuildContext context) { + final chatTheme = StreamChatTheme.of(context); + final textTheme = chatTheme.textTheme; + final colorTheme = chatTheme.colorTheme; + + final attachmentTitle = attachment.title; + final attachmentText = attachment.text; + + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Icon( + Icons.link, + color: colorTheme.accentPrimary, + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: colorTheme.accentPrimary, + width: 2, + ), + ), + ), + padding: const EdgeInsets.only(left: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (attachmentTitle != null) + Text( + attachmentTitle.trim(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.body.copyWith(fontWeight: FontWeight.w700), + ), + if (attachmentText != null) + Text( + attachmentText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.body.copyWith(fontWeight: FontWeight.w400), + ), + ], + ), + ), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: onDismissPreviewPressed, + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_send_button.dart new file mode 100644 index 000000000..67a06f670 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_send_button.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that displays a sending button. +class StreamMessageSendButton extends StatelessWidget { + /// Returns a [StreamMessageSendButton] with the given [timeOut], [isIdle], + /// [isCommandEnabled], [isEditEnabled], [idleSendButton], [activeSendButton], + /// [onSendMessage]. + const StreamMessageSendButton({ + Key? key, + this.timeOut = 0, + this.isIdle = true, + this.isCommandEnabled = false, + this.isEditEnabled = false, + this.idleSendButton, + this.activeSendButton, + required this.onSendMessage, + }) : super(key: key); + + /// Time out related to slow mode. + final int timeOut; + + /// If true the button will be disabled. + final bool isIdle; + + /// True if a command is being sent. + final bool isCommandEnabled; + + /// True if in editing mode. + final bool isEditEnabled; + + /// The widget to display when the button is disabled. + final Widget? idleSendButton; + + /// The widget to display when the button is enabled. + final Widget? activeSendButton; + + /// The callback to call when the button is pressed. + final VoidCallback onSendMessage; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + + late Widget sendButton; + if (timeOut > 0) { + sendButton = StreamCountdownButton(count: timeOut); + } else if (isIdle) { + sendButton = idleSendButton ?? _buildIdleSendButton(context); + } else { + sendButton = activeSendButton != null + ? InkWell( + onTap: onSendMessage, + child: activeSendButton, + ) + : _buildSendButton(context); + } + + return AnimatedSwitcher( + duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, + child: sendButton, + ); + } + + Widget _buildIdleSendButton(BuildContext context) { + final _messageInputTheme = StreamMessageInputTheme.of(context); + + return Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon( + assetName: _getIdleSendIcon(), + color: _messageInputTheme.sendButtonIdleColor, + ), + ); + } + + Widget _buildSendButton(BuildContext context) { + final _messageInputTheme = StreamMessageInputTheme.of(context); + + return Padding( + padding: const EdgeInsets.all(8), + child: IconButton( + onPressed: onSendMessage, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + icon: StreamSvgIcon( + assetName: _getSendIcon(), + color: _messageInputTheme.sendButtonColor, + ), + ), + ); + } + + String _getIdleSendIcon() { + if (isCommandEnabled) { + return 'Icon_search.svg'; + } else { + return 'Icon_circle_right.svg'; + } + } + + String _getSendIcon() { + if (isEditEnabled) { + return 'Icon_circle_up.svg'; + } else if (isCommandEnabled) { + return 'Icon_search.svg'; + } else { + return 'Icon_circle_up.svg'; + } + } +} 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 new file mode 100644 index 000000000..0aaccd0aa --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart @@ -0,0 +1,766 @@ +// ignore_for_file: prefer-trailing-comma, cascade_invocations, lines_longer_than_80_chars + +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +export 'package:flutter/services.dart' + show + TextInputType, + TextInputAction, + TextCapitalization, + SmartQuotesType, + SmartDashesType; + +/// A widget the wraps the [TextField] and adds some StreamChat specifics. +class StreamMessageTextField extends StatefulWidget { + /// Creates a Material Design text field. + /// + /// If [decoration] is non-null (which is the default), the text field + /// requires one of its ancestors to be a [Material] widget. + /// + /// To remove the decoration entirely (including the extra padding introduced + /// by the decoration to save space for the labels), set the [decoration] to + /// null. + /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is one, meaning this is a single-line + /// text field. [maxLines] must not be zero. + /// + /// The [maxLength] property is set to null by default, which means the + /// number of characters allowed in the text field is not restricted. If + /// [maxLength] is set a character counter will be displayed below the + /// field showing how many characters have been entered. If the value is + /// set to a positive integer it will also display the maximum allowed + /// number of characters to be entered. If the value is set to + /// [TextField.noMaxLength] then only the current length is displayed. + /// + /// After [maxLength] characters have been input, additional input + /// is ignored, unless [maxLengthEnforcement] is set to + /// [MaxLengthEnforcement.none]. + /// The text field enforces the length with a + /// [LengthLimitingTextInputFormatter], + /// which is evaluated after the supplied [inputFormatters], if any. + /// The [maxLength] value must be either null or greater than zero. + /// + /// The text cursor is not shown if [showCursor] is false or if [showCursor] + /// is null (the default) and [readOnly] is true. + /// + /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow + /// changing the shape of the selection highlighting. These properties default + /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and + /// must not be null. + /// + /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect], + /// [scrollPadding], [maxLines], [maxLength], + /// [selectionHeightStyle], [selectionWidthStyle], [enableSuggestions], and + /// [enableIMEPersonalizedLearning] arguments must not be null. + /// + /// See also: + /// + /// * [maxLength], which discusses the precise meaning of "number of + /// characters" and how it may differ from the intuitive meaning. + const StreamMessageTextField({ + Key? key, + this.controller, + this.focusNode, + this.decoration = const InputDecoration(), + TextInputType? keyboardType, + this.textInputAction, + this.textCapitalization = TextCapitalization.none, + this.style, + this.strutStyle, + this.textAlign = TextAlign.start, + this.textAlignVertical, + this.textDirection, + this.readOnly = false, + ToolbarOptions? toolbarOptions, + this.showCursor, + this.autofocus = false, + this.obscuringCharacter = '•', + this.obscureText = false, + this.autocorrect = true, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + this.enableSuggestions = true, + this.maxLines = 1, + this.minLines, + this.expands = false, + this.maxLength, + @Deprecated( + 'Use maxLengthEnforcement parameter which provides more specific ' + 'behavior related to the maxLength limit. ' + 'This feature was deprecated after v1.25.0-5.0.pre.', + ) + this.maxLengthEnforced = true, + this.maxLengthEnforcement, + this.onChanged, + this.onEditingComplete, + this.onSubmitted, + this.onAppPrivateCommand, + this.inputFormatters, + this.enabled, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.cursorColor, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.keyboardAppearance, + this.scrollPadding = const EdgeInsets.all(20), + this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection = true, + this.selectionControls, + this.onTap, + this.mouseCursor, + this.buildCounter, + this.scrollController, + this.scrollPhysics, + this.autofillHints, + this.restorationId, + this.enableIMEPersonalizedLearning = true, + }) : assert(obscuringCharacter.length == 1, + '`obscuringCharacter.length` must be 1'), + smartDashesType = smartDashesType ?? + (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + smartQuotesType = smartQuotesType ?? + (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), + assert( + maxLengthEnforced || maxLengthEnforcement == null, + 'maxLengthEnforced is deprecated, use only maxLengthEnforcement', + ), + assert(maxLines == null || maxLines > 0, + '`maxLines` needs to be left as null or bigger than 0'), + assert(minLines == null || minLines > 0, + '`minLines` needs to be left as null or bigger than 0'), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + !expands || (maxLines == null && minLines == null), + 'minLines and maxLines must be null when expands is true.', + ), + assert(!obscureText || maxLines == 1, + 'Obscured fields cannot be multiline.'), + assert( + maxLength == null || + maxLength == TextField.noMaxLength || + maxLength > 0, + '`maxLength` needs to be null or a positive integer'), + + // Assert the following instead of setting it directly to avoid + // surprising the user by silently changing the value they set. + assert( + !identical(textInputAction, TextInputAction.newline) || + maxLines == 1 || + !identical(keyboardType, TextInputType.text), + '''Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.''', + ), + keyboardType = keyboardType ?? + (maxLines == 1 ? TextInputType.text : TextInputType.multiline), + toolbarOptions = toolbarOptions ?? + (obscureText + ? const ToolbarOptions( + selectAll: true, + paste: true, + ) + : const ToolbarOptions( + copy: true, + cut: true, + selectAll: true, + paste: true, + )), + super(key: key); + + /// Controls the message being edited. + /// + /// If null, this widget will create its own [StreamMessageInputController]. + final StreamMessageInputController? controller; + + /// Defines the keyboard focus for this widget. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the keyboard focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// focusNode.addListener(() { print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode]. + /// + /// ## Keyboard + /// + /// Requesting the focus will typically cause the keyboard to be shown + /// if it's not showing already. + /// + /// On Android, the user can hide the keyboard - without changing the focus - + /// with the system back button. They can restore the keyboard's visibility + /// by tapping on a text field. The user might hide the keyboard and + /// switch to a physical keyboard, or they might just need to get it + /// out of the way for a moment, to expose something it's + /// obscuring. In this case requesting the focus again will not + /// cause the focus to change, and will not make the keyboard visible. + /// + /// This widget builds an [EditableText] and will ensure that the keyboard is + /// showing when it is tapped by calling + /// [EditableTextState.requestKeyboard()]. + final FocusNode? focusNode; + + /// The decoration to show around the text field. + /// + /// By default, draws a horizontal line under the text field but can be + /// configured to show an icon, label, hint text, and error text. + /// + /// Specify null to remove the decoration entirely (including the + /// extra padding introduced by the decoration to save space for the labels). + final InputDecoration? decoration; + + /// {@macro flutter.widgets.editableText.keyboardType} + final TextInputType keyboardType; + + /// The type of action button to use for the keyboard. + /// + /// Defaults to [TextInputAction.newline] if [keyboardType] is + /// [TextInputType.multiline] and [TextInputAction.done] otherwise. + final TextInputAction? textInputAction; + + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization textCapitalization; + + /// The style to use for the text being edited. + /// + /// This text style is also used as the base style for the [decoration]. + /// + /// If null, defaults to the `subtitle1` text style from the current [Theme]. + final TextStyle? style; + + /// {@macro flutter.widgets.editableText.strutStyle} + final StrutStyle? strutStyle; + + /// {@macro flutter.widgets.editableText.textAlign} + final TextAlign textAlign; + + /// {@macro flutter.material.InputDecorator.textAlignVertical} + final TextAlignVertical? textAlignVertical; + + /// {@macro flutter.widgets.editableText.textDirection} + final TextDirection? textDirection; + + /// {@macro flutter.widgets.editableText.autofocus} + final bool autofocus; + + /// {@macro flutter.widgets.editableText.obscuringCharacter} + final String obscuringCharacter; + + /// {@macro flutter.widgets.editableText.obscureText} + final bool obscureText; + + /// {@macro flutter.widgets.editableText.autocorrect} + final bool autocorrect; + + /// {@macro flutter.services.TextInputConfiguration.smartDashesType} + final SmartDashesType smartDashesType; + + /// {@macro flutter.services.TextInputConfiguration.smartQuotesType} + final SmartQuotesType smartQuotesType; + + /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} + final bool enableSuggestions; + + /// {@macro flutter.widgets.editableText.maxLines} + /// * [expands], which determines whether the field should fill the height of + /// its parent. + final int? maxLines; + + /// {@macro flutter.widgets.editableText.minLines} + /// * [expands], which determines whether the field should fill the height of + /// its parent. + final int? minLines; + + /// {@macro flutter.widgets.editableText.expands} + final bool expands; + + /// {@macro flutter.widgets.editableText.readOnly} + final bool readOnly; + + /// Configuration of toolbar options. + /// + /// If not set, select all and paste will default to be enabled. Copy and cut + /// will be disabled if [obscureText] is true. If [readOnly] is true, + /// paste and cut will be disabled regardless. + final ToolbarOptions toolbarOptions; + + /// {@macro flutter.widgets.editableText.showCursor} + final bool? showCursor; + + /// If [maxLength] is set to this value, only the "current input length" + /// part of the character counter is shown. + static const int noMaxLength = -1; + + /// The maximum number of characters (Unicode scalar values) to allow in the + /// text field. + /// + /// If set, a character counter will be displayed below the + /// field showing how many characters have been entered. If set to a number + /// greater than 0, it will also display the maximum number allowed. If set + /// to [TextField.noMaxLength] then only the current character count is + /// displayed. + /// + /// After [maxLength] characters have been input, additional input + /// is ignored, unless [maxLengthEnforcement] is set to + /// [MaxLengthEnforcement.none]. + /// + /// The text field enforces the length with a + /// [LengthLimitingTextInputFormatter], which is evaluated after the supplied + /// [inputFormatters], if any. + /// + /// This value must be either null, [TextField.noMaxLength], or greater than + /// 0. + /// + /// If null (the default) then there is no limit to the number of characters + /// that can be entered. If set to [TextField.noMaxLength], then no limit will + /// be enforced, but the number of characters entered will still be displayed. + /// + /// Whitespace characters (e.g. newline, space, tab) are included in the + /// character count. + /// + /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} + final int? maxLength; + + /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to + /// enforce the limit, or merely provide a character counter and warning when + /// [maxLength] is exceeded. + /// + /// If true, prevents the field from allowing more than [maxLength] + /// characters. + @Deprecated( + 'Use maxLengthEnforcement parameter which provides more specific ' + 'behavior related to the maxLength limit. ' + 'This feature was deprecated after v1.25.0-5.0.pre.', + ) + final bool maxLengthEnforced; + + /// Determines how the [maxLength] limit should be enforced. + /// + /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} + /// + /// {@macro flutter.services.textFormatter.maxLengthEnforcement} + final MaxLengthEnforcement? maxLengthEnforcement; + + /// {@macro flutter.widgets.editableText.onChanged} + /// + /// See also: + /// + /// * [inputFormatters], which are called before [onChanged] + /// runs and can validate and change ("format") the input value. + /// * [onEditingComplete], [onSubmitted]: + /// which are more specialized input change notifications. + final ValueChanged? onChanged; + + /// {@macro flutter.widgets.editableText.onEditingComplete} + final VoidCallback? onEditingComplete; + + /// {@macro flutter.widgets.editableText.onSubmitted} + /// + /// See also: + /// + /// * [TextInputAction.next] and [TextInputAction.previous], which + /// automatically shift the focus to the next/previous focusable item when + /// the user is done editing. + final ValueChanged? onSubmitted; + + /// {@macro flutter.widgets.editableText.onAppPrivateCommand} + final AppPrivateCommandCallback? onAppPrivateCommand; + + /// {@macro flutter.widgets.editableText.inputFormatters} + final List? inputFormatters; + + /// If false the text field is "disabled": it ignores taps and its + /// [decoration] is rendered in grey. + /// + /// If non-null this property overrides the [decoration]'s + /// [InputDecoration.enabled] property. + final bool? enabled; + + /// {@macro flutter.widgets.editableText.cursorWidth} + final double cursorWidth; + + /// {@macro flutter.widgets.editableText.cursorHeight} + final double? cursorHeight; + + /// {@macro flutter.widgets.editableText.cursorRadius} + final Radius? cursorRadius; + + /// The color of the cursor. + /// + /// The cursor indicates the current location of text insertion point in + /// the field. + /// + /// If this is null it will default to the ambient + /// [TextSelectionThemeData.cursorColor]. If that is null, and the + /// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS] + /// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use + /// the value of [ColorScheme.primary] of [ThemeData.colorScheme]. + final Color? cursorColor; + + /// Controls how tall the selection highlight boxes are computed to be. + /// + /// See [ui.BoxHeightStyle] for details on available styles. + final ui.BoxHeightStyle selectionHeightStyle; + + /// Controls how wide the selection highlight boxes are computed to be. + /// + /// See [ui.BoxWidthStyle] for details on available styles. + final ui.BoxWidthStyle selectionWidthStyle; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// If unset, defaults to the brightness of + /// [ThemeData.brightness]. + final Brightness? keyboardAppearance; + + /// {@macro flutter.widgets.editableText.scrollPadding} + final EdgeInsets scrollPadding; + + /// {@macro flutter.widgets.editableText.enableInteractiveSelection} + final bool enableInteractiveSelection; + + /// {@macro flutter.widgets.editableText.selectionControls} + final TextSelectionControls? selectionControls; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@macro flutter.widgets.editableText.selectionEnabled} + bool get selectionEnabled => enableInteractiveSelection; + + /// {@template flutter.material.textfield.onTap} + /// Called for each distinct tap except for every second tap of a double tap. + /// + /// The text field builds a [GestureDetector] to handle input events like tap, + /// to trigger focus requests, to move the caret, adjust the selection, etc. + /// Handling some of those events by wrapping the text field with a competing + /// GestureDetector is problematic. + /// + /// To unconditionally handle taps, without interfering with the text field's + /// internal gesture detector, provide this callback. + /// + /// If the text field is created with [enabled] false, taps will not be + /// recognized. + /// + /// To be notified when the text field gains or loses the focus, provide a + /// [focusNode] and add a listener to that. + /// + /// To listen to arbitrary pointer events without competing with the + /// text field's internal gesture detector, use a [Listener]. + /// {@endtemplate} + final GestureTapCallback? onTap; + + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [MaterialStateProperty], + /// [MaterialStateProperty.resolve] is used for the following + /// [MaterialState]s: + /// + /// * [MaterialState.error]. + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// * [MaterialState.disabled]. + /// + /// If this property is null, [MaterialStateMouseCursor.textable] will be + /// used. + /// + /// The [mouseCursor] is the only property of [TextField] that controls the + /// appearance of the mouse pointer. All other properties related to "cursor" + /// stand for the text cursor, which is usually a blinking vertical line at + /// the editing position. + final MouseCursor? mouseCursor; + + /// Callback that generates a custom [InputDecoration.counter] widget. + /// + /// See [InputCounterWidgetBuilder] for an explanation of the passed in + /// arguments. The returned widget will be placed below the line in place of + /// the default widget built when [InputDecoration.counterText] is specified. + /// + /// The returned widget will be wrapped in a [Semantics] widget for + /// accessibility, but it also needs to be accessible itself. For example, + /// if returning a Text widget, set the [Text.semanticsLabel] property. + /// + /// {@tool snippet} + /// ```dart + /// Widget counter( + /// BuildContext context, + /// { + /// required int currentLength, + /// required int? maxLength, + /// required bool isFocused, + /// } + /// ) { + /// return Text( + /// '$currentLength of $maxLength characters', + /// semanticsLabel: 'character count', + /// ); + /// } + /// ``` + /// {@end-tool} + /// + /// If buildCounter returns null, then no counter and no Semantics widget will + /// be created at all. + final InputCounterWidgetBuilder? buildCounter; + + /// {@macro flutter.widgets.editableText.scrollPhysics} + final ScrollPhysics? scrollPhysics; + + /// {@macro flutter.widgets.editableText.scrollController} + final ScrollController? scrollController; + + /// {@macro flutter.widgets.editableText.autofillHints} + /// {@macro flutter.services.AutofillConfiguration.autofillHints} + final Iterable? autofillHints; + + /// {@template flutter.material.textfield.restorationId} + /// Restoration ID to save and restore the state of the text field. + /// + /// If non-null, the text field will persist and restore its current scroll + /// offset and - if no [controller] has been provided - the content of the + /// text field. If a [controller] has been provided, it is the responsibility + /// of the owner of that controller to persist and restore it, e.g. by using + /// a [RestorableTextEditingController]. + /// + /// The state of this widget is persisted in a [RestorationBucket] claimed + /// from the surrounding [RestorationScope] using the provided restoration ID. + /// + /// See also: + /// + /// * [RestorationManager], which explains how state restoration works in + /// Flutter. + /// {@endtemplate} + final String? restorationId; + + /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} + final bool enableIMEPersonalizedLearning; + + @override + _StreamMessageTextFieldState createState() => _StreamMessageTextFieldState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('focusNode', focusNode, + defaultValue: null)); + properties + .add(DiagnosticsProperty('enabled', enabled, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'decoration', decoration, + defaultValue: const InputDecoration())); + properties.add(DiagnosticsProperty( + 'keyboardType', keyboardType, + defaultValue: TextInputType.text)); + properties.add( + DiagnosticsProperty('style', style, defaultValue: null)); + properties.add( + DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty( + 'obscuringCharacter', obscuringCharacter, + defaultValue: '•')); + properties.add(DiagnosticsProperty('obscureText', obscureText, + defaultValue: false)); + properties.add(DiagnosticsProperty('autocorrect', autocorrect, + defaultValue: true)); + properties.add(EnumProperty( + 'smartDashesType', smartDashesType, + defaultValue: + obscureText ? SmartDashesType.disabled : SmartDashesType.enabled)); + properties.add(EnumProperty( + 'smartQuotesType', smartQuotesType, + defaultValue: + obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled)); + properties.add(DiagnosticsProperty( + 'enableSuggestions', enableSuggestions, + defaultValue: true)); + properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); + properties.add(IntProperty('minLines', minLines, defaultValue: null)); + properties.add( + DiagnosticsProperty('expands', expands, defaultValue: false)); + properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); + properties.add(EnumProperty( + 'maxLengthEnforcement', maxLengthEnforcement, + defaultValue: null)); + properties.add(EnumProperty( + 'textInputAction', textInputAction, + defaultValue: null)); + properties.add(EnumProperty( + 'textCapitalization', textCapitalization, + defaultValue: TextCapitalization.none)); + properties.add(EnumProperty('textAlign', textAlign, + defaultValue: TextAlign.start)); + properties.add(DiagnosticsProperty( + 'textAlignVertical', textAlignVertical, + defaultValue: null)); + properties.add(EnumProperty('textDirection', textDirection, + defaultValue: null)); + properties + .add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); + properties + .add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); + properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, + defaultValue: null)); + properties + .add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'keyboardAppearance', keyboardAppearance, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollPadding', scrollPadding, + defaultValue: const EdgeInsets.all(20))); + properties.add(FlagProperty('selectionEnabled', + value: selectionEnabled, + defaultValue: true, + ifFalse: 'selection disabled')); + properties.add(DiagnosticsProperty( + 'selectionControls', selectionControls, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollController', scrollController, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollPhysics', scrollPhysics, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, + defaultValue: true)); + } +} + +class _StreamMessageTextFieldState extends State + with RestorationMixin { + StreamRestorableMessageInputController? _controller; + + StreamMessageInputController get _effectiveController => + widget.controller ?? _controller!.value; + + @override + void initState() { + super.initState(); + if (widget.controller == null) { + _createLocalController(); + } + } + + void _createLocalController([Message? message]) { + assert(_controller == null, ''); + _controller = StreamRestorableMessageInputController(message: message); + } + + @override + void didUpdateWidget(covariant StreamMessageTextField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller == null && oldWidget.controller != null) { + _createLocalController(oldWidget.controller!.value); + } else if (widget.controller != null && oldWidget.controller == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + } + } + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (_controller != null) { + _registerController(); + } + } + + @override + String? get restorationId => widget.restorationId; + + void _registerController() { + assert(_controller != null, ''); + registerForRestoration(_controller!, restorationId ?? 'controller'); + } + + @override + Widget build(BuildContext context) => TextField( + controller: _effectiveController.textEditingController, + onChanged: (newText) { + _effectiveController.text = newText; + }, + focusNode: widget.focusNode, + decoration: widget.decoration, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + textCapitalization: widget.textCapitalization, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + textDirection: widget.textDirection, + readOnly: widget.readOnly, + toolbarOptions: widget.toolbarOptions, + showCursor: widget.showCursor, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + maxLines: widget.maxLines, + minLines: widget.minLines, + expands: widget.expands, + maxLength: widget.maxLength, + maxLengthEnforcement: widget.maxLengthEnforcement, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + onAppPrivateCommand: widget.onAppPrivateCommand, + inputFormatters: widget.inputFormatters, + enabled: widget.enabled, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + dragStartBehavior: widget.dragStartBehavior, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + onTap: widget.onTap, + mouseCursor: widget.mouseCursor, + buildCounter: widget.buildCounter, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + autofillHints: widget.autofillHints, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + ); + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/tld.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/tld.dart new file mode 100644 index 000000000..2bfa52578 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/tld.dart @@ -0,0 +1,1553 @@ +/// Extension on String adding utilities checking TLD validity. +extension TLDString on String { + /// Returns true if the string is a valid TLD. + bool isValidTLD() => + isNotEmpty && + tlds.containsKey(this[0].toUpperCase()) && + tlds[this[0].toUpperCase()]!.contains(toUpperCase()); +} + +/// List of valid TLDs. +/// https://data.iana.org/TLD/tlds-alpha-by-domain.txt +const tlds = { + 'A': [ + 'AAA', + 'AARP', + 'ABARTH', + 'ABB', + 'ABBOTT', + 'ABBVIE', + 'ABC', + 'ABLE', + 'ABOGADO', + 'ABUDHABI', + 'AC', + 'ACADEMY', + 'ACCENTURE', + 'ACCOUNTANT', + 'ACCOUNTANTS', + 'ACO', + 'ACTOR', + 'AD', + 'ADAC', + 'ADS', + 'ADULT', + 'AE', + 'AEG', + 'AERO', + 'AETNA', + 'AF', + 'AFL', + 'AFRICA', + 'AG', + 'AGAKHAN', + 'AGENCY', + 'AI', + 'AIG', + 'AIRBUS', + 'AIRFORCE', + 'AIRTEL', + 'AKDN', + 'AL', + 'ALFAROMEO', + 'ALIBABA', + 'ALIPAY', + 'ALLFINANZ', + 'ALLSTATE', + 'ALLY', + 'ALSACE', + 'ALSTOM', + 'AM', + 'AMAZON', + 'AMERICANEXPRESS', + 'AMERICANFAMILY', + 'AMEX', + 'AMFAM', + 'AMICA', + 'AMSTERDAM', + 'ANALYTICS', + 'ANDROID', + 'ANQUAN', + 'ANZ', + 'AO', + 'AOL', + 'APARTMENTS', + 'APP', + 'APPLE', + 'AQ', + 'AQUARELLE', + 'AR', + 'ARAB', + 'ARAMCO', + 'ARCHI', + 'ARMY', + 'ARPA', + 'ART', + 'ARTE', + 'AS', + 'ASDA', + 'ASIA', + 'ASSOCIATES', + 'AT', + 'ATHLETA', + 'ATTORNEY', + 'AU', + 'AUCTION', + 'AUDI', + 'AUDIBLE', + 'AUDIO', + 'AUSPOST', + 'AUTHOR', + 'AUTO', + 'AUTOS', + 'AVIANCA', + 'AW', + 'AWS', + 'AX', + 'AXA', + 'AZ', + 'AZURE', + ], + 'B': [ + 'BA', + 'BABY', + 'BAIDU', + 'BANAMEX', + 'BANANAREPUBLIC', + 'BAND', + 'BANK', + 'BAR', + 'BARCELONA', + 'BARCLAYCARD', + 'BARCLAYS', + 'BAREFOOT', + 'BARGAINS', + 'BASEBALL', + 'BASKETBALL', + 'BAUHAUS', + 'BAYERN', + 'BB', + 'BBC', + 'BBT', + 'BBVA', + 'BCG', + 'BCN', + 'BD', + 'BE', + 'BEATS', + 'BEAUTY', + 'BEER', + 'BENTLEY', + 'BERLIN', + 'BEST', + 'BESTBUY', + 'BET', + 'BF', + 'BG', + 'BH', + 'BHARTI', + 'BI', + 'BIBLE', + 'BID', + 'BIKE', + 'BING', + 'BINGO', + 'BIO', + 'BIZ', + 'BJ', + 'BLACK', + 'BLACKFRIDAY', + 'BLOCKBUSTER', + 'BLOG', + 'BLOOMBERG', + 'BLUE', + 'BM', + 'BMS', + 'BMW', + 'BN', + 'BNPPARIBAS', + 'BO', + 'BOATS', + 'BOEHRINGER', + 'BOFA', + 'BOM', + 'BOND', + 'BOO', + 'BOOK', + 'BOOKING', + 'BOSCH', + 'BOSTIK', + 'BOSTON', + 'BOT', + 'BOUTIQUE', + 'BOX', + 'BR', + 'BRADESCO', + 'BRIDGESTONE', + 'BROADWAY', + 'BROKER', + 'BROTHER', + 'BRUSSELS', + 'BS', + 'BT', + 'BUDAPEST', + 'BUGATTI', + 'BUILD', + 'BUILDERS', + 'BUSINESS', + 'BUY', + 'BUZZ', + 'BV', + 'BW', + 'BY', + 'BZ', + 'BZH', + ], + 'C': [ + 'CA', + 'CAB', + 'CAFE', + 'CAL', + 'CALL', + 'CALVINKLEIN', + 'CAM', + 'CAMERA', + 'CAMP', + 'CANCERRESEARCH', + 'CANON', + 'CAPETOWN', + 'CAPITAL', + 'CAPITALONE', + 'CAR', + 'CARAVAN', + 'CARDS', + 'CARE', + 'CAREER', + 'CAREERS', + 'CARS', + 'CASA', + 'CASE', + 'CASH', + 'CASINO', + 'CAT', + 'CATERING', + 'CATHOLIC', + 'CBA', + 'CBN', + 'CBRE', + 'CBS', + 'CC', + 'CD', + 'CENTER', + 'CEO', + 'CERN', + 'CF', + 'CFA', + 'CFD', + 'CG', + 'CH', + 'CHANEL', + 'CHANNEL', + 'CHARITY', + 'CHASE', + 'CHAT', + 'CHEAP', + 'CHINTAI', + 'CHRISTMAS', + 'CHROME', + 'CHURCH', + 'CI', + 'CIPRIANI', + 'CIRCLE', + 'CISCO', + 'CITADEL', + 'CITI', + 'CITIC', + 'CITY', + 'CITYEATS', + 'CK', + 'CL', + 'CLAIMS', + 'CLEANING', + 'CLICK', + 'CLINIC', + 'CLINIQUE', + 'CLOTHING', + 'CLOUD', + 'CLUB', + 'CLUBMED', + 'CM', + 'CN', + 'CO', + 'COACH', + 'CODES', + 'COFFEE', + 'COLLEGE', + 'COLOGNE', + 'COM', + 'COMCAST', + 'COMMBANK', + 'COMMUNITY', + 'COMPANY', + 'COMPARE', + 'COMPUTER', + 'COMSEC', + 'CONDOS', + 'CONSTRUCTION', + 'CONSULTING', + 'CONTACT', + 'CONTRACTORS', + 'COOKING', + 'COOKINGCHANNEL', + 'COOL', + 'COOP', + 'CORSICA', + 'COUNTRY', + 'COUPON', + 'COUPONS', + 'COURSES', + 'CPA', + 'CR', + 'CREDIT', + 'CREDITCARD', + 'CREDITUNION', + 'CRICKET', + 'CROWN', + 'CRS', + 'CRUISE', + 'CRUISES', + 'CSC', + 'CU', + 'CUISINELLA', + 'CV', + 'CW', + 'CX', + 'CY', + 'CYMRU', + 'CYOU', + 'CZ', + ], + 'D': [ + 'DABUR', + 'DAD', + 'DANCE', + 'DATA', + 'DATE', + 'DATING', + 'DATSUN', + 'DAY', + 'DCLK', + 'DDS', + 'DE', + 'DEAL', + 'DEALER', + 'DEALS', + 'DEGREE', + 'DELIVERY', + 'DELL', + 'DELOITTE', + 'DELTA', + 'DEMOCRAT', + 'DENTAL', + 'DENTIST', + 'DESI', + 'DESIGN', + 'DEV', + 'DHL', + 'DIAMONDS', + 'DIET', + 'DIGITAL', + 'DIRECT', + 'DIRECTORY', + 'DISCOUNT', + 'DISCOVER', + 'DISH', + 'DIY', + 'DJ', + 'DK', + 'DM', + 'DNP', + 'DO', + 'DOCS', + 'DOCTOR', + 'DOG', + 'DOMAINS', + 'DOT', + 'DOWNLOAD', + 'DRIVE', + 'DTV', + 'DUBAI', + 'DUNLOP', + 'DUPONT', + 'DURBAN', + 'DVAG', + 'DVR', + 'DZ', + ], + 'E': [ + 'EARTH', + 'EAT', + 'EC', + 'ECO', + 'EDEKA', + 'EDU', + 'EDUCATION', + 'EE', + 'EG', + 'EMAIL', + 'EMERCK', + 'ENERGY', + 'ENGINEER', + 'ENGINEERING', + 'ENTERPRISES', + 'EPSON', + 'EQUIPMENT', + 'ER', + 'ERICSSON', + 'ERNI', + 'ES', + 'ESQ', + 'ESTATE', + 'ET', + 'ETISALAT', + 'EU', + 'EUROVISION', + 'EUS', + 'EVENTS', + 'EXCHANGE', + 'EXPERT', + 'EXPOSED', + 'EXPRESS', + 'EXTRASPACE', + ], + 'F': [ + 'FAGE', + 'FAIL', + 'FAIRWINDS', + 'FAITH', + 'FAMILY', + 'FAN', + 'FANS', + 'FARM', + 'FARMERS', + 'FASHION', + 'FAST', + 'FEDEX', + 'FEEDBACK', + 'FERRARI', + 'FERRERO', + 'FI', + 'FIAT', + 'FIDELITY', + 'FIDO', + 'FILM', + 'FINAL', + 'FINANCE', + 'FINANCIAL', + 'FIRE', + 'FIRESTONE', + 'FIRMDALE', + 'FISH', + 'FISHING', + 'FIT', + 'FITNESS', + 'FJ', + 'FK', + 'FLICKR', + 'FLIGHTS', + 'FLIR', + 'FLORIST', + 'FLOWERS', + 'FLY', + 'FM', + 'FO', + 'FOO', + 'FOOD', + 'FOODNETWORK', + 'FOOTBALL', + 'FORD', + 'FOREX', + 'FORSALE', + 'FORUM', + 'FOUNDATION', + 'FOX', + 'FR', + 'FREE', + 'FRESENIUS', + 'FRL', + 'FROGANS', + 'FRONTDOOR', + 'FRONTIER', + 'FTR', + 'FUJITSU', + 'FUN', + 'FUND', + 'FURNITURE', + 'FUTBOL', + 'FYI', + ], + 'G': [ + 'GA', + 'GAL', + 'GALLERY', + 'GALLO', + 'GALLUP', + 'GAME', + 'GAMES', + 'GAP', + 'GARDEN', + 'GAY', + 'GB', + 'GBIZ', + 'GD', + 'GDN', + 'GE', + 'GEA', + 'GENT', + 'GENTING', + 'GEORGE', + 'GF', + 'GG', + 'GGEE', + 'GH', + 'GI', + 'GIFT', + 'GIFTS', + 'GIVES', + 'GIVING', + 'GL', + 'GLASS', + 'GLE', + 'GLOBAL', + 'GLOBO', + 'GM', + 'GMAIL', + 'GMBH', + 'GMO', + 'GMX', + 'GN', + 'GODADDY', + 'GOLD', + 'GOLDPOINT', + 'GOLF', + 'GOO', + 'GOODYEAR', + 'GOOG', + 'GOOGLE', + 'GOP', + 'GOT', + 'GOV', + 'GP', + 'GQ', + 'GR', + 'GRAINGER', + 'GRAPHICS', + 'GRATIS', + 'GREEN', + 'GRIPE', + 'GROCERY', + 'GROUP', + 'GS', + 'GT', + 'GU', + 'GUARDIAN', + 'GUCCI', + 'GUGE', + 'GUIDE', + 'GUITARS', + 'GURU', + 'GW', + 'GY', + ], + 'H': [ + 'HAIR', + 'HAMBURG', + 'HANGOUT', + 'HAUS', + 'HBO', + 'HDFC', + 'HDFCBANK', + 'HEALTH', + 'HEALTHCARE', + 'HELP', + 'HELSINKI', + 'HERE', + 'HERMES', + 'HGTV', + 'HIPHOP', + 'HISAMITSU', + 'HITACHI', + 'HIV', + 'HK', + 'HKT', + 'HM', + 'HN', + 'HOCKEY', + 'HOLDINGS', + 'HOLIDAY', + 'HOMEDEPOT', + 'HOMEGOODS', + 'HOMES', + 'HOMESENSE', + 'HONDA', + 'HORSE', + 'HOSPITAL', + 'HOST', + 'HOSTING', + 'HOT', + 'HOTELES', + 'HOTELS', + 'HOTMAIL', + 'HOUSE', + 'HOW', + 'HR', + 'HSBC', + 'HT', + 'HU', + 'HUGHES', + 'HYATT', + 'HYUNDAI', + ], + 'I': [ + 'IBM', + 'ICBC', + 'ICE', + 'ICU', + 'ID', + 'IE', + 'IEEE', + 'IFM', + 'IKANO', + 'IL', + 'IM', + 'IMAMAT', + 'IMDB', + 'IMMO', + 'IMMOBILIEN', + 'IN', + 'INC', + 'INDUSTRIES', + 'INFINITI', + 'INFO', + 'ING', + 'INK', + 'INSTITUTE', + 'INSURANCE', + 'INSURE', + 'INT', + 'INTERNATIONAL', + 'INTUIT', + 'INVESTMENTS', + 'IO', + 'IPIRANGA', + 'IQ', + 'IR', + 'IRISH', + 'IS', + 'ISMAILI', + 'IST', + 'ISTANBUL', + 'IT', + 'ITAU', + 'ITV', + ], + 'J': [ + 'JAGUAR', + 'JAVA', + 'JCB', + 'JE', + 'JEEP', + 'JETZT', + 'JEWELRY', + 'JIO', + 'JLL', + 'JM', + 'JMP', + 'JNJ', + 'JO', + 'JOBS', + 'JOBURG', + 'JOT', + 'JOY', + 'JP', + 'JPMORGAN', + 'JPRS', + 'JUEGOS', + 'JUNIPER', + ], + 'K': [ + 'KAUFEN', + 'KDDI', + 'KE', + 'KERRYHOTELS', + 'KERRYLOGISTICS', + 'KERRYPROPERTIES', + 'KFH', + 'KG', + 'KH', + 'KI', + 'KIA', + 'KIM', + 'KINDER', + 'KINDLE', + 'KITCHEN', + 'KIWI', + 'KM', + 'KN', + 'KOELN', + 'KOMATSU', + 'KOSHER', + 'KP', + 'KPMG', + 'KPN', + 'KR', + 'KRD', + 'KRED', + 'KUOKGROUP', + 'KW', + 'KY', + 'KYOTO', + 'KZ', + ], + 'L': [ + 'LA', + 'LACAIXA', + 'LAMBORGHINI', + 'LAMER', + 'LANCASTER', + 'LANCIA', + 'LAND', + 'LANDROVER', + 'LANXESS', + 'LASALLE', + 'LAT', + 'LATINO', + 'LATROBE', + 'LAW', + 'LAWYER', + 'LB', + 'LC', + 'LDS', + 'LEASE', + 'LECLERC', + 'LEFRAK', + 'LEGAL', + 'LEGO', + 'LEXUS', + 'LGBT', + 'LI', + 'LIDL', + 'LIFE', + 'LIFEINSURANCE', + 'LIFESTYLE', + 'LIGHTING', + 'LIKE', + 'LILLY', + 'LIMITED', + 'LIMO', + 'LINCOLN', + 'LINDE', + 'LINK', + 'LIPSY', + 'LIVE', + 'LIVING', + 'LK', + 'LLC', + 'LLP', + 'LOAN', + 'LOANS', + 'LOCKER', + 'LOCUS', + 'LOFT', + 'LOL', + 'LONDON', + 'LOTTE', + 'LOTTO', + 'LOVE', + 'LPL', + 'LPLFINANCIAL', + 'LR', + 'LS', + 'LT', + 'LTD', + 'LTDA', + 'LU', + 'LUNDBECK', + 'LUXE', + 'LUXURY', + 'LV', + 'LY', + ], + 'M': [ + 'MA', + 'MACYS', + 'MADRID', + 'MAIF', + 'MAISON', + 'MAKEUP', + 'MAN', + 'MANAGEMENT', + 'MANGO', + 'MAP', + 'MARKET', + 'MARKETING', + 'MARKETS', + 'MARRIOTT', + 'MARSHALLS', + 'MASERATI', + 'MATTEL', + 'MBA', + 'MC', + 'MCKINSEY', + 'MD', + 'ME', + 'MED', + 'MEDIA', + 'MEET', + 'MELBOURNE', + 'MEME', + 'MEMORIAL', + 'MEN', + 'MENU', + 'MERCKMSD', + 'MG', + 'MH', + 'MIAMI', + 'MICROSOFT', + 'MIL', + 'MINI', + 'MINT', + 'MIT', + 'MITSUBISHI', + 'MK', + 'ML', + 'MLB', + 'MLS', + 'MM', + 'MMA', + 'MN', + 'MO', + 'MOBI', + 'MOBILE', + 'MODA', + 'MOE', + 'MOI', + 'MOM', + 'MONASH', + 'MONEY', + 'MONSTER', + 'MORMON', + 'MORTGAGE', + 'MOSCOW', + 'MOTO', + 'MOTORCYCLES', + 'MOV', + 'MOVIE', + 'MP', + 'MQ', + 'MR', + 'MS', + 'MSD', + 'MT', + 'MTN', + 'MTR', + 'MU', + 'MUSEUM', + 'MUSIC', + 'MUTUAL', + 'MV', + 'MW', + 'MX', + 'MY', + 'MZ', + ], + 'N': [ + 'NA', + 'NAB', + 'NAGOYA', + 'NAME', + 'NATURA', + 'NAVY', + 'NBA', + 'NC', + 'NE', + 'NEC', + 'NET', + 'NETBANK', + 'NETFLIX', + 'NETWORK', + 'NEUSTAR', + 'NEW', + 'NEWS', + 'NEXT', + 'NEXTDIRECT', + 'NEXUS', + 'NF', + 'NFL', + 'NG', + 'NGO', + 'NHK', + 'NI', + 'NICO', + 'NIKE', + 'NIKON', + 'NINJA', + 'NISSAN', + 'NISSAY', + 'NL', + 'NO', + 'NOKIA', + 'NORTHWESTERNMUTUAL', + 'NORTON', + 'NOW', + 'NOWRUZ', + 'NOWTV', + 'NP', + 'NR', + 'NRA', + 'NRW', + 'NTT', + 'NU', + 'NYC', + 'NZ', + ], + 'O': [ + 'OBI', + 'OBSERVER', + 'OFFICE', + 'OKINAWA', + 'OLAYAN', + 'OLAYANGROUP', + 'OLDNAVY', + 'OLLO', + 'OM', + 'OMEGA', + 'ONE', + 'ONG', + 'ONL', + 'ONLINE', + 'OOO', + 'OPEN', + 'ORACLE', + 'ORANGE', + 'ORG', + 'ORGANIC', + 'ORIGINS', + 'OSAKA', + 'OTSUKA', + 'OTT', + 'OVH', + ], + 'P': [ + 'PA', + 'PAGE', + 'PANASONIC', + 'PARIS', + 'PARS', + 'PARTNERS', + 'PARTS', + 'PARTY', + 'PASSAGENS', + 'PAY', + 'PCCW', + 'PE', + 'PET', + 'PF', + 'PFIZER', + 'PG', + 'PH', + 'PHARMACY', + 'PHD', + 'PHILIPS', + 'PHONE', + 'PHOTO', + 'PHOTOGRAPHY', + 'PHOTOS', + 'PHYSIO', + 'PICS', + 'PICTET', + 'PICTURES', + 'PID', + 'PIN', + 'PING', + 'PINK', + 'PIONEER', + 'PIZZA', + 'PK', + 'PL', + 'PLACE', + 'PLAY', + 'PLAYSTATION', + 'PLUMBING', + 'PLUS', + 'PM', + 'PN', + 'PNC', + 'POHL', + 'POKER', + 'POLITIE', + 'PORN', + 'POST', + 'PR', + 'PRAMERICA', + 'PRAXI', + 'PRESS', + 'PRIME', + 'PRO', + 'PROD', + 'PRODUCTIONS', + 'PROF', + 'PROGRESSIVE', + 'PROMO', + 'PROPERTIES', + 'PROPERTY', + 'PROTECTION', + 'PRU', + 'PRUDENTIAL', + 'PS', + 'PT', + 'PUB', + 'PW', + 'PWC', + 'PY', + ], + 'Q': [ + 'QA', + 'QPON', + 'QUEBEC', + 'QUEST', + ], + 'R': [ + 'RACING', + 'RADIO', + 'RE', + 'READ', + 'REALESTATE', + 'REALTOR', + 'REALTY', + 'RECIPES', + 'RED', + 'REDSTONE', + 'REDUMBRELLA', + 'REHAB', + 'REISE', + 'REISEN', + 'REIT', + 'RELIANCE', + 'REN', + 'RENT', + 'RENTALS', + 'REPAIR', + 'REPORT', + 'REPUBLICAN', + 'REST', + 'RESTAURANT', + 'REVIEW', + 'REVIEWS', + 'REXROTH', + 'RICH', + 'RICHARDLI', + 'RICOH', + 'RIL', + 'RIO', + 'RIP', + 'RO', + 'ROCHER', + 'ROCKS', + 'RODEO', + 'ROGERS', + 'ROOM', + 'RS', + 'RSVP', + 'RU', + 'RUGBY', + 'RUHR', + 'RUN', + 'RW', + 'RWE', + 'RYUKYU', + ], + 'S': [ + 'SA', + 'SAARLAND', + 'SAFE', + 'SAFETY', + 'SAKURA', + 'SALE', + 'SALON', + 'SAMSCLUB', + 'SAMSUNG', + 'SANDVIK', + 'SANDVIKCOROMANT', + 'SANOFI', + 'SAP', + 'SARL', + 'SAS', + 'SAVE', + 'SAXO', + 'SB', + 'SBI', + 'SBS', + 'SC', + 'SCA', + 'SCB', + 'SCHAEFFLER', + 'SCHMIDT', + 'SCHOLARSHIPS', + 'SCHOOL', + 'SCHULE', + 'SCHWARZ', + 'SCIENCE', + 'SCOT', + 'SD', + 'SE', + 'SEARCH', + 'SEAT', + 'SECURE', + 'SECURITY', + 'SEEK', + 'SELECT', + 'SENER', + 'SERVICES', + 'SES', + 'SEVEN', + 'SEW', + 'SEX', + 'SEXY', + 'SFR', + 'SG', + 'SH', + 'SHANGRILA', + 'SHARP', + 'SHAW', + 'SHELL', + 'SHIA', + 'SHIKSHA', + 'SHOES', + 'SHOP', + 'SHOPPING', + 'SHOUJI', + 'SHOW', + 'SHOWTIME', + 'SI', + 'SILK', + 'SINA', + 'SINGLES', + 'SITE', + 'SJ', + 'SK', + 'SKI', + 'SKIN', + 'SKY', + 'SKYPE', + 'SL', + 'SLING', + 'SM', + 'SMART', + 'SMILE', + 'SN', + 'SNCF', + 'SO', + 'SOCCER', + 'SOCIAL', + 'SOFTBANK', + 'SOFTWARE', + 'SOHU', + 'SOLAR', + 'SOLUTIONS', + 'SONG', + 'SONY', + 'SOY', + 'SPA', + 'SPACE', + 'SPORT', + 'SPOT', + 'SR', + 'SRL', + 'SS', + 'ST', + 'STADA', + 'STAPLES', + 'STAR', + 'STATEBANK', + 'STATEFARM', + 'STC', + 'STCGROUP', + 'STOCKHOLM', + 'STORAGE', + 'STORE', + 'STREAM', + 'STUDIO', + 'STUDY', + 'STYLE', + 'SU', + 'SUCKS', + 'SUPPLIES', + 'SUPPLY', + 'SUPPORT', + 'SURF', + 'SURGERY', + 'SUZUKI', + 'SV', + 'SWATCH', + 'SWISS', + 'SX', + 'SY', + 'SYDNEY', + 'SYSTEMS', + 'SZ', + ], + 'T': [ + 'TAB', + 'TAIPEI', + 'TALK', + 'TAOBAO', + 'TARGET', + 'TATAMOTORS', + 'TATAR', + 'TATTOO', + 'TAX', + 'TAXI', + 'TC', + 'TCI', + 'TD', + 'TDK', + 'TEAM', + 'TECH', + 'TECHNOLOGY', + 'TEL', + 'TEMASEK', + 'TENNIS', + 'TEVA', + 'TF', + 'TG', + 'TH', + 'THD', + 'THEATER', + 'THEATRE', + 'TIAA', + 'TICKETS', + 'TIENDA', + 'TIFFANY', + 'TIPS', + 'TIRES', + 'TIROL', + 'TJ', + 'TJMAXX', + 'TJX', + 'TK', + 'TKMAXX', + 'TL', + 'TM', + 'TMALL', + 'TN', + 'TO', + 'TODAY', + 'TOKYO', + 'TOOLS', + 'TOP', + 'TORAY', + 'TOSHIBA', + 'TOTAL', + 'TOURS', + 'TOWN', + 'TOYOTA', + 'TOYS', + 'TR', + 'TRADE', + 'TRADING', + 'TRAINING', + 'TRAVEL', + 'TRAVELCHANNEL', + 'TRAVELERS', + 'TRAVELERSINSURANCE', + 'TRUST', + 'TRV', + 'TT', + 'TUBE', + 'TUI', + 'TUNES', + 'TUSHU', + 'TV', + 'TVS', + 'TW', + 'TZ', + ], + 'U': [ + 'UA', + 'UBANK', + 'UBS', + 'UG', + 'UK', + 'UNICOM', + 'UNIVERSITY', + 'UNO', + 'UOL', + 'UPS', + 'US', + 'UY', + 'UZ', + ], + 'V': [ + 'VA', + 'VACATIONS', + 'VANA', + 'VANGUARD', + 'VC', + 'VE', + 'VEGAS', + 'VENTURES', + 'VERISIGN', + 'VERSICHERUNG', + 'VET', + 'VG', + 'VI', + 'VIAJES', + 'VIDEO', + 'VIG', + 'VIKING', + 'VILLAS', + 'VIN', + 'VIP', + 'VIRGIN', + 'VISA', + 'VISION', + 'VIVA', + 'VIVO', + 'VLAANDEREN', + 'VN', + 'VODKA', + 'VOLKSWAGEN', + 'VOLVO', + 'VOTE', + 'VOTING', + 'VOTO', + 'VOYAGE', + 'VU', + 'VUELOS', + ], + 'W': [ + 'WALES', + 'WALMART', + 'WALTER', + 'WANG', + 'WANGGOU', + 'WATCH', + 'WATCHES', + 'WEATHER', + 'WEATHERCHANNEL', + 'WEBCAM', + 'WEBER', + 'WEBSITE', + 'WED', + 'WEDDING', + 'WEIBO', + 'WEIR', + 'WF', + 'WHOSWHO', + 'WIEN', + 'WIKI', + 'WILLIAMHILL', + 'WIN', + 'WINDOWS', + 'WINE', + 'WINNERS', + 'WME', + 'WOLTERSKLUWER', + 'WOODSIDE', + 'WORK', + 'WORKS', + 'WORLD', + 'WOW', + 'WS', + 'WTC', + 'WTF', + ], + 'X': [ + 'XBOX', + 'XEROX', + 'XFINITY', + 'XIHUAN', + 'XIN', + 'XN--11B4C3D', + 'XN--1CK2E1B', + 'XN--1QQW23A', + 'XN--2SCRJ9C', + 'XN--30RR7Y', + 'XN--3BST00M', + 'XN--3DS443G', + 'XN--3E0B707E', + 'XN--3HCRJ9C', + 'XN--3PXU8K', + 'XN--42C2D9A', + 'XN--45BR5CYL', + 'XN--45BRJ9C', + 'XN--45Q11C', + 'XN--4DBRK0CE', + 'XN--4GBRIM', + 'XN--54B7FTA0CC', + 'XN--55QW42G', + 'XN--55QX5D', + 'XN--5SU34J936BGSG', + 'XN--5TZM5G', + 'XN--6FRZ82G', + 'XN--6QQ986B3XL', + 'XN--80ADXHKS', + 'XN--80AO21A', + 'XN--80AQECDR1A', + 'XN--80ASEHDB', + 'XN--80ASWG', + 'XN--8Y0A063A', + 'XN--90A3AC', + 'XN--90AE', + 'XN--90AIS', + 'XN--9DBQ2A', + 'XN--9ET52U', + 'XN--9KRT00A', + 'XN--B4W605FERD', + 'XN--BCK1B9A5DRE4C', + 'XN--C1AVG', + 'XN--C2BR7G', + 'XN--CCK2B3B', + 'XN--CCKWCXETD', + 'XN--CG4BKI', + 'XN--CLCHC0EA0B2G2A9GCD', + 'XN--CZR694B', + 'XN--CZRS0T', + 'XN--CZRU2D', + 'XN--D1ACJ3B', + 'XN--D1ALF', + 'XN--E1A4C', + 'XN--ECKVDTC9D', + 'XN--EFVY88H', + 'XN--FCT429K', + 'XN--FHBEI', + 'XN--FIQ228C5HS', + 'XN--FIQ64B', + 'XN--FIQS8S', + 'XN--FIQZ9S', + 'XN--FJQ720A', + 'XN--FLW351E', + 'XN--FPCRJ9C3D', + 'XN--FZC2C9E2C', + 'XN--FZYS8D69UVGM', + 'XN--G2XX48C', + 'XN--GCKR3F0F', + 'XN--GECRJ9C', + 'XN--GK3AT1E', + 'XN--H2BREG3EVE', + 'XN--H2BRJ9C', + 'XN--H2BRJ9C8C', + 'XN--HXT814E', + 'XN--I1B6B1A6A2E', + 'XN--IMR513N', + 'XN--IO0A7I', + 'XN--J1AEF', + 'XN--J1AMH', + 'XN--J6W193G', + 'XN--JLQ480N2RG', + 'XN--JLQ61U9W7B', + 'XN--JVR189M', + 'XN--KCRX77D1X4A', + 'XN--KPRW13D', + 'XN--KPRY57D', + 'XN--KPUT3I', + 'XN--L1ACC', + 'XN--LGBBAT1AD8J', + 'XN--MGB9AWBF', + 'XN--MGBA3A3EJT', + 'XN--MGBA3A4F16A', + 'XN--MGBA7C0BBN0A', + 'XN--MGBAAKC7DVF', + 'XN--MGBAAM7A8H', + 'XN--MGBAB2BD', + 'XN--MGBAH1A3HJKRD', + 'XN--MGBAI9AZGQP6J', + 'XN--MGBAYH7GPA', + 'XN--MGBBH1A', + 'XN--MGBBH1A71E', + 'XN--MGBC0A9AZCG', + 'XN--MGBCA7DZDO', + 'XN--MGBCPQ6GPA1A', + 'XN--MGBERP4A5D4AR', + 'XN--MGBGU82A', + 'XN--MGBI4ECEXP', + 'XN--MGBPL2FH', + 'XN--MGBT3DHD', + 'XN--MGBTX2B', + 'XN--MGBX4CD0AB', + 'XN--MIX891F', + 'XN--MK1BU44C', + 'XN--MXTQ1M', + 'XN--NGBC5AZD', + 'XN--NGBE9E0A', + 'XN--NGBRX', + 'XN--NODE', + 'XN--NQV7F', + 'XN--NQV7FS00EMA', + 'XN--NYQY26A', + 'XN--O3CW4H', + 'XN--OGBPF8FL', + 'XN--OTU796D', + 'XN--P1ACF', + 'XN--P1AI', + 'XN--PGBS0DH', + 'XN--PSSY2U', + 'XN--Q7CE6A', + 'XN--Q9JYB4C', + 'XN--QCKA1PMC', + 'XN--QXA6A', + 'XN--QXAM', + 'XN--RHQV96G', + 'XN--ROVU88B', + 'XN--RVC1E0AM3E', + 'XN--S9BRJ9C', + 'XN--SES554G', + 'XN--T60B56A', + 'XN--TCKWE', + 'XN--TIQ49XQYJ', + 'XN--UNUP4Y', + 'XN--VERMGENSBERATER-CTB', + 'XN--VERMGENSBERATUNG-PWB', + 'XN--VHQUV', + 'XN--VUQ861B', + 'XN--W4R85EL8FHU5DNRA', + 'XN--W4RS40L', + 'XN--WGBH1C', + 'XN--WGBL6A', + 'XN--XHQ521B', + 'XN--XKC2AL3HYE2A', + 'XN--XKC2DL3A5EE0H', + 'XN--Y9A3AQ', + 'XN--YFRO4I67O', + 'XN--YGBI2AMMX', + 'XN--ZFR164B', + 'XXX', + 'XYZ', + ], + 'Y': [ + 'YACHTS', + 'YAHOO', + 'YAMAXUN', + 'YANDEX', + 'YE', + 'YODOBASHI', + 'YOGA', + 'YOKOHAMA', + 'YOU', + 'YOUTUBE', + 'YT', + 'YUN', + ], + 'Z': [ + 'ZA', + 'ZAPPOS', + 'ZARA', + 'ZERO', + 'ZIP', + 'ZM', + 'ZONE', + 'ZUERICH', + 'ZW', + ], +}; 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 000000000..b8e2cf24d --- /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 000000000..d1969fa40 --- /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/scroll_view/channel_scroll_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart new file mode 100644 index 000000000..8a180fca8 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart @@ -0,0 +1,361 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that displays a channel preview. +/// +/// This widget is intended to be used as a Tile in [StreamChannelListView] +/// +/// It shows the last message of the channel, the last message time, the unread +/// message count, the typing indicator, the sending indicator and the channel +/// avatar. +/// +/// See also: +/// * [StreamChannelAvatar] +/// * [StreamChannelName] +class StreamChannelListTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTile] widget. + StreamChannelListTile({ + Key? key, + required this.channel, + this.leading, + this.title, + this.subtitle, + this.trailing, + this.onTap, + this.onLongPress, + this.tileColor, + this.visualDensity = VisualDensity.compact, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + this.unreadIndicatorBuilder, + this.sendingIndicatorBuilder, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to display. + final Channel channel; + + /// A widget to display before the title. + final Widget? leading; + + /// The primary content of the list tile. + final Widget? title; + + /// Additional content displayed below the title. + final Widget? subtitle; + + /// A widget to display at the end of tile. + final Widget? trailing; + + /// Called when the user taps this list tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this list tile. + final GestureLongPressCallback? onLongPress; + + /// {@template flutter.material.ListTile.tileColor} + /// Defines the background color of `ListTile`. + /// + /// When the value is null, + /// the `tileColor` is set to [ListTileTheme.tileColor] + /// if it's not null and to [Colors.transparent] if it's null. + /// {@endtemplate} + final Color? tileColor; + + /// 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; + + /// The widget builder for the unread indicator. + final WidgetBuilder? unreadIndicatorBuilder; + + /// The widget builder for the sending indicator. + /// + /// `Message` is the last message in the channel, Use it to determine the + /// status using [Message.status]. + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamChannelListTile copyWith({ + Key? key, + Channel? channel, + Widget? leading, + Widget? title, + Widget? subtitle, + VoidCallback? onTap, + VoidCallback? onLongPress, + VisualDensity? visualDensity, + EdgeInsetsGeometry? contentPadding, + }) => + StreamChannelListTile( + key: key ?? this.key, + channel: channel ?? this.channel, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); + + @override + Widget build(BuildContext context) { + final channelState = channel.state!; + final currentUser = channel.client.state.currentUser!; + + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + + final leading = this.leading ?? + StreamChannelAvatar( + channel: channel, + ); + + final title = this.title ?? + StreamChannelName( + channel: channel, + textStyle: channelPreviewTheme.titleStyle, + ); + + final subtitle = this.subtitle ?? + ChannelListTileSubtitle( + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ); + + final trailing = this.trailing ?? + ChannelLastMessageDate( + channel: channel, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ); + + return BetterStreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, isMuted) => AnimatedOpacity( + opacity: isMuted ? 0.5 : 1, + duration: const Duration(milliseconds: 300), + child: ListTile( + onTap: onTap, + onLongPress: onLongPress, + visualDensity: visualDensity, + contentPadding: contentPadding, + leading: leading, + tileColor: tileColor, + title: Row( + children: [ + Expanded(child: title), + BetterStreamBuilder>( + stream: channelState.membersStream, + initialData: channelState.members, + comparator: const ListEquality().equals, + builder: (context, members) { + if (members.isEmpty || + !members.any((it) => it.user!.id == currentUser.id)) { + return const Offstage(); + } + return unreadIndicatorBuilder?.call(context) ?? + StreamUnreadIndicator(cid: channel.cid); + }, + ), + ], + ), + subtitle: Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: subtitle, + ), + ), + BetterStreamBuilder>( + stream: channelState.messagesStream, + initialData: channelState.messages, + comparator: const ListEquality().equals, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null || + (lastMessage.user?.id != currentUser.id)) { + return const Offstage(); + } + + return Padding( + padding: const EdgeInsets.only(right: 4), + child: + sendingIndicatorBuilder?.call(context, lastMessage) ?? + StreamSendingIndicator( + message: lastMessage, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channelState + .currentUserRead!.lastRead + .isAfter(lastMessage.createdAt), + ), + ); + }, + ), + trailing, + ], + ), + ), + ), + ); + } +} + +/// A widget that displays the channel last message date. +class ChannelLastMessageDate extends StatelessWidget { + /// Creates a new instance of the [ChannelLastMessageDate] widget. + ChannelLastMessageDate({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to display the last message date for. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.lastMessageAtStream, + initialData: channel.lastMessageAt, + builder: (context, data) { + final lastMessageAt = data.toLocal(); + + String stringDate; + final now = DateTime.now(); + + final startOfDay = DateTime(now.year, now.month, now.day); + + if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.millisecondsSinceEpoch) { + stringDate = Jiffy(lastMessageAt.toLocal()).jm; + } else if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch) { + stringDate = context.translations.yesterdayLabel; + } else if (startOfDay.difference(lastMessageAt).inDays < 7) { + stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).yMd; + } + + return Text( + stringDate, + style: textStyle, + ); + }, + ); +} + +/// A widget that displays the subtitle for [StreamChannelListTile]. +class ChannelListTileSubtitle extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTileSubtitle] widget. + ChannelListTileSubtitle({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to create the subtitle from. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + if (channel.isMuted) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + StreamSvgIcon.mute(size: 16), + Text( + ' ${context.translations.channelIsMutedText}', + style: textStyle, + ), + ], + ); + } + return StreamTypingIndicator( + channel: channel, + style: textStyle, + alternativeWidget: ChannelLastMessageText( + channel: channel, + textStyle: textStyle, + ), + ); + } +} + +/// A widget that displays the last message of a channel. +class ChannelLastMessageText extends StatelessWidget { + /// Creates a new instance of [ChannelLastMessageText] widget. + ChannelLastMessageText({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to display the last message of. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null) return const Offstage(); + + return StreamMessagePreviewText( + message: lastMessage, + textStyle: textStyle, + language: channel.client.state.currentUser?.language, + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart new file mode 100644 index 000000000..2a5118358 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart @@ -0,0 +1,426 @@ +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_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]. +Widget defaultChannelListViewSeparatorBuilder( + BuildContext context, + List items, + int index, +) => + const StreamChannelListSeparator(); + +/// Signature for the item builder that creates the children of the +/// [StreamChannelListView]. +typedef StreamChannelListViewIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// A [ListView] that shows a list of [Channel]s, +/// it uses [StreamChannelListTile] as a default item. +/// +/// This is the new version of [ChannelListView] that uses +/// [StreamChannelListController]. +/// +/// Example: +/// +/// ```dart +/// StreamChannelListView( +/// controller: controller, +/// onChannelTap: (channel) { +/// // Handle channel tap event +/// }, +/// onChannelLongPress: (channel) { +/// // Handle channel long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamChannelListTile] +/// * [StreamChannelListController] +class StreamChannelListView extends StatelessWidget { + /// Creates a new instance of [StreamChannelListView]. + const StreamChannelListView({ + Key? key, + required this.controller, + this.itemBuilder, + this.separatorBuilder = defaultChannelListViewSeparatorBuilder, + this.emptyBuilder, + 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.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [StreamChannelListController] used to control the list of channels. + final StreamChannelListController controller; + + /// A builder that is called to build items in the [ListView]. + 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. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + /// + /// If not provided, [StreamChannelListErrorWidget] will be used. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the user taps this list tile. + final void Function(Channel)? onChannelTap; + + /// Called when the user long-presses on this list 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; + + /// 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; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, 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 [scrollController] is null. + final bool? primary; + + /// {@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; + + /// {@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; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@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) => PagedValueListView( + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, channels, index) { + final channel = channels[index]; + final onTap = onChannelTap; + final onLongPress = onChannelLongPress; + + final streamChannelListTile = StreamChannelListTile( + channel: channel, + onTap: onTap == null ? null : () => onTap(channel), + onLongPress: + onLongPress == null ? null : () => onLongPress(channel), + ); + + return itemBuilder?.call( + context, + channels, + index, + streamChannelListTile, + ) ?? + streamChannelListTile; + }, + 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.list( + onTap: controller.retry, + error: Text(context.translations.loadingChannelsError), + ), + 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, + ), + ), + ); +} + +/// A widget that is used to display a separator between +/// [StreamChannelListTile] items. +class StreamChannelListSeparator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListSeparator]. + const StreamChannelListSeparator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final effect = StreamChatTheme.of(context).colorTheme.borderBottom; + return Container( + height: 1, + color: effect.color!.withOpacity(effect.alpha ?? 1.0), + ); + } +} + +/// A widget that is used to display an error screen +/// when [StreamChannelListController] fails to load initial channels. +class StreamChannelListErrorWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListErrorWidget] widget. + const StreamChannelListErrorWidget({ + 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), + ), + ], + ); +} 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 000000000..488148b56 --- /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/scroll_view/message_search_scroll_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 new file mode 100644 index 000000000..c303add05 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that displays a message search item. +/// +/// This widget is intended to be used as a +/// Tile in [StreamMessageSearchListView]. +/// +/// It displays the message's text, channel, sender, and timestamp. +/// +/// See also: +/// * [StreamMessageSearchListView] +/// * [StreamUserAvatar] +class StreamMessageSearchListTile extends StatelessWidget { + /// Creates a new instance of [StreamMessageSearchListTile]. + const StreamMessageSearchListTile({ + Key? key, + required this.messageResponse, + this.leading, + this.title, + this.subtitle, + this.trailing, + this.onTap, + this.onLongPress, + this.tileColor, + this.visualDensity = VisualDensity.compact, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + }) : super(key: key); + + /// The message response to display. + final GetMessageResponse messageResponse; + + /// A widget to display before the title. + final Widget? leading; + + /// The primary content of the list tile. + final Widget? title; + + /// Additional content displayed below the title. + final Widget? subtitle; + + /// A widget to display at the end of tile. + final Widget? trailing; + + /// Called when the user taps this list tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this list tile. + final GestureLongPressCallback? onLongPress; + + /// {@template flutter.material.ListTile.tileColor} + /// Defines the background color of `ListTile`. + /// + /// When the value is null, + /// the `tileColor` is set to [ListTileTheme.tileColor] + /// if it's not null and to [Colors.transparent] if it's null. + /// {@endtemplate} + final Color? tileColor; + + /// 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; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamMessageSearchListTile copyWith({ + Key? key, + GetMessageResponse? messageResponse, + Widget? leading, + Widget? title, + Widget? subtitle, + Widget? trailing, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + Color? tileColor, + VisualDensity? visualDensity, + EdgeInsetsGeometry? contentPadding, + }) => + StreamMessageSearchListTile( + key: key ?? this.key, + messageResponse: messageResponse ?? this.messageResponse, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + trailing: trailing ?? this.trailing, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + tileColor: tileColor ?? this.tileColor, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); + + @override + Widget build(BuildContext context) { + final message = messageResponse.message; + final user = message.user!; + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + + final leading = this.leading ?? + StreamUserAvatar( + user: user, + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ); + + final title = this.title ?? + MessageSearchListTileTitle( + messageResponse: messageResponse, + textStyle: channelPreviewTheme.titleStyle, + ); + + final subtitle = this.subtitle ?? + Row( + children: [ + Expanded( + child: StreamMessagePreviewText( + message: message, + textStyle: channelPreviewTheme.subtitleStyle, + ), + ), + const SizedBox(width: 16), + MessageSearchTileMessageDate( + message: message, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ), + ], + ); + + return ListTile( + onTap: onTap, + onLongPress: onLongPress, + visualDensity: visualDensity, + contentPadding: contentPadding, + tileColor: tileColor, + leading: leading, + trailing: trailing, + title: title, + subtitle: subtitle, + ); + } +} + +/// 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 + Widget build(BuildContext context) { + final user = messageResponse.message.user!; + final channel = messageResponse.channel; + final channelName = channel?.extraData['name']; + + return Row( + children: [ + Text( + user.id == StreamChat.of(context).currentUser?.id + ? context.translations.youText + : user.name, + style: textStyle, + ), + if (channelName != null) ...[ + Text( + ' ${context.translations.inText} ', + style: textStyle?.copyWith( + fontWeight: FontWeight.normal, + ), + ), + Text( + channelName as String, + style: textStyle, + ), + ], + ], + ); + } +} + +/// A widget which shows formatted created date of the passed [message]. +class MessageSearchTileMessageDate extends StatelessWidget { + /// Creates a new instance of [MessageSearchTileMessageDate]. + const MessageSearchTileMessageDate({ + Key? key, + required this.message, + this.textStyle, + }) : super(key: key); + + /// The searched message response. + final Message message; + + /// The text style to use for the date. + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final createdAt = message.createdAt; + String stringDate; + final now = DateTime.now(); + if (now.year != createdAt.year || + now.month != createdAt.month || + now.day != createdAt.day) { + stringDate = Jiffy(createdAt.toLocal()).yMd; + } else { + stringDate = Jiffy(createdAt.toLocal()).jm; + } + + return Text( + stringDate, + style: textStyle, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_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 new file mode 100644 index 000000000..9c3f165d1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart @@ -0,0 +1,386 @@ +// 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]. +Widget defaultMessageSearchListViewSeparatorBuilder( + BuildContext context, + List responses, + int index, +) => + const StreamMessageSearchListSeparator(); + +/// Signature for the item builder that creates the children of the +/// [StreamMessageSearchListView]. +typedef StreamMessageSearchListViewIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// A [ListView] that shows a list of [GetMessageResponse]s, +/// it uses [StreamMessageSearchListTile] as a default item. +/// +/// This is the new version of [MessageSearchListView] that uses +/// [StreamMessageSearchListController]. +/// +/// Example: +/// +/// ```dart +/// StreamMessageSearchListView( +/// controller: controller, +/// onMessageTap: (user) { +/// // Handle user tap event +/// }, +/// onMessageLongPress: (user) { +/// // Handle user long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamMessageSearchListTile] +/// * [StreamMessageSearchListController] +class StreamMessageSearchListView extends StatelessWidget { + /// Creates a new instance of [StreamMessageSearchListView]. + const StreamMessageSearchListView({ + Key? key, + required this.controller, + this.itemBuilder, + this.separatorBuilder = defaultMessageSearchListViewSeparatorBuilder, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, + this.onMessageTap, + this.onMessageLongPress, + 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.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [StreamUserListController] used to control the list of + /// searched messages. + final StreamMessageSearchListController controller; + + /// A builder that is called to build items in the [ListView]. + final StreamMessageSearchListViewIndexedWidgetBuilder? 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. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the user taps this list tile. + final void Function(GetMessageResponse)? onMessageTap; + + /// Called when the user long-presses on this list tile. + final void Function(GetMessageResponse)? onMessageLongPress; + + /// 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; + + /// 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; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, 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 [scrollController] is null. + final bool? primary; + + /// {@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; + + /// {@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; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@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) => + PagedValueListView( + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, messageResponses, index) { + final messageResponse = messageResponses[index]; + final onTap = onMessageTap; + final onLongPress = onMessageLongPress; + + final streamMessageSearchListTile = StreamMessageSearchListTile( + messageResponse: messageResponse, + onTap: onTap == null ? null : () => onTap(messageResponse), + onLongPress: + onLongPress == null ? null : () => onLongPress(messageResponse), + ); + + return itemBuilder?.call( + context, + messageResponses, + index, + streamMessageSearchListTile, + ) ?? + 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, + ), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.list( + 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( + errorTitle: Text(context.translations.loadingMessagesError), + onRetryPressed: controller.refresh, + ), + ), + ); +} + +/// A widget that is used to display a separator between +/// [StreamMessageSearchListTile] items. +class StreamMessageSearchListSeparator extends StatelessWidget { + /// Creates a new instance of [StreamMessageSearchListSeparator]. + const StreamMessageSearchListSeparator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final effect = StreamChatTheme.of(context).colorTheme.borderBottom; + return Container( + height: 1, + color: effect.color!.withOpacity(effect.alpha ?? 1.0), + ); + } +} 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 000000000..531eac79b --- /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 000000000..9d6af29c5 --- /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 000000000..0305cd1f2 --- /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 000000000..03575b1f9 --- /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 000000000..93bf8d064 --- /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 000000000..ae47b152f --- /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 000000000..6d904086a --- /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 000000000..08cc4a50a --- /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/scroll_view/user_scroll_view/stream_user_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart new file mode 100644 index 000000000..c88862fa0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.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/user_avatar.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' + show User; + +/// A widget that displays a user. +/// +/// This widget is intended to be used as a Tile in [StreamUserListView] +/// +/// It shows the user's avatar, name and last message. +/// +/// See also: +/// * [StreamUserListView] +/// * [StreamUserAvatar] +class StreamUserListTile extends StatelessWidget { + /// Creates a new instance of [StreamUserListTile]. + const StreamUserListTile({ + Key? key, + required this.user, + this.leading, + this.title, + this.subtitle, + this.selected = false, + this.selectedWidget, + this.onTap, + this.onLongPress, + this.tileColor, + this.visualDensity = VisualDensity.compact, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + }) : super(key: key); + + /// The user to display. + final User user; + + /// A widget to display before the title. + final Widget? leading; + + /// The primary content of the list tile. + final Widget? title; + + /// Additional content displayed below the title. + final Widget? subtitle; + + /// 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. + /// + /// 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 + /// selected index, and using that to set the `selected` property on the + /// corresponding [ListTile]. + /// + /// ** See code in examples/api/lib/material/list_tile/list_tile.selected.0.dart ** + /// {@end-tool} + final bool selected; + + /// Called when the user taps this list tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this list tile. + final GestureLongPressCallback? onLongPress; + + /// {@template flutter.material.ListTile.tileColor} + /// Defines the background color of `ListTile`. + /// + /// When the value is null, + /// the `tileColor` is set to [ListTileTheme.tileColor] + /// if it's not null and to [Colors.transparent] if it's null. + /// {@endtemplate} + final Color? tileColor; + + /// 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; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamUserListTile copyWith({ + Key? key, + User? user, + Widget? leading, + Widget? title, + Widget? subtitle, + Widget? selectedWidget, + bool? selected, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + Color? tileColor, + VisualDensity? visualDensity, + EdgeInsetsGeometry? contentPadding, + }) => + StreamUserListTile( + key: key ?? this.key, + user: user ?? this.user, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + selectedWidget: selectedWidget ?? this.selectedWidget, + selected: selected ?? this.selected, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + tileColor: tileColor ?? this.tileColor, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + + final leading = this.leading ?? + StreamUserAvatar( + user: user, + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ); + + final title = this.title ?? + Text( + user.name, + style: chatThemeData.textTheme.bodyBold, + ); + + final subtitle = this.subtitle ?? + UserLastActive( + user: user, + ); + + final selectedWidget = this.selectedWidget ?? + StreamSvgIcon.checkSend( + color: chatThemeData.colorTheme.accentPrimary, + ); + + return ListTile( + onTap: onTap, + onLongPress: onLongPress, + leading: leading, + trailing: selected ? selectedWidget : null, + title: title, + subtitle: subtitle, + ); + } +} + +/// A widget that displays a user's last active time. +class UserLastActive extends StatelessWidget { + /// Creates a new instance of the [UserLastActive] widget. + const UserLastActive({ + Key? key, + required this.user, + }) : super(key: key); + + /// The user whose last active time is displayed. + final User user; + + @override + Widget build(BuildContext context) { + final chatTheme = StreamChatTheme.of(context); + return Text( + user.online + ? context.translations.userOnlineText + : '${context.translations.userLastOnlineText} ' + '${Jiffy(user.lastActive).fromNow()}', + style: chatTheme.textTheme.footnote.copyWith( + color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart new file mode 100644 index 000000000..d87f8ab95 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart @@ -0,0 +1,381 @@ +// 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]. +Widget defaultUserListViewSeparatorBuilder( + BuildContext context, + List users, + int index, +) => + const StreamUserListSeparator(); + +/// Signature for the item builder that creates the children of the +/// [StreamUserListView]. +typedef StreamUserListViewIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// A [ListView] that shows a list of [User]s, +/// it uses [StreamUserListTile] as a default item. +/// +/// This is the new version of [UserListView] that uses +/// [StreamUserListController]. +/// +/// Example: +/// +/// ```dart +/// StreamUserListView( +/// controller: controller, +/// onUserTap: (user) { +/// // Handle user tap event +/// }, +/// onUserLongPress: (user) { +/// // Handle user long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamUserListTile] +/// * [StreamUserListController] +class StreamUserListView extends StatelessWidget { + /// Creates a new instance of [StreamUserListView]. + const StreamUserListView({ + Key? key, + required this.controller, + this.itemBuilder, + this.separatorBuilder = defaultUserListViewSeparatorBuilder, + this.emptyBuilder, + 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.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [StreamUserListController] used to control the list of users. + final StreamUserListController controller; + + /// A builder that is called to build items in the [ListView]. + 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. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the user taps this list tile. + final void Function(User)? onUserTap; + + /// Called when the user long-presses on this list 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; + + /// 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; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, 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 [scrollController] is null. + final bool? primary; + + /// {@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; + + /// {@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; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@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) => PagedValueListView( + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, users, index) { + final user = users[index]; + final onTap = onUserTap; + final onLongPress = onUserLongPress; + + final streamUserListTile = StreamUserListTile( + user: user, + onTap: onTap == null ? null : () => onTap(user), + onLongPress: onLongPress == null ? null : () => onLongPress(user), + ); + + return itemBuilder?.call( + context, + users, + index, + streamUserListTile, + ) ?? + 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) => + StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingUsersError), + ), + 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, + ), + ), + ); +} + +/// A widget that is used to display a separator between +/// [StreamUserListTile] items. +class StreamUserListSeparator extends StatelessWidget { + /// Creates a new instance of [StreamUserListSeparator]. + const StreamUserListSeparator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final effect = StreamChatTheme.of(context).colorTheme.borderBottom; + return Container( + height: 1, + color: effect.color!.withOpacity(effect.alpha ?? 1.0), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart new file mode 100644 index 000000000..448e39d8e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart @@ -0,0 +1,200 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/group_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) +/// +/// It shows the current [Channel] image. +/// +/// ```dart +/// class MyApp extends StatelessWidget { +/// final StreamChatClient client; +/// final Channel channel; +/// +/// MyApp(this.client, this.channel); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// debugShowCheckedModeBanner: false, +/// home: StreamChat( +/// client: client, +/// child: StreamChannel( +/// channel: channel, +/// child: Center( +/// child: StreamChannelAvatar( +/// channel: channel, +/// ), +/// ), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +/// +/// By default the widget radius size is 40x40 pixels. +/// Set the property [constraints] to set a custom dimension. +/// +/// The widget renders the ui based on the first ancestor of type +/// [StreamChatTheme]. +/// Modify it to change the widget appearance. +class StreamChannelAvatar extends StatelessWidget { + /// Instantiate a new ChannelImage + StreamChannelAvatar({ + Key? key, + required this.channel, + this.constraints, + this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor, + this.selectionThickness = 4, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// [BorderRadius] to display the widget + final BorderRadius? borderRadius; + + /// The channel to show the image of + final Channel channel; + + /// The diameter of the image + final BoxConstraints? constraints; + + /// The function called when the image is tapped + final VoidCallback? onTap; + + /// If image is selected + final bool selected; + + /// Selection color for image + final Color? selectionColor; + + /// Thickness of selection image + final double selectionThickness; + + @override + Widget build(BuildContext context) { + final client = channel.client.state; + + final chatThemeData = StreamChatTheme.of(context); + final colorTheme = chatThemeData.colorTheme; + final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; + + return BetterStreamBuilder( + stream: channel.imageStream, + initialData: channel.image, + builder: (context, channelImage) { + Widget child = ClipRRect( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + child: Container( + constraints: constraints ?? previewTheme?.constraints, + decoration: BoxDecoration(color: colorTheme.accentPrimary), + child: InkWell( + onTap: onTap, + child: CachedNetworkImage( + imageUrl: channelImage, + errorWidget: (_, __, ___) => Center( + child: Text( + channel.name?[0] ?? '', + style: TextStyle( + color: colorTheme.barsBg, + fontWeight: FontWeight.bold, + ), + ), + ), + fit: BoxFit.cover, + ), + ), + ), + ); + + if (selected) { + child = ClipRRect( + key: const Key('selectedImage'), + borderRadius: BorderRadius.circular(selectionThickness) + + (borderRadius ?? + previewTheme?.borderRadius ?? + BorderRadius.zero), + child: Container( + constraints: constraints ?? previewTheme?.constraints, + color: selectionColor ?? colorTheme.accentPrimary, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, + ), + ), + ); + } + return child; + }, + noDataBuilder: (context) { + final currentUser = client.currentUser!; + final otherMembers = channel.state!.members + .where((it) => it.userId != currentUser.id) + .toList(growable: false); + + // our own space, no other members + if (otherMembers.isEmpty) { + return BetterStreamBuilder( + stream: client.currentUserStream.map((it) => it!), + initialData: currentUser, + builder: (context, user) => StreamUserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: user, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // 1-1 Conversation + if (otherMembers.length == 1) { + final member = otherMembers.first; + return BetterStreamBuilder( + stream: channel.state!.membersStream.map( + (members) => members.firstWhere( + (it) => it.userId == member.userId, + orElse: () => member, + ), + ), + initialData: member, + builder: (context, member) => StreamUserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: member.user!, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // Group conversation + return StreamGroupAvatar( + channel: channel, + members: otherMembers, + borderRadius: borderRadius ?? previewTheme?.borderRadius, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart new file mode 100644 index 000000000..986a9cb31 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/option_list_tile.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/theme/themes.dart'; +import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// A [BottomSheet] that shows information about a [Channel]. +class StreamChannelInfoBottomSheet extends StatelessWidget { + /// Creates a new instance [StreamChannelInfoBottomSheet] widget. + StreamChannelInfoBottomSheet({ + Key? key, + required this.channel, + this.onMemberTap, + this.onViewInfoTap, + this.onLeaveChannelTap, + this.onDeleteConversationTap, + this.onCancelTap, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The [Channel] to show information about. + final Channel channel; + + /// A callback that is called when a member is tapped. + final void Function(Member)? onMemberTap; + + /// A callback that is called when the "View Info" button is tapped. + final VoidCallback? onViewInfoTap; + + /// A callback that is called when the "Leave Channel" button is tapped. + /// + /// Only shown when the channel is a group channel. + final VoidCallback? onLeaveChannelTap; + + /// A callback that is called when the "Delete Conversation" button is tapped. + /// + /// Only shown when you are the `owner` of the channel. + final VoidCallback? onDeleteConversationTap; + + /// A callback that is called when the "Cancel" button is tapped. + final VoidCallback? onCancelTap; + + @override + Widget build(BuildContext context) { + final themeData = StreamChatTheme.of(context); + final colorTheme = themeData.colorTheme; + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + + final currentUser = channel.client.state.currentUser; + final isOneToOneChannel = channel.isDistinct && channel.memberCount == 2; + + final members = channel.state?.members ?? []; + + final isOwner = members.any( + (it) => it.user?.id == currentUser?.id && it.role == 'owner', + ); + + // remove current user in case it's 1-1 conversation + if (isOneToOneChannel) { + members.removeWhere((it) => it.user?.id == currentUser?.id); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 24), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamChannelName( + channel: channel, + textStyle: themeData.textTheme.headlineBold, + ), + ), + ), + const SizedBox(height: 5), + Center( + // TODO: Refactor ChannelInfo + child: StreamChannelInfo( + showTypingIndicator: false, + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ), + ), + const SizedBox(height: 17), + Container( + height: 94, + alignment: Alignment.center, + child: ListView.separated( + shrinkWrap: true, + itemCount: members.length, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemBuilder: (context, index) { + final member = members[index]; + final user = member.user!; + return Column( + children: [ + StreamUserAvatar( + user: user, + constraints: const BoxConstraints( + maxHeight: 64, + maxWidth: 64, + ), + borderRadius: BorderRadius.circular(32), + onlineIndicatorConstraints: BoxConstraints.tight( + const Size(12, 12), + ), + onTap: onMemberTap != null + ? (_) => onMemberTap!(member) + : null, + ), + const SizedBox(height: 6), + Text( + user.name, + style: themeData.textTheme.footnoteBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + }, + ), + ), + const SizedBox(height: 24), + StreamOptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.user( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.viewInfoLabel, + onTap: onViewInfoTap, + ), + if (!isOneToOneChannel) + StreamOptionListTile( + title: context.translations.leaveGroupLabel, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.userRemove( + color: colorTheme.textLowEmphasis, + ), + ), + onTap: onLeaveChannelTap, + ), + if (isOwner) + StreamOptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.delete( + color: colorTheme.accentError, + ), + ), + title: context.translations.deleteConversationLabel, + titleColor: colorTheme.accentError, + onTap: onDeleteConversationTap, + ), + StreamOptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.closeSmall( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.cancelLabel, + onTap: onCancelTap ?? Navigator.of(context).pop, + ), + ], + ); + } +} + +const _kDefaultChannelInfoBottomSheetShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), +); + +/// Shows a modal material design bottom sheet. +/// +/// A modal bottom sheet is an alternative to a menu or a dialog and prevents +/// the user from interacting with the rest of the app. +/// +/// A closely related widget is a persistent bottom sheet, which shows +/// information that supplements the primary content of the app without +/// preventing the use from interacting with the app. Persistent bottom sheets +/// can be created and displayed with the [showBottomSheet] function or the +/// [ScaffoldState.showBottomSheet] method. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the bottom sheet. It is only used when the method is called. Its +/// corresponding widget can be safely removed from the tree before the bottom +/// sheet is closed. +/// +/// The `isScrollControlled` parameter specifies whether this is a route for +/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish +/// to have a bottom sheet that has a scrollable child such as a [ListView] or +/// a [GridView] and have the bottom sheet be draggable, you should set this +/// parameter to true. +/// +/// The `useRootNavigator` parameter ensures that the root navigator is used to +/// display the [BottomSheet] when set to `true`. This is useful in the case +/// that a modal [BottomSheet] needs to be displayed above all other content +/// but the caller is inside another [Navigator]. +/// +/// The [isDismissible] parameter specifies whether the bottom sheet will be +/// dismissed when user taps on the scrim. +/// +/// The [enableDrag] parameter specifies whether the bottom sheet can be +/// dragged up and down and dismissed by swiping downwards. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// modal bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// The [transitionAnimationController] controls the bottom sheet's entrance and +/// exit animations if provided. +/// +/// The optional `routeSettings` parameter sets the [RouteSettings] +/// of the modal bottom sheet sheet. +/// This is particularly useful in the case that a user wants to observe +/// [PopupRoute]s within a [NavigatorObserver]. +/// +/// Returns a `Future` that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the modal bottom sheet was closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// function passed as the `builder` argument to [showModalBottomSheet]. +/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing +/// non-modal bottom sheets. +/// * [DraggableScrollableSheet], which allows you to create a bottom sheet +/// that grows and then becomes scrollable once it reaches its maximum size. +/// * +Future showChannelInfoModalBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + Color? barrierColor, + bool isScrollControlled = true, + bool useRootNavigator = false, + bool isDismissible = true, + bool enableDrag = true, + RouteSettings? routeSettings, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showModalBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + barrierColor: barrierColor, + isScrollControlled: isScrollControlled, + useRootNavigator: useRootNavigator, + isDismissible: isDismissible, + enableDrag: enableDrag, + routeSettings: routeSettings, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); + +/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If +/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet]. +/// +/// Returns a controller that can be used to close and otherwise manipulate the +/// bottom sheet. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// persistent bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// To rebuild the bottom sheet (e.g. if it is stateful), call +/// [PersistentBottomSheetController.setState] on the controller returned by +/// this method. +/// +/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing +/// [ModalRoute] and a back button is added to the app bar of the [Scaffold] +/// that closes the bottom sheet. +/// +/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and +/// does not add a back button to the enclosing Scaffold's app bar, use the +/// [Scaffold.bottomSheet] constructor parameter. +/// +/// A closely related widget is a modal bottom sheet, which is an alternative +/// to a menu or a dialog and prevents the user from interacting with the rest +/// of the app. Modal bottom sheets can be created and displayed with the +/// [showModalBottomSheet] function. +/// +/// The `context` argument is used to look up the [Scaffold] for the bottom +/// sheet. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the bottom sheet is closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// `builder`. +/// * [showModalBottomSheet], which can be used to display a modal bottom +/// sheet. +/// * [Scaffold.of], for information about how to obtain the [BuildContext]. +/// * +PersistentBottomSheetController showChannelInfoBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart new file mode 100644 index 000000000..c6070e0f1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// It shows the current [Channel] name using a [Text] widget. +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +class StreamChannelName extends StatelessWidget { + /// Instantiate a new ChannelName + StreamChannelName({ + Key? key, + required this.channel, + this.textStyle, + this.textOverflow = TextOverflow.ellipsis, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The [Channel] to show the name for. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// How visual overflow should be handled. + final TextOverflow textOverflow; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.nameStream, + initialData: channel.name, + builder: (context, channelName) => Text( + channelName, + style: textStyle, + overflow: textOverflow, + ), + noDataBuilder: (context) => _generateName( + channel.client.state.currentUser!, + channel.state!.members, + ), + ); + + Widget _generateName( + User currentUser, + List members, + ) => + LayoutBuilder( + builder: (context, constraints) { + var channelName = context.translations.noTitleText; + final otherMembers = members.where( + (member) => member.userId != currentUser.id, + ); + + if (otherMembers.isNotEmpty) { + if (otherMembers.length == 1) { + final user = otherMembers.first.user; + if (user != null) { + channelName = user.name; + } + } else { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / (textStyle?.fontSize ?? 1); + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = + currentChars + (element.user?.name.length ?? 0); + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); + + final exceedingMembers = + otherMembers.length - currentMembers.length; + channelName = + '${currentMembers.map((e) => e.user?.name).join(', ')} ' + '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } + } + + return Text( + channelName, + style: textStyle, + overflow: textOverflow, + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_message_preview_text.dart b/packages/stream_chat_flutter/lib/src/v4/stream_message_preview_text.dart new file mode 100644 index 000000000..53d260a29 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_message_preview_text.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that renders a preview of the message text. +class StreamMessagePreviewText extends StatelessWidget { + /// Creates a new instance of [StreamMessagePreviewText]. + const StreamMessagePreviewText({ + Key? key, + required this.message, + this.language, + this.textStyle, + }) : super(key: key); + + /// The message to display. + final Message message; + + /// The language to use for translations. + final String? language; + + /// The style to use for the text. + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final messageText = message + .translate(language ?? 'en') + .replaceMentions(linkify: false) + .text; + final messageAttachments = message.attachments; + final messageMentionedUsers = message.mentionedUsers; + + final mentionedUsersRegex = RegExp( + messageMentionedUsers.map((it) => '@${it.name}').join('|'), + caseSensitive: false, + ); + + final messageTextParts = [ + ...messageAttachments.map((it) { + if (it.type == 'image') { + return '📷'; + } else if (it.type == 'video') { + return '🎬'; + } else if (it.type == 'giphy') { + return '[GIF]'; + } + return it == message.attachments.last + ? (it.title ?? 'File') + : '${it.title ?? 'File'} , '; + }), + if (messageText != null) + if (messageMentionedUsers.isNotEmpty) + ...mentionedUsersRegex.allMatchesWithSep(messageText) + else + messageText, + ]; + + final fontStyle = (message.isSystem || message.isDeleted) + ? FontStyle.italic + : FontStyle.normal; + + final regularTextStyle = textStyle?.copyWith(fontStyle: fontStyle); + + final mentionsTextStyle = textStyle?.copyWith( + fontStyle: fontStyle, + fontWeight: FontWeight.bold, + ); + + final spans = [ + for (final part in messageTextParts) + if (messageMentionedUsers.isNotEmpty && + messageMentionedUsers.any((it) => '@${it.name}' == part)) + TextSpan( + text: part, + style: mentionsTextStyle, + ) + else if (messageAttachments.isNotEmpty && + messageAttachments + .where((it) => it.title != null) + .any((it) => it.title == part)) + TextSpan( + text: part, + style: regularTextStyle?.copyWith( + fontStyle: FontStyle.italic, + ), + ) + else + TextSpan( + text: part, + style: regularTextStyle, + ), + ]; + + return Text.rich( + TextSpan(children: spans), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ); + } +} + +extension _RegExpX on RegExp { + List allMatchesWithSep(String input, [int start = 0]) { + final result = []; + for (final match in allMatches(input, start)) { + result.add(input.substring(start, match.start)); + // ignore: cascade_invocations + result.add(match[0]!); + // ignore: parameter_assignments + start = match.end; + } + result.add(input.substring(start)); + return result; + } +} diff --git a/packages/stream_chat_flutter/lib/src/video_service.dart b/packages/stream_chat_flutter/lib/src/video_service.dart index a50a43c81..120460be9 100644 --- a/packages/stream_chat_flutter/lib/src/video_service.dart +++ b/packages/stream_chat_flutter/lib/src/video_service.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:synchronized/synchronized.dart'; -import 'package:video_compress/video_compress.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; /// @@ -12,32 +10,6 @@ class _IVideoService { /// Singleton instance of [_IVideoService] static final _IVideoService instance = _IVideoService._(); - final _lock = Lock(); - - /// compress video from [path] - /// compress video from [path] return [Future] - /// - /// you can choose its [quality] and [frameRate] - /// - /// ## example - /// ```dart - /// final info = await _flutterVideoCompress.compressVideo( - /// file.path, - /// ); - /// debugPrint(info.toJson()); - /// ``` - Future compressVideo( - String path, { - int frameRate = 30, - VideoQuality quality = VideoQuality.DefaultQuality, - }) async => - _lock.synchronized( - () => VideoCompress.compressVideo( - path, - frameRate: frameRate, - quality: quality, - ), - ); /// Generates a thumbnail image data in memory as UInt8List, /// it can be easily used by Image.memory(...). @@ -66,5 +38,10 @@ class _IVideoService { } /// Get instance of [_IVideoService] +@Deprecated("Use 'StreamVideoService' instead") // ignore: non_constant_identifier_names _IVideoService get VideoService => _IVideoService.instance; + +/// Get instance of [_IVideoService] +// ignore: non_constant_identifier_names +_IVideoService get StreamVideoService => _IVideoService.instance; diff --git a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart index 2286bc3dd..4d253da0a 100644 --- a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart @@ -6,10 +6,16 @@ import 'package:stream_chat_flutter/src/video_service.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; +/// {@macro video_thumbnail_image} +@Deprecated("Use 'StreamVideoThumbnailImage' instead") +typedef VideoThumbnailImage = StreamVideoThumbnailImage; + +/// {@template video_thumbnail_image} /// Widget for creating video thumbnail image -class VideoThumbnailImage extends StatefulWidget { - /// Constructor for creating [VideoThumbnailImage] - const VideoThumbnailImage({ +/// {@endtemplate} +class StreamVideoThumbnailImage extends StatefulWidget { + /// Constructor for creating [StreamVideoThumbnailImage] + const StreamVideoThumbnailImage({ Key? key, required this.video, this.width, @@ -42,16 +48,17 @@ class VideoThumbnailImage extends StatefulWidget { final WidgetBuilder? placeholderBuilder; @override - _VideoThumbnailImageState createState() => _VideoThumbnailImageState(); + _StreamVideoThumbnailImageState createState() => + _StreamVideoThumbnailImageState(); } -class _VideoThumbnailImageState extends State { +class _StreamVideoThumbnailImageState extends State { late Future thumbnailFuture; late StreamChatThemeData _streamChatTheme; @override void initState() { - thumbnailFuture = VideoService.generateVideoThumbnail( + thumbnailFuture = StreamVideoService.generateVideoThumbnail( video: widget.video, imageFormat: widget.format, ); @@ -65,9 +72,9 @@ class _VideoThumbnailImageState extends State { } @override - void didUpdateWidget(covariant VideoThumbnailImage oldWidget) { + void didUpdateWidget(covariant StreamVideoThumbnailImage oldWidget) { if (oldWidget.video != widget.video || oldWidget.format != widget.format) { - thumbnailFuture = VideoService.generateVideoThumbnail( + thumbnailFuture = StreamVideoService.generateVideoThumbnail( video: widget.video, imageFormat: widget.format, ); diff --git a/packages/stream_chat_flutter/lib/src/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/visible_footnote.dart index 700e22ad3..1456de3a1 100644 --- a/packages/stream_chat_flutter/lib/src/visible_footnote.dart +++ b/packages/stream_chat_flutter/lib/src/visible_footnote.dart @@ -2,10 +2,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro visible_footnote} +@Deprecated("Use 'StreamVisibleFootnote' instead") +typedef VisibleFootnote = StreamVisibleFootnote; + +/// {@template visible_footnote} /// Widget for displaying a footnote -class VisibleFootnote extends StatelessWidget { - /// Constructor for creating a [VisibleFootnote] - const VisibleFootnote({Key? key}) : super(key: key); +/// {@endtemplate} +class StreamVisibleFootnote extends StatelessWidget { + /// Constructor for creating a [StreamVisibleFootnote] + const StreamVisibleFootnote({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index cd0d93a0b..5052393ab 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -21,18 +21,20 @@ export 'src/gradient_avatar.dart'; export 'src/info_tile.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; -export 'src/mention_tile.dart'; export 'src/message_action.dart'; -export 'src/message_input.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'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; +export 'src/multi_overlay.dart'; export 'src/option_list_tile.dart'; export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; export 'src/sending_indicator.dart'; +export 'src/stream_attachment_package.dart'; export 'src/stream_chat.dart'; export 'src/stream_chat_theme.dart'; export 'src/stream_neumorphic_button.dart'; @@ -47,4 +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/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/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_message_preview_text.dart'; export 'src/visible_footnote.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 4ebc8f9e2..ac7a6c699 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 3.6.1 +version: 4.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -15,14 +15,14 @@ dependencies: chewie: ^1.3.0 collection: ^1.15.0 diacritic: ^0.1.3 - dio: ^4.0.0 + dio: ^4.0.6 ezanimation: ^0.6.0 file_picker: ^4.1.3 flutter: sdk: flutter flutter_markdown: ^0.6.1 flutter_portal: ^0.4.0 - flutter_slidable: ^0.6.0 + flutter_slidable: ^1.2.0 flutter_svg: ^1.0.1 http_parser: ^4.0.0 image_gallery_saver: ^1.7.0 @@ -36,11 +36,9 @@ dependencies: rxdart: ^0.27.0 share_plus: ^4.0.1 shimmer: ^2.0.0 - stream_chat_flutter_core: ^3.6.1 + stream_chat_flutter_core: ^4.0.0 substring_highlight: ^1.0.26 - synchronized: ^3.0.0 url_launcher: ^6.0.3 - video_compress: ^3.0.0 video_player: ^2.1.0 video_thumbnail: ^0.5.0 diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart index 3b711a85c..d59b80028 100644 --- a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart @@ -9,13 +9,16 @@ import 'mocks.dart'; class MockAttachmentDownloader extends Mock { ProgressCallback? progressCallback; + DownloadedPathCallback? downloadedPathCallback; Completer completer = Completer(); Future call( Attachment attachment, { ProgressCallback? progressCallback, + DownloadedPathCallback? downloadedPathCallback, }) { this.progressCallback = progressCallback; + this.downloadedPathCallback = downloadedPathCallback; return completer.future; } } @@ -38,6 +41,19 @@ void main() { final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); + final attachment = Attachment( + type: 'image', + title: 'text.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -45,19 +61,8 @@ void main() { streamChatThemeData: streamTheme, client: client, child: AttachmentActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'text.jpg', - ), - ], - ), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -80,6 +85,19 @@ void main() { final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); + final attachment = Attachment( + type: 'image', + title: 'text.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -87,19 +105,8 @@ void main() { streamChatThemeData: streamTheme, client: client, child: AttachmentActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'text.jpg', - ), - ], - ), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -122,6 +129,19 @@ void main() { final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); + final attachment = Attachment( + type: 'video', + title: 'video.mp4', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); await tester.pumpWidget( MaterialApp( theme: themeData, @@ -130,19 +150,8 @@ void main() { client: client, child: SizedBox( child: AttachmentActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'video', - title: 'video.mp4', - ), - ], - ), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -166,16 +175,17 @@ void main() { final mockObserver = MockNavigatorObserver(); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( text: 'test', user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), + attachment, ], ); await tester.pumpWidget( @@ -188,7 +198,7 @@ void main() { child: SizedBox( child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: attachment, ), ), ), @@ -212,6 +222,20 @@ void main() { final streamTheme = StreamChatThemeData.fromTheme(themeData); final onShowMessage = MockVoidCallback(); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); + await tester.pumpWidget( MaterialApp( theme: themeData, @@ -221,18 +245,8 @@ void main() { child: SizedBox( child: AttachmentActionsModal( onShowMessage: onShowMessage, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), - ]), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -255,20 +269,22 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + final targetAttachment = Attachment( + type: 'image', + title: 'image.jpg', + ); + final remainingAttachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( text: 'test', user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), - Attachment( - type: 'image', - title: 'image.jpg', - ), + targetAttachment, + remainingAttachment, ], ); @@ -283,7 +299,7 @@ void main() { channel: mockChannel, child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: targetAttachment, ), ), ), @@ -309,16 +325,17 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( text: 'test', user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), + attachment, ], ); @@ -333,7 +350,7 @@ void main() { channel: mockChannel, child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: attachment, ), ), ), @@ -358,15 +375,16 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); final message = Message( user: User( id: 'user-id', ), attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), + attachment, ], ); @@ -381,7 +399,7 @@ void main() { channel: mockChannel, child: AttachmentActionsModal( message: message, - currentIndex: 0, + attachment: attachment, ), ), ), @@ -402,6 +420,20 @@ void main() { final imageDownloader = MockAttachmentDownloader(); + final attachment = Attachment( + type: 'image', + title: 'image.jpg', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ], + ); + await tester.pumpWidget( MaterialApp( builder: (context, child) => StreamChat( @@ -411,18 +443,8 @@ void main() { home: SizedBox( child: AttachmentActionsModal( imageDownloader: imageDownloader, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'image', - title: 'image.jpg', - ), - ]), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -430,15 +452,16 @@ void main() { await tester.tap(find.text('Save Image')); - imageDownloader.progressCallback!(0, 100); + imageDownloader.progressCallback!(0, 100000); await tester.pump(); - expect(find.text('0%'), findsOneWidget); + expect(find.text('0.00 MB'), findsOneWidget); - imageDownloader.progressCallback!(50, 100); + imageDownloader.progressCallback!(50000, 100000); await tester.pump(); - expect(find.text('50%'), findsOneWidget); + expect(find.text('0.05 MB'), findsOneWidget); - imageDownloader.progressCallback!(100, 100); + imageDownloader.progressCallback!(100000, 100000); + imageDownloader.downloadedPathCallback!('path'); imageDownloader.completer.complete('path'); await tester.pump(); expect(find.byKey(const Key('completedIcon')), findsOneWidget); @@ -457,6 +480,19 @@ void main() { final fileDownloader = MockAttachmentDownloader(); + final attachment = Attachment( + type: 'video', + title: 'video.mp4', + ); + final message = Message( + text: 'test', + user: User( + id: 'user-id', + ), + attachments: [ + attachment, + ]); + await tester.pumpWidget( MaterialApp( builder: (context, child) => StreamChat( @@ -466,18 +502,8 @@ void main() { home: SizedBox( child: AttachmentActionsModal( fileDownloader: fileDownloader, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - Attachment( - type: 'video', - title: 'video.mp4', - ), - ]), - currentIndex: 0, + message: message, + attachment: attachment, ), ), ), @@ -485,15 +511,16 @@ void main() { await tester.tap(find.text('Save Video')); - fileDownloader.progressCallback!(0, 100); + fileDownloader.progressCallback!(0, 100000); await tester.pump(); - expect(find.text('0%'), findsOneWidget); + expect(find.text('0.00 MB'), findsOneWidget); - fileDownloader.progressCallback!(50, 100); + fileDownloader.progressCallback!(50000, 100000); await tester.pump(); - expect(find.text('50%'), findsOneWidget); + expect(find.text('0.05 MB'), findsOneWidget); - fileDownloader.progressCallback!(100, 100); + fileDownloader.progressCallback!(100000, 100000); + fileDownloader.downloadedPathCallback!('path'); fileDownloader.completer.complete('path'); await tester.pump(); expect(find.byKey(const Key('completedIcon')), findsOneWidget); diff --git a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart index ed4ce267a..9747f11d7 100644 --- a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart @@ -24,7 +24,7 @@ void main() { child: StreamChannel( channel: channel, child: SizedBox( - child: FileAttachment( + child: StreamFileAttachment( size: const Size( 300, 300, diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/back_button_test.dart index fca30c5b5..f6ec0fc43 100644 --- a/packages/stream_chat_flutter/test/src/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/back_button_test.dart @@ -136,7 +136,7 @@ void main() { ), ); - expect(find.byType(UnreadIndicator), findsOneWidget); + expect(find.byType(StreamUnreadIndicator), findsOneWidget); }, ); } 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 95d2e7519..6866eae39 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -58,16 +58,16 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader(), + body: StreamChannelHeader(), ), ), ), )); expect(find.text('test'), findsOneWidget); - expect(find.byType(ChannelAvatar), findsOneWidget); + expect(find.byType(StreamChannelAvatar), findsOneWidget); expect(find.byType(StreamBackButton), findsOneWidget); - expect(find.byType(ChannelInfo), findsOneWidget); + expect(find.byType(StreamChannelInfo), findsOneWidget); }, ); @@ -124,7 +124,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showConnectionStateTile: true, ), ), @@ -132,8 +132,12 @@ void main() { ), )); - expect(tester.widget(find.byType(InfoTile)).showMessage, true); - expect(tester.widget(find.byType(InfoTile)).message, + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Disconnected'); }, ); @@ -190,7 +194,7 @@ void main() { channel: channel, showLoading: false, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showConnectionStateTile: true, ), ), @@ -200,8 +204,12 @@ void main() { await tester.pump(); - expect(tester.widget(find.byType(InfoTile)).showMessage, true); - expect(tester.widget(find.byType(InfoTile)).message, + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Reconnecting...'); }, ); @@ -257,7 +265,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( leading: Text('leading'), subtitle: Text('subtitle'), actions: [ @@ -272,8 +280,8 @@ void main() { expect(find.text('test'), findsNothing); expect(find.byType(StreamBackButton), findsNothing); - expect(find.byType(ChannelAvatar), findsNothing); - expect(find.byType(ChannelInfo), findsNothing); + expect(find.byType(StreamChannelAvatar), findsNothing); + expect(find.byType(StreamChannelInfo), findsNothing); expect(find.text('leading'), findsOneWidget); expect(find.text('title'), findsOneWidget); expect(find.text('subtitle'), findsOneWidget); @@ -331,7 +339,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showTypingIndicator: false, showBackButton: false, ), @@ -343,10 +351,14 @@ void main() { expect(find.byType(StreamBackButton), findsNothing); expect( tester - .widget(find.byType(ChannelInfo)) + .widget(find.byType(StreamChannelInfo)) .showTypingIndicator, false); - expect(tester.widget(find.byType(InfoTile)).showMessage, false); + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + false); }, ); @@ -405,7 +417,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( onBackPressed: () => backPressed = true, onImageTap: () => imageTapped = true, onTitleTap: () => titleTapped = true, @@ -416,8 +428,8 @@ void main() { )); await tester.tap(find.byType(StreamBackButton)); - await tester.tap(find.byType(ChannelAvatar)); - await tester.tap(find.byType(ChannelName)); + await tester.tap(find.byType(StreamChannelAvatar)); + await tester.tap(find.byType(StreamChannelName)); expect(backPressed, true); expect(imageTapped, true); 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 be2ade556..1ec1c1665 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,14 +166,17 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: const Scaffold( - body: ChannelAvatar(), + child: Scaffold( + body: StreamChannelAvatar( + channel: channel, + ), ), ), ), )); - final image = tester.widget(find.byType(GroupAvatar)); + final image = + tester.widget(find.byType(StreamGroupAvatar)); final otherMembers = members.where((it) => it.userId != currentUser.id); expect( image.members.map((it) => it.user?.id), @@ -201,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_list_header_test.dart b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart index a27830672..532d07d6f 100644 --- a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart @@ -22,14 +22,15 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader(), + body: StreamChannelListHeader(), ), ), ), ); await tester.pumpAndSettle(); - final userAvatar = tester.widget(find.byType(UserAvatar)); + final userAvatar = + tester.widget(find.byType(StreamUserAvatar)); expect(userAvatar.user, clientState.currentUser); expect(find.byType(StreamNeumorphicButton), findsOneWidget); expect(find.text('Stream Chat'), findsOneWidget); @@ -52,7 +53,7 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( showConnectionStateTile: true, ), ), @@ -81,7 +82,7 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( showConnectionStateTile: true, ), ), @@ -110,7 +111,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( titleBuilder: (context, status, client) => const Text('TITLE'), subtitle: const Text('SUBTITLE'), leading: const Text('LEADING'), @@ -150,7 +151,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( preNavigationCallback: () { tapped = true; }, @@ -161,7 +162,7 @@ void main() { ); await tester.pump(); - await tester.tap(find.byType(UserAvatar)); + await tester.tap(find.byType(StreamUserAvatar)); expect(tapped, true); }, ); @@ -183,7 +184,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( onUserAvatarTap: (u) { tapped++; }, @@ -197,7 +198,7 @@ void main() { ); await tester.pump(); - await tester.tap(find.byType(UserAvatar)); + await tester.tap(find.byType(StreamUserAvatar)); await tester.tap(find.byType(StreamNeumorphicButton)); expect(tapped, 2); }, 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 dcfda04f8..8848207ed 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 475546059..a2ab02f1b 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'; @@ -83,7 +85,7 @@ void main() { expect(find.text('test name'), findsOneWidget); expect(find.text('1'), findsOneWidget); expect(find.text('hello'), findsOneWidget); - expect(find.byType(ChannelAvatar), findsOneWidget); + expect(find.byType(StreamChannelAvatar), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/date_divider_test.dart b/packages/stream_chat_flutter/test/src/date_divider_test.dart index f84d4dceb..59ba4be11 100644 --- a/packages/stream_chat_flutter/test/src/date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/date_divider_test.dart @@ -19,7 +19,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: DateDivider( + body: StreamDateDivider( dateTime: DateTime.now(), ), ), diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart index 0d86e89c9..171b993de 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -20,8 +20,8 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: DeletedMessage( - messageTheme: MessageThemeData( + body: StreamDeletedMessage( + messageTheme: StreamMessageThemeData( createdAtStyle: TextStyle( color: Colors.black, ), @@ -75,7 +75,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, ), ), @@ -128,7 +128,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, ), ), @@ -181,7 +181,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, reverse: true, shape: RoundedRectangleBorder( diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index 2d2710909..7a815888f 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -63,22 +63,29 @@ void main() { Event(type: EventType.typingStart), })); + final attachment = Attachment( + type: 'image', + title: 'demo image', + imageUrl: '', + ); + final message = Message( + createdAt: DateTime.now(), + attachments: [ + attachment, + ], + ); await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, child: StreamChannel( channel: channel, - child: FullScreenMedia( - mediaAttachments: [ - Attachment( - type: 'image', - title: 'demo image', - imageUrl: '', + child: StreamFullScreenMedia( + mediaAttachmentPackages: [ + StreamAttachmentPackage( + attachment: attachment, + message: message, ), ], - message: Message( - createdAt: DateTime.now(), - ), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart index 6dcc0e2dd..073adc555 100644 --- a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart @@ -25,7 +25,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo user', userId: 'demo123'), + child: StreamGradientAvatar( + name: 'demo user', userId: 'demo123'), ), ), ), @@ -33,7 +34,7 @@ void main() { ), ); - expect(find.byType(GradientAvatar), findsOneWidget); + expect(find.byType(StreamGradientAvatar), findsOneWidget); }, ); @@ -47,7 +48,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo user', userId: 'demo123'), + child: + StreamGradientAvatar(name: 'demo user', userId: 'demo123'), ), ), ), @@ -68,7 +70,7 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo', userId: 'demo1'), + child: StreamGradientAvatar(name: 'demo', userId: 'demo1'), ), ), ), @@ -89,7 +91,7 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar( + child: StreamGradientAvatar( name: 'd123@/d de:\$as', userId: 'demo123', ), @@ -113,7 +115,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: '123@/d \$as', userId: 'demo123'), + child: StreamGradientAvatar( + name: '123@/d \$as', userId: 'demo123'), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart index 434a78d45..2e2771b4e 100644 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/image_footer_test.dart @@ -37,8 +37,8 @@ void main() { child: WillPopScope( onWillPop: () async => false, child: Scaffold( - body: GalleryFooter( - message: Message(), + body: StreamGalleryFooter( + mediaAttachmentPackages: Message().getAttachmentPackageList(), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/info_tile_test.dart b/packages/stream_chat_flutter/test/src/info_tile_test.dart index f175ca9ae..e0687964a 100644 --- a/packages/stream_chat_flutter/test/src/info_tile_test.dart +++ b/packages/stream_chat_flutter/test/src/info_tile_test.dart @@ -22,7 +22,7 @@ void main() { child: const Scaffold( body: Portal( child: SizedBox( - child: InfoTile( + child: StreamInfoTile( showMessage: true, message: 'message', child: Text('test'), @@ -52,7 +52,7 @@ void main() { child: const Scaffold( body: Portal( child: SizedBox( - child: InfoTile( + child: StreamInfoTile( showMessage: false, message: 'message', child: Text('test'), 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 index 3500d30b6..c8dd1d7d3 100644 --- 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 @@ -1,5 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; -import 'package:test/test.dart'; void main() { test('should update media', () { diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 9210ac49d..e0f8dcb30 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -18,6 +18,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -31,18 +32,25 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, + showEditMessage: true, + showDeleteMessage: true, ), - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -64,6 +72,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -77,22 +86,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - showEditMessage: false, - showCopyMessage: false, - showDeleteMessage: false, - showReplyMessage: false, - showThreadReplyMessage: false, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + showCopyMessage: false, + showReplyMessage: false, + showThreadReplyMessage: false, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), ), - ), - messageTheme: streamTheme.ownMessageTheme, - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), ), ), ), @@ -115,6 +125,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -130,24 +141,27 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + messageWidget: const Text('test'), + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), ), + messageTheme: streamTheme.ownMessageTheme, + customActions: [ + StreamMessageAction( + leading: const Icon(Icons.check), + title: const Text('title'), + onTap: (m) { + tapped = true; + }, + ), + ], ), - messageTheme: streamTheme.ownMessageTheme, - customActions: [ - MessageAction( - leading: const Icon(Icons.check), - title: const Text('title'), - onTap: (m) { - tapped = true; - }, - ), - ], ), ), ), @@ -170,6 +184,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -186,18 +201,22 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + messageWidget: const Text('test'), + onReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), + messageTheme: streamTheme.ownMessageTheme, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -216,6 +235,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -232,18 +252,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onThreadReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + messageWidget: const Text('test'), + onThreadReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -284,7 +309,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( text: 'test', @@ -293,6 +318,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -304,7 +330,7 @@ void main() { await tester.pumpAndSettle(); - expect(find.byType(MessageInput), findsOneWidget); + expect(find.byType(StreamMessageInput), findsOneWidget); }, ); @@ -333,7 +359,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), editMessageInputBuilder: (context, m) => const Text('test'), message: Message( @@ -343,6 +369,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -385,7 +412,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), onCopyTap: (m) => tapped = true, message: Message( @@ -435,7 +462,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed, @@ -485,7 +512,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed_update, @@ -533,7 +560,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -543,6 +570,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -589,7 +617,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -599,6 +627,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -645,7 +674,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -655,6 +684,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -699,7 +729,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -709,6 +739,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), @@ -755,7 +786,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -765,6 +796,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input_test.dart index 62fe0107d..cdedc8180 100644 --- a/packages/stream_chat_flutter/test/src/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input_test.dart @@ -59,7 +59,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: MessageInput(), + body: StreamMessageInput(), ), ), ), @@ -125,7 +125,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: MessageInput(), + body: StreamMessageInput(), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_list_view_test.dart b/packages/stream_chat_flutter/test/src/message_list_view_test.dart index b82d53b3a..08e238207 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/message_list_view_test.dart @@ -54,7 +54,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: MessageListView( + child: StreamMessageListView( emptyBuilder: (_) => Container(key: emptyWidgetKey), ), ), @@ -63,7 +63,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byType(MessageListView), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); expect(find.byKey(emptyWidgetKey), findsOneWidget); }); @@ -92,7 +92,7 @@ void main() { child: StreamChat( client: client, streamChatThemeData: StreamChatThemeData.light().copyWith( - messageListViewTheme: const MessageListViewThemeData( + messageListViewTheme: const StreamMessageListViewThemeData( backgroundColor: Colors.grey, backgroundImage: DecorationImage( image: AssetImage('images/placeholder.png'), @@ -102,7 +102,7 @@ void main() { ), child: StreamChannel( channel: channel, - child: const MessageListView( + child: const StreamMessageListView( key: nonEmptyWidgetKey, ), ), @@ -118,7 +118,7 @@ void main() { widget.decoration is BoxDecoration && (widget.decoration! as BoxDecoration).image != null; - expect(find.byType(MessageListView), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); expect(find.byKey(nonEmptyWidgetKey), findsOneWidget); expect( find.byWidgetPredicate( diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart index 3732ca0d3..31812bf2b 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -13,6 +13,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -33,13 +34,16 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: StreamMessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -47,9 +51,9 @@ void main() { await tester.pump(const Duration(milliseconds: 1000)); - expect(find.byType(ReactionBubble), findsNothing); + expect(find.byType(StreamReactionBubble), findsNothing); - expect(find.byType(UserAvatar), findsNothing); + expect(find.byType(StreamUserAvatar), findsNothing); }, ); @@ -58,6 +62,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -89,16 +94,19 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: StreamMessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, + reverse: true, + showReactions: false, + onUserAvatarTap: onUserAvatarTap, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, - reverse: true, - showReactions: false, - onUserAvatarTap: onUserAvatarTap, ), ), ), @@ -108,8 +116,8 @@ void main() { expect(find.byKey(const Key('MessageWidget')), findsOneWidget); - expect(find.byType(ReactionBubble), findsOneWidget); - expect(find.byType(UserAvatar), findsOneWidget); + expect(find.byType(StreamReactionBubble), findsOneWidget); + expect(find.byType(StreamUserAvatar), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_text_test.dart index 57f0a282a..8f2d4c108 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -65,7 +65,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: Message( text: 'demo', ), @@ -84,7 +84,7 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); - const messageTheme = MessageThemeData(); + const messageTheme = StreamMessageThemeData(); final currentUser = OwnUser( id: 'sahil', @@ -122,7 +122,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: message, messageTheme: messageTheme, ), @@ -158,7 +158,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: message, messageTheme: messageTheme, ), @@ -221,7 +221,7 @@ cool.'''; child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: Message( text: messageText, ), diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 443f770ca..1fa1e99b6 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -21,6 +21,9 @@ class MockChannel extends Mock implements Channel { Future keyStroke([String? parentId]) async { return; } + + @override + List get ownCapabilities => ['send-message']; } class MockChannelState extends Mock implements ChannelClientState { diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index 6e823b43a..50719b171 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -25,7 +25,7 @@ void main() { streamChatThemeData: theme, connectivityStream: Stream.value(ConnectivityResult.mobile), child: SizedBox( - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -62,7 +62,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -99,7 +99,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -144,7 +144,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -187,7 +187,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), streamChatThemeData: StreamChatThemeData.fromTheme(themeData), child: SizedBox( - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/system_message_test.dart index 24eccdf6f..c723ff242 100644 --- a/packages/stream_chat_flutter/test/src/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/system_message_test.dart @@ -40,7 +40,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: SystemMessage( + body: StreamSystemMessage( onMessageTap: (m) => tapped = true, message: Message( text: 'demo message', @@ -51,7 +51,7 @@ void main() { ), )); - await tester.tap(find.byType(SystemMessage)); + await tester.tap(find.byType(StreamSystemMessage)); expect(find.text('demo message'), findsOneWidget); expect(tapped, true); @@ -94,7 +94,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: SystemMessage( + child: StreamSystemMessage( message: Message( text: 'demo message', ), @@ -146,7 +146,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: SystemMessage( + child: StreamSystemMessage( message: Message( text: 'demo message', ), diff --git a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart index 63e6da861..8d055d529 100644 --- a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart @@ -4,22 +4,23 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('AvatarThemeData copyWith, ==, hashCode basics', () { - expect(const AvatarThemeData(), const AvatarThemeData().copyWith()); - expect(const AvatarThemeData().hashCode, - const AvatarThemeData().copyWith().hashCode); + expect(const StreamAvatarThemeData(), + const StreamAvatarThemeData().copyWith()); + expect(const StreamAvatarThemeData().hashCode, + const StreamAvatarThemeData().copyWith().hashCode); }); group('AvatarThemeData lerps correctly', () { test('Lerp completely', () { expect( - const AvatarThemeData() + const StreamAvatarThemeData() .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 1), _avatarThemeDataControl2); }); test('Lerp halfway', () { expect( - const AvatarThemeData() + const StreamAvatarThemeData() .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 0.5), _avatarThemeDataControlMidLerp); }); @@ -31,9 +32,9 @@ void main() { }); } -const _avatarThemeDataControl1 = AvatarThemeData(); +const _avatarThemeDataControl1 = StreamAvatarThemeData(); -final _avatarThemeDataControlMidLerp = AvatarThemeData( +final _avatarThemeDataControlMidLerp = StreamAvatarThemeData( borderRadius: BorderRadius.circular(16), constraints: const BoxConstraints.tightFor( height: 33, @@ -41,7 +42,7 @@ final _avatarThemeDataControlMidLerp = AvatarThemeData( ), ); -final _avatarThemeDataControl2 = AvatarThemeData( +final _avatarThemeDataControl2 = StreamAvatarThemeData( borderRadius: BorderRadius.circular(12), constraints: const BoxConstraints.tightFor( height: 34, diff --git a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart index 475b10848..1b5499dd4 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelHeaderThemeData(), - const ChannelHeaderThemeData().copyWith()); - expect(const ChannelHeaderThemeData().hashCode, - const ChannelHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelHeaderThemeData(), + const StreamChannelHeaderThemeData().copyWith()); + expect(const StreamChannelHeaderThemeData().hashCode, + const StreamChannelHeaderThemeData().copyWith().hashCode); }); group('ChannelHeaderThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelHeaderThemeData lerps completely to dark ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControl, _channelThemeControlDark, 1), _channelThemeControlDark); }); @@ -24,7 +24,7 @@ void main() { '''Light ChannelHeaderThemeData lerps halfway to dark ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControl, _channelThemeControlDark, 0.5), _channelThemeControlMidLerp); }); @@ -33,7 +33,7 @@ void main() { '''Dark ChannelHeaderThemeData lerps completely to light ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControlDark, _channelThemeControl, 1), _channelThemeControl); }); @@ -45,8 +45,8 @@ void main() { }); } -final _channelThemeControl = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControl = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -54,16 +54,16 @@ final _channelThemeControl = ChannelHeaderThemeData( ), ), color: const Color(0xff101418), - titleStyle: TextTheme.light().headlineBold.copyWith( + titleStyle: StreamTextTheme.light().headlineBold.copyWith( color: const Color(0xffffffff), ), - subtitleStyle: TextTheme.light().footnote.copyWith( + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); -final _channelThemeControlMidLerp = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControlMidLerp = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -76,22 +76,22 @@ final _channelThemeControlMidLerp = ChannelHeaderThemeData( fontWeight: FontWeight.bold, fontSize: 16, ), - subtitleStyle: TextTheme.light().footnote.copyWith( + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); -final _channelThemeControlDark = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControlDark = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.dark().barsBg, - titleStyle: TextTheme.dark().headlineBold, - subtitleStyle: TextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().barsBg, + titleStyle: StreamTextTheme.dark().headlineBold, + subtitleStyle: StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart index aa378f3ae..14c097066 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelListHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelListHeaderThemeData(), - const ChannelListHeaderThemeData().copyWith()); - expect(const ChannelListHeaderThemeData().hashCode, - const ChannelListHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelListHeaderThemeData(), + const StreamChannelListHeaderThemeData().copyWith()); + expect(const StreamChannelListHeaderThemeData().hashCode, + const StreamChannelListHeaderThemeData().copyWith().hashCode); }); group('ChannelListHeaderThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelListHeaderThemeData lerps completely to dark ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControl, _channelListHeaderThemeControlDark, 1), @@ -26,7 +26,7 @@ void main() { '''Light ChannelListHeaderThemeData lerps halfway to dark ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControl, _channelListHeaderThemeControlDark, 0.5), @@ -37,7 +37,7 @@ void main() { '''Dark ChannelListHeaderThemeData lerps completely to light ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControlDark, _channelListHeaderThemeControl, 1), @@ -53,20 +53,20 @@ void main() { }); } -final _channelListHeaderThemeControl = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControl = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.light().barsBg, - titleStyle: TextTheme.light().headlineBold, + color: StreamColorTheme.light().barsBg, + titleStyle: StreamTextTheme.light().headlineBold, ); -final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControlMidLerp = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -81,14 +81,14 @@ final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( ), ); -final _channelListHeaderThemeControlDark = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControlDark = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.dark().barsBg, - titleStyle: TextTheme.dark().headlineBold, + color: StreamColorTheme.dark().barsBg, + titleStyle: StreamTextTheme.dark().headlineBold, ); 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 079c01e38..71193da4e 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'; @@ -6,16 +8,18 @@ import '../mocks.dart'; void main() { test('ChannelListViewThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelListViewThemeData(), - const ChannelListViewThemeData().copyWith()); + expect(const StreamChannelListViewThemeData(), + const StreamChannelListViewThemeData().copyWith()); }); test( '''Light ChannelListViewThemeData lerps completely to dark ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, 1), + const StreamChannelListViewThemeData().lerp( + _channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, + 1), _channelListViewThemeDataControlDark); }); @@ -23,8 +27,10 @@ void main() { '''Light ChannelListViewThemeData lerps halfway to dark ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, 0.5), + const StreamChannelListViewThemeData().lerp( + _channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, + 0.5), _channelListViewThemeDataControlHalfLerp); }); @@ -32,7 +38,7 @@ void main() { '''Dark ChannelListViewThemeData lerps completely to light ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp( + const StreamChannelListViewThemeData().lerp( _channelListViewThemeDataControlDark, _channelListViewThemeDataControl, 1), @@ -70,7 +76,7 @@ void main() { ), ); - final channelListViewTheme = ChannelListViewTheme.of(_context); + final channelListViewTheme = StreamChannelListViewTheme.of(_context); expect(channelListViewTheme.backgroundColor, _channelListViewThemeDataControl.backgroundColor); }); @@ -92,7 +98,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -100,20 +106,20 @@ void main() { ), ); - final channelListViewTheme = ChannelListViewTheme.of(_context); + final channelListViewTheme = StreamChannelListViewTheme.of(_context); expect(channelListViewTheme.backgroundColor, _channelListViewThemeDataControlDark.backgroundColor); }); } -final _channelListViewThemeDataControl = ChannelListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _channelListViewThemeDataControl = StreamChannelListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); -const _channelListViewThemeDataControlHalfLerp = ChannelListViewThemeData( +const _channelListViewThemeDataControlHalfLerp = StreamChannelListViewThemeData( backgroundColor: Color(0xff818384), ); -final _channelListViewThemeDataControlDark = ChannelListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, +final _channelListViewThemeDataControlDark = StreamChannelListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart index dc5c0529b..782be835e 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelPreviewThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelPreviewThemeData(), - const ChannelPreviewThemeData().copyWith()); - expect(const ChannelPreviewThemeData().hashCode, - const ChannelPreviewThemeData().copyWith().hashCode); + expect(const StreamChannelPreviewThemeData(), + const StreamChannelPreviewThemeData().copyWith()); + expect(const StreamChannelPreviewThemeData().hashCode, + const StreamChannelPreviewThemeData().copyWith().hashCode); }); group('ChannelPreviewThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelPreviewThemeData lerps completely to dark ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp( + const StreamChannelPreviewThemeData().lerp( _channelPreviewThemeControl, _channelPreviewThemeControlDark, 1), _channelPreviewThemeControlDark); }); @@ -24,8 +24,10 @@ void main() { '''Light ChannelPreviewThemeData lerps halfway to dark ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp(_channelPreviewThemeControl, - _channelPreviewThemeControlDark, 0.5), + const StreamChannelPreviewThemeData().lerp( + _channelPreviewThemeControl, + _channelPreviewThemeControlDark, + 0.5), _channelPreviewThemeControlMidLerp); }); @@ -33,7 +35,7 @@ void main() { '''Dark ChannelPreviewThemeData lerps completely to light ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp( + const StreamChannelPreviewThemeData().lerp( _channelPreviewThemeControlDark, _channelPreviewThemeControl, 1), _channelPreviewThemeControl); }); @@ -45,28 +47,28 @@ void main() { }); } -final _channelPreviewThemeControl = ChannelPreviewThemeData( - unreadCounterColor: ColorTheme.light().accentError, - avatarTheme: AvatarThemeData( +final _channelPreviewThemeControl = StreamChannelPreviewThemeData( + unreadCounterColor: StreamColorTheme.light().accentError, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - titleStyle: TextTheme.light().bodyBold, - subtitleStyle: TextTheme.light().footnote.copyWith( + titleStyle: StreamTextTheme.light().bodyBold, + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textHighEmphasis.withOpacity(0.5), + lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); -final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( +final _channelPreviewThemeControlMidLerp = StreamChannelPreviewThemeData( unreadCounterColor: const Color(0xffff3742), - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -82,27 +84,27 @@ final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( color: Color(0xff7a7a7a), fontSize: 12, ), - lastMessageAtStyle: TextTheme.light().footnote.copyWith( + lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0x807f7f7f).withOpacity(0.5), ), indicatorIconSize: 16, ); -final _channelPreviewThemeControlDark = ChannelPreviewThemeData( - unreadCounterColor: ColorTheme.dark().accentError, - avatarTheme: AvatarThemeData( +final _channelPreviewThemeControlDark = StreamChannelPreviewThemeData( + unreadCounterColor: StreamColorTheme.dark().accentError, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - titleStyle: TextTheme.dark().bodyBold, - subtitleStyle: TextTheme.dark().footnote.copyWith( + titleStyle: StreamTextTheme.dark().bodyBold, + subtitleStyle: StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textHighEmphasis.withOpacity(0.5), + lastMessageAtStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart index 6a73e34e2..f8d1d164d 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart @@ -7,18 +7,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryFooterThemeData copyWith, ==, hashCode basics', () { - expect(const GalleryFooterThemeData(), - const GalleryFooterThemeData().copyWith()); - expect(const GalleryFooterThemeData().hashCode, - const GalleryFooterThemeData().copyWith().hashCode); + expect(const StreamGalleryFooterThemeData(), + const StreamGalleryFooterThemeData().copyWith()); + expect(const StreamGalleryFooterThemeData().hashCode, + const StreamGalleryFooterThemeData().copyWith().hashCode); }); test( '''Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, - _galleryFooterThemeDataControlDark, 1), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, + 1), _galleryFooterThemeDataControlDark); }); @@ -26,8 +28,10 @@ void main() { '''Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, - _galleryFooterThemeDataControlDark, 0.5), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, + 0.5), _galleryFooterThemeDataControlMidLerp); }); @@ -35,8 +39,10 @@ void main() { '''Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark, - _galleryFooterThemeDataControl, 1), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControlDark, + _galleryFooterThemeDataControl, + 1), _galleryFooterThemeDataControl); }); @@ -68,8 +74,8 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryFooter( - message: Message(), + appBar: StreamGalleryFooter( + mediaAttachmentPackages: Message().getAttachmentPackageList(), ), ); }, @@ -77,7 +83,7 @@ void main() { ), ); - final imageFooterTheme = GalleryFooterTheme.of(_context); + final imageFooterTheme = StreamGalleryFooterTheme.of(_context); expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControl.backgroundColor); expect(imageFooterTheme.shareIconColor, @@ -111,8 +117,8 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryFooter( - message: Message(), + appBar: StreamGalleryFooter( + mediaAttachmentPackages: Message().getAttachmentPackageList(), ), ); }, @@ -120,7 +126,7 @@ void main() { ), ); - final imageFooterTheme = GalleryFooterTheme.of(_context); + final imageFooterTheme = StreamGalleryFooterTheme.of(_context); expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControlDark.backgroundColor); expect(imageFooterTheme.shareIconColor, @@ -141,19 +147,19 @@ void main() { } // Light theme control -final _galleryFooterThemeDataControl = GalleryFooterThemeData( - backgroundColor: ColorTheme.light().barsBg, - shareIconColor: ColorTheme.light().textHighEmphasis, - titleTextStyle: TextTheme.light().headlineBold, - gridIconButtonColor: ColorTheme.light().textHighEmphasis, - bottomSheetBackgroundColor: ColorTheme.light().barsBg, - bottomSheetBarrierColor: ColorTheme.light().overlay, - bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis, - bottomSheetPhotosTextStyle: TextTheme.light().headlineBold, +final _galleryFooterThemeDataControl = StreamGalleryFooterThemeData( + backgroundColor: StreamColorTheme.light().barsBg, + shareIconColor: StreamColorTheme.light().textHighEmphasis, + titleTextStyle: StreamTextTheme.light().headlineBold, + gridIconButtonColor: StreamColorTheme.light().textHighEmphasis, + bottomSheetBackgroundColor: StreamColorTheme.light().barsBg, + bottomSheetBarrierColor: StreamColorTheme.light().overlay, + bottomSheetCloseIconColor: StreamColorTheme.light().textHighEmphasis, + bottomSheetPhotosTextStyle: StreamTextTheme.light().headlineBold, ); // Mid-lerp theme control -const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( +const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData( backgroundColor: Color(0xff87898b), shareIconColor: Color(0xff7f7f7f), titleTextStyle: TextStyle( @@ -173,13 +179,13 @@ const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( ); // Dark theme control -final _galleryFooterThemeDataControlDark = GalleryFooterThemeData( - backgroundColor: ColorTheme.dark().barsBg, - shareIconColor: ColorTheme.dark().textHighEmphasis, - titleTextStyle: TextTheme.dark().headlineBold, - gridIconButtonColor: ColorTheme.dark().textHighEmphasis, - bottomSheetBackgroundColor: ColorTheme.dark().barsBg, - bottomSheetBarrierColor: ColorTheme.dark().overlay, - bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis, - bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold, +final _galleryFooterThemeDataControlDark = StreamGalleryFooterThemeData( + backgroundColor: StreamColorTheme.dark().barsBg, + shareIconColor: StreamColorTheme.dark().textHighEmphasis, + titleTextStyle: StreamTextTheme.dark().headlineBold, + gridIconButtonColor: StreamColorTheme.dark().textHighEmphasis, + bottomSheetBackgroundColor: StreamColorTheme.dark().barsBg, + bottomSheetBarrierColor: StreamColorTheme.dark().overlay, + bottomSheetCloseIconColor: StreamColorTheme.dark().textHighEmphasis, + bottomSheetPhotosTextStyle: StreamTextTheme.dark().headlineBold, ); diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart index 534008bad..651da1ce1 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart @@ -7,18 +7,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const GalleryHeaderThemeData(), - const GalleryHeaderThemeData().copyWith()); - expect(const GalleryHeaderThemeData().hashCode, - const GalleryHeaderThemeData().copyWith().hashCode); + expect(const StreamGalleryHeaderThemeData(), + const StreamGalleryHeaderThemeData().copyWith()); + expect(const StreamGalleryHeaderThemeData().hashCode, + const StreamGalleryHeaderThemeData().copyWith().hashCode); }); test( '''Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, - _galleryHeaderThemeDataDarkControl, 1), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, + 1), _galleryHeaderThemeDataDarkControl); }); @@ -26,8 +28,10 @@ void main() { '''Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, - _galleryHeaderThemeDataDarkControl, 0.5), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, + 0.5), _galleryHeaderThemeDataHalfLerpControl); }); @@ -35,8 +39,10 @@ void main() { '''Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl, - _galleryHeaderThemeDataControl, 1), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataDarkControl, + _galleryHeaderThemeDataControl, + 1), _galleryHeaderThemeDataControl); }); @@ -60,9 +66,20 @@ void main() { home: Builder( builder: (context) { _context = context; + final attachment = Attachment( + type: 'video', + title: 'video.mp4', + ); + final _message = Message( + createdAt: DateTime.now(), + attachments: [ + attachment, + ], + ); return Scaffold( - appBar: GalleryHeader( - message: Message(), + appBar: StreamGalleryHeader( + message: _message, + attachment: _message.attachments[0], ), ); }, @@ -70,7 +87,7 @@ void main() { ), ); - final imageHeaderTheme = GalleryHeaderTheme.of(_context); + final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataControl.closeButtonColor); expect(imageHeaderTheme.backgroundColor, @@ -99,9 +116,20 @@ void main() { home: Builder( builder: (context) { _context = context; + final attachment = Attachment( + type: 'video', + title: 'video.mp4', + ); + final _message = Message( + createdAt: DateTime.now(), + attachments: [ + attachment, + ], + ); return Scaffold( - appBar: GalleryHeader( - message: Message(), + appBar: StreamGalleryHeader( + message: _message, + attachment: _message.attachments[0], ), ); }, @@ -109,7 +137,7 @@ void main() { ), ); - final imageHeaderTheme = GalleryHeaderTheme.of(_context); + final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataDarkControl.closeButtonColor); expect(imageHeaderTheme.backgroundColor, @@ -126,7 +154,7 @@ void main() { } // Light theme test control. -final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xff000000), backgroundColor: const Color(0xffffffff), iconMenuPointColor: const Color(0xff000000), @@ -145,7 +173,7 @@ final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( ); // Light theme test control. -final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xff7f7f7f), backgroundColor: const Color(0xff87898b), iconMenuPointColor: const Color(0xff7f7f7f), @@ -164,7 +192,7 @@ final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( ); // Dark theme test control. -final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xffffffff), backgroundColor: const Color(0xff101418), iconMenuPointColor: const Color(0xffffffff), diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index c618d65c3..3c62baeaa 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -4,30 +4,30 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('MessageInputThemeData copyWith, ==, hashCode basics', () { - expect(const MessageInputThemeData(), - const MessageInputThemeData().copyWith()); - expect(const MessageInputThemeData().hashCode, - const MessageInputThemeData().copyWith().hashCode); + expect(const StreamMessageInputThemeData(), + const StreamMessageInputThemeData().copyWith()); + expect(const StreamMessageInputThemeData().hashCode, + const StreamMessageInputThemeData().copyWith().hashCode); }); group('MessageInputThemeData lerps correctly', () { test('Lerp completely from light to dark', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControl, _messageInputThemeControlDark, 1), _messageInputThemeControlDark); }); test('Lerp halfway from light to dark', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControl, _messageInputThemeControlDark, 0.5), _messageInputThemeControlMidLerp); }); test('Lerp completely from dark to light', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControlDark, _messageInputThemeControl, 1), _messageInputThemeControl); }); @@ -39,33 +39,33 @@ void main() { }); } -final _messageInputThemeControl = MessageInputThemeData( +final _messageInputThemeControl = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: ColorTheme.light().accentPrimary, - actionButtonIdleColor: ColorTheme.light().textLowEmphasis, - expandButtonColor: ColorTheme.light().accentPrimary, - sendButtonColor: ColorTheme.light().accentPrimary, - sendButtonIdleColor: ColorTheme.light().disabled, - inputBackgroundColor: ColorTheme.light().barsBg, - inputTextStyle: TextTheme.light().body, + actionButtonColor: StreamColorTheme.light().accentPrimary, + actionButtonIdleColor: StreamColorTheme.light().textLowEmphasis, + expandButtonColor: StreamColorTheme.light().accentPrimary, + sendButtonColor: StreamColorTheme.light().accentPrimary, + sendButtonIdleColor: StreamColorTheme.light().disabled, + inputBackgroundColor: StreamColorTheme.light().barsBg, + inputTextStyle: StreamTextTheme.light().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + StreamColorTheme.light().disabled, + StreamColorTheme.light().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + StreamColorTheme.light().disabled, + StreamColorTheme.light().disabled, ], ), ); -final _messageInputThemeControlMidLerp = MessageInputThemeData( +final _messageInputThemeControlMidLerp = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), inputBackgroundColor: const Color(0xff87898b), @@ -95,28 +95,28 @@ final _messageInputThemeControlMidLerp = MessageInputThemeData( ), ); -final _messageInputThemeControlDark = MessageInputThemeData( +final _messageInputThemeControlDark = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: ColorTheme.dark().accentPrimary, - actionButtonIdleColor: ColorTheme.dark().textLowEmphasis, - expandButtonColor: ColorTheme.dark().accentPrimary, - sendButtonColor: ColorTheme.dark().accentPrimary, - sendButtonIdleColor: ColorTheme.dark().disabled, - inputBackgroundColor: ColorTheme.dark().barsBg, - inputTextStyle: TextTheme.dark().body, + actionButtonColor: StreamColorTheme.dark().accentPrimary, + actionButtonIdleColor: StreamColorTheme.dark().textLowEmphasis, + expandButtonColor: StreamColorTheme.dark().accentPrimary, + sendButtonColor: StreamColorTheme.dark().accentPrimary, + sendButtonIdleColor: StreamColorTheme.dark().disabled, + inputBackgroundColor: StreamColorTheme.dark().barsBg, + inputTextStyle: StreamTextTheme.dark().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.dark().disabled, - ColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.dark().disabled, - ColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, ], ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart index 94becc06d..264710eca 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart @@ -9,18 +9,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('MessageListViewThemeData copyWith, ==, hashCode basics', () { - expect(const MessageListViewThemeData(), - const MessageListViewThemeData().copyWith()); - expect(const MessageListViewThemeData().hashCode, - const MessageListViewThemeData().copyWith().hashCode); + expect(const StreamMessageListViewThemeData(), + const StreamMessageListViewThemeData().copyWith()); + expect(const StreamMessageListViewThemeData().hashCode, + const StreamMessageListViewThemeData().copyWith().hashCode); }); test( '''Light MessageListViewThemeData lerps completely to dark MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, - _messageListViewThemeDataControlDark, 1), + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, + 1), _messageListViewThemeDataControlDark); }); @@ -28,8 +30,10 @@ void main() { '''Light MessageListViewThemeData lerps halfway to dark MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, - _messageListViewThemeDataControlDark, 0.5), + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, + 0.5), _messageListViewThemeDataControlHalfLerp); }); @@ -37,7 +41,7 @@ void main() { '''Dark MessageListViewThemeData lerps completely to light MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp( + const StreamMessageListViewThemeData().lerp( _messageListViewThemeDataControlDark, _messageListViewThemeDataControl, 1), @@ -67,7 +71,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -75,7 +79,7 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControl.backgroundColor); }); @@ -97,7 +101,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -105,7 +109,7 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControlDark.backgroundColor); }); @@ -128,7 +132,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -136,25 +140,25 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundImage, _messageListViewThemeDataImage.backgroundImage); }); } -final _messageListViewThemeDataControl = MessageListViewThemeData( - backgroundColor: ColorTheme.light().barsBg, +final _messageListViewThemeDataControl = StreamMessageListViewThemeData( + backgroundColor: StreamColorTheme.light().barsBg, ); -const _messageListViewThemeDataControlHalfLerp = MessageListViewThemeData( +const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData( backgroundColor: Color(0xff87898b), ); -final _messageListViewThemeDataControlDark = MessageListViewThemeData( - backgroundColor: ColorTheme.dark().barsBg, +final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData( + backgroundColor: StreamColorTheme.dark().barsBg, ); -const _messageListViewThemeDataImage = MessageListViewThemeData( +const _messageListViewThemeDataImage = StreamMessageListViewThemeData( backgroundImage: DecorationImage( image: AssetImage('example/assets/background_doodle.png'), fit: BoxFit.cover, 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 69c63abea..a12b52f40 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'; @@ -6,17 +9,17 @@ import '../mocks.dart'; void main() { test('MessageSearchListViewThemeData copyWith, ==, hashCode basics', () { - expect(const MessageSearchListViewThemeData(), - const MessageSearchListViewThemeData().copyWith()); - expect(const MessageSearchListViewThemeData().hashCode, - const MessageSearchListViewThemeData().copyWith().hashCode); + expect(const StreamMessageSearchListViewThemeData(), + const StreamMessageSearchListViewThemeData().copyWith()); + expect(const StreamMessageSearchListViewThemeData().hashCode, + const StreamMessageSearchListViewThemeData().copyWith().hashCode); }); test( '''Light MessageSearchListViewThemeData lerps completely to dark MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControl, _messageSearchListViewThemeDataControlDark, 1), @@ -27,7 +30,7 @@ void main() { '''Light MessageSearchListViewThemeData lerps halfway to dark MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControl, _messageSearchListViewThemeDataControlDark, 0.5), @@ -38,7 +41,7 @@ void main() { '''Dark MessageSearchListViewThemeData lerps completely to light MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControlDark, _messageSearchListViewThemeDataControl, 1), @@ -78,7 +81,8 @@ void main() { ), ); - final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + final messageSearchListViewTheme = + StreamMessageSearchListViewTheme.of(_context); expect(messageSearchListViewTheme.backgroundColor, _messageSearchListViewThemeDataControl.backgroundColor); }); @@ -110,22 +114,24 @@ void main() { ), ); - final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + final messageSearchListViewTheme = + StreamMessageSearchListViewTheme.of(_context); expect(messageSearchListViewTheme.backgroundColor, _messageSearchListViewThemeDataControlDark.backgroundColor); }); } -final _messageSearchListViewThemeDataControl = MessageSearchListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _messageSearchListViewThemeDataControl = + StreamMessageSearchListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); const _messageSearchListViewThemeDataControlHalfLerp = - MessageSearchListViewThemeData( + StreamMessageSearchListViewThemeData( backgroundColor: Color(0xff818384), ); final _messageSearchListViewThemeDataControlDark = - MessageSearchListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, + StreamMessageSearchListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart index 502be84a1..3b6c3a6fa 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -4,16 +4,17 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('MessageThemeData copyWith, ==, hashCode basics', () { - expect(const MessageThemeData(), const MessageThemeData().copyWith()); - expect(const MessageThemeData().hashCode, - const MessageThemeData().copyWith().hashCode); + expect(const StreamMessageThemeData(), + const StreamMessageThemeData().copyWith()); + expect(const StreamMessageThemeData().hashCode, + const StreamMessageThemeData().copyWith().hashCode); }); group('MessageThemeData lerps', () { test('''Light MessageThemeData lerps completely to dark MessageThemeData''', () { expect( - const MessageThemeData() + const StreamMessageThemeData() .lerp(_messageThemeControl, _messageThemeControlDark, 1), _messageThemeControlDark); }); @@ -21,7 +22,7 @@ void main() { test('''Dark MessageThemeData lerps completely to light MessageThemeData''', () { expect( - const MessageThemeData() + const StreamMessageThemeData() .lerp(_messageThemeControlDark, _messageThemeControl, 1), _messageThemeControl); }); @@ -33,23 +34,23 @@ void main() { }); } -final _messageThemeControl = MessageThemeData( - messageAuthorStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textLowEmphasis, +final _messageThemeControl = StreamMessageThemeData( + messageAuthorStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textLowEmphasis, ), - messageTextStyle: TextTheme.light().body, - createdAtStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textLowEmphasis, + messageTextStyle: StreamTextTheme.light().body, + createdAtStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textLowEmphasis, ), - repliesStyle: TextTheme.light().footnoteBold.copyWith( - color: ColorTheme.light().accentPrimary, + repliesStyle: StreamTextTheme.light().footnoteBold.copyWith( + color: StreamColorTheme.light().accentPrimary, ), - messageBackgroundColor: ColorTheme.light().disabled, - reactionsBackgroundColor: ColorTheme.light().barsBg, - reactionsBorderColor: ColorTheme.light().borders, - reactionsMaskColor: ColorTheme.light().appBg, - messageBorderColor: ColorTheme.light().disabled, - avatarTheme: AvatarThemeData( + messageBackgroundColor: StreamColorTheme.light().disabled, + reactionsBackgroundColor: StreamColorTheme.light().barsBg, + reactionsBorderColor: StreamColorTheme.light().borders, + reactionsMaskColor: StreamColorTheme.light().appBg, + messageBorderColor: StreamColorTheme.light().disabled, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -57,28 +58,28 @@ final _messageThemeControl = MessageThemeData( ), ), messageLinksStyle: TextStyle( - color: ColorTheme.light().accentPrimary, + color: StreamColorTheme.light().accentPrimary, ), - linkBackgroundColor: ColorTheme.light().linkBg, + linkBackgroundColor: StreamColorTheme.light().linkBg, ); -final _messageThemeControlDark = MessageThemeData( - messageAuthorStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textLowEmphasis, +final _messageThemeControlDark = StreamMessageThemeData( + messageAuthorStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textLowEmphasis, ), - messageTextStyle: TextTheme.dark().body, - createdAtStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textLowEmphasis, + messageTextStyle: StreamTextTheme.dark().body, + createdAtStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textLowEmphasis, ), - repliesStyle: TextTheme.dark().footnoteBold.copyWith( - color: ColorTheme.dark().accentPrimary, + repliesStyle: StreamTextTheme.dark().footnoteBold.copyWith( + color: StreamColorTheme.dark().accentPrimary, ), - messageBackgroundColor: ColorTheme.dark().disabled, - reactionsBackgroundColor: ColorTheme.dark().barsBg, - reactionsBorderColor: ColorTheme.dark().borders, - reactionsMaskColor: ColorTheme.dark().appBg, - messageBorderColor: ColorTheme.dark().disabled, - avatarTheme: AvatarThemeData( + messageBackgroundColor: StreamColorTheme.dark().disabled, + reactionsBackgroundColor: StreamColorTheme.dark().barsBg, + reactionsBorderColor: StreamColorTheme.dark().borders, + reactionsMaskColor: StreamColorTheme.dark().appBg, + messageBorderColor: StreamColorTheme.dark().disabled, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -86,7 +87,7 @@ final _messageThemeControlDark = MessageThemeData( ), ), messageLinksStyle: TextStyle( - color: ColorTheme.dark().accentPrimary, + color: StreamColorTheme.dark().accentPrimary, ), - linkBackgroundColor: ColorTheme.dark().linkBg, + linkBackgroundColor: StreamColorTheme.dark().linkBg, ); 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 3aa207441..7b3550c8a 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'; @@ -6,15 +9,15 @@ import '../mocks.dart'; void main() { test('UserListViewThemeData copyWith, ==, hashCode basics', () { - expect(const UserListViewThemeData(), - const UserListViewThemeData().copyWith()); + expect(const StreamUserListViewThemeData(), + const StreamUserListViewThemeData().copyWith()); }); test( '''Light UserListViewThemeData lerps completely to dark UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControl, + const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, _userListViewThemeDataControlDark, 1), _userListViewThemeDataControlDark); }); @@ -23,7 +26,7 @@ void main() { '''Light UserListViewThemeData lerps halfway to dark UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControl, + const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, _userListViewThemeDataControlDark, 0.5), _userListViewThemeDataControlHalfLerp); }); @@ -32,8 +35,10 @@ void main() { '''Dark UserListViewThemeData lerps completely to light UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControlDark, - _userListViewThemeDataControl, 1), + const StreamUserListViewThemeData().lerp( + _userListViewThemeDataControlDark, + _userListViewThemeDataControl, + 1), _userListViewThemeDataControl); }); @@ -66,7 +71,7 @@ void main() { ), ); - final userListViewTheme = UserListViewTheme.of(_context); + final userListViewTheme = StreamUserListViewTheme.of(_context); expect(userListViewTheme.backgroundColor, _userListViewThemeDataControl.backgroundColor); }); @@ -95,20 +100,20 @@ void main() { ), ); - final userListViewTheme = UserListViewTheme.of(_context); + final userListViewTheme = StreamUserListViewTheme.of(_context); expect(userListViewTheme.backgroundColor, _userListViewThemeDataControlDark.backgroundColor); }); } -final _userListViewThemeDataControl = UserListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _userListViewThemeDataControl = StreamUserListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); -const _userListViewThemeDataControlHalfLerp = UserListViewThemeData( +const _userListViewThemeDataControlHalfLerp = StreamUserListViewThemeData( backgroundColor: Color(0xff818384), ); -final _userListViewThemeDataControlDark = UserListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, +final _userListViewThemeDataControlDark = StreamUserListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); 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 babe43180..e347803e6 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'; @@ -51,7 +53,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ThreadHeader( + body: StreamThreadHeader( parent: Message(), ), ), @@ -61,7 +63,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('with '), findsOneWidget); - expect(find.byType(ChannelName), findsOneWidget); + expect(find.byType(StreamChannelName), findsOneWidget); expect(find.byType(StreamBackButton), findsOneWidget); expect(find.text('Thread Reply'), findsOneWidget); }, @@ -112,7 +114,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ThreadHeader( + body: StreamThreadHeader( parent: Message(), subtitle: const Text('subtitle'), leading: const Text('leading'), diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 8d790266d..27b3f977f 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -63,19 +63,23 @@ void main() { Event(type: EventType.typingStart), })); + const typingKey = Key('typing'); + await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, child: StreamChannel( channel: channel, child: const Scaffold( - body: TypingIndicator(), + body: StreamTypingIndicator( + key: typingKey, + ), ), ), ), )); - expect(find.byKey(const Key('typings')), findsOneWidget); + expect(find.byKey(typingKey), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart index 003590891..a470fe747 100644 --- a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart @@ -37,7 +37,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: UnreadIndicator(), + body: StreamUnreadIndicator(), ), ), ), @@ -75,7 +75,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: UnreadIndicator( + body: StreamUnreadIndicator( cid: channel.cid, ), ), @@ -115,7 +115,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: UnreadIndicator( + body: StreamUnreadIndicator( cid: channel.cid, ), ), diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index a2a2c4d8d..9245efd2f 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,23 @@ +## 4.0.0 + +- Deprecated `UsersBloc` in favor of `StreamUserListController` to control the user list. +- Deprecated `MessageSearchBloc` in favor of `StreamMessageSearchListController` to control the user list. + +## 4.0.0-beta.2 + +- Updated `stream_chat` dependency to [`4.0.0-beta.2`](https://pub.dev/packages/stream_chat/changelog). + +## 4.0.0-beta.0 + +✅ Added + +- Added `MessageInputController` to hold `Message` related data. +- Deprecated old widgets in favor of Stream-prefixed ones. +- Deprecated `ChannelsBloc` in favor of `StreamChannelListController` to control the channel list. +- Added `MessageTextFieldController` to be used with the new `StreamTextField` ui widget. + +- Updated `stream_chat` dependency to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat/changelog). + ## 3.6.1 - Updated `stream_chat` dependency to [`3.6.1`](https://pub.dev/packages/stream_chat/changelog). 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 000000000..d56da71f9 --- /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 d2eb734aa..dfabc8451 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 000000000..3db14bb53 --- /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/ios/Flutter/AppFrameworkInfo.plist b/packages/stream_chat_flutter_core/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78a..f2872cf47 100644 --- a/packages/stream_chat_flutter_core/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/stream_chat_flutter_core/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.pbxproj index 26de6f3b5..b55c0437c 100644 --- a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.pbxproj @@ -139,6 +139,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + EDC4CAB19281E65D2B18E888 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -155,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -247,6 +248,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + EDC4CAB19281E65D2B18E888 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..3db53b6e1 100644 --- a/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/stream_chat_flutter_core/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ 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!); - } + 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( + /// We're using the channels length when there are no more + /// pages to load and there are no errors with pagination. + /// In case we need to show a loading indicator or and error + /// tile we're increasing the count by 1. + itemCount: (nextPageKey != null || error != null) + ? channels.length + 1 + : channels.length, + itemBuilder: (BuildContext context, int index) { + if (index == channels.length) { + if (error != null) { + return TextButton( + onPressed: () { + channelListController.retry(); + }, + child: Text(error.message), + ); + } + return CircularProgressIndicator(); + } - 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(), + 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', + ), + ), + ); + }, ), ); } @@ -175,20 +205,20 @@ class MessageScreen extends StatefulWidget { } class _MessageScreenState extends State { - late final TextEditingController _controller; + final StreamMessageInputController messageInputController = + StreamMessageInputController(); late final ScrollController _scrollController; final messageListController = MessageListController(); @override void initState() { super.initState(); - _controller = TextEditingController(); _scrollController = ScrollController(); } @override void dispose() { - _controller.dispose(); + messageInputController.dispose(); _scrollController.dispose(); super.dispose(); } @@ -289,7 +319,8 @@ class _MessageScreenState extends State { children: [ Expanded( child: TextField( - controller: _controller, + controller: messageInputController.textEditingController, + onChanged: (s) => messageInputController.text = s, decoration: const InputDecoration( hintText: 'Enter your message', ), @@ -301,12 +332,13 @@ class _MessageScreenState extends State { clipBehavior: Clip.hardEdge, child: InkWell( onTap: () async { - if (_controller.value.text.isNotEmpty) { + if (messageInputController.message.text?.isNotEmpty == + true) { await channel.sendMessage( - Message(text: _controller.value.text), + messageInputController.message, ); + messageInputController.clear(); if (mounted) { - _controller.clear(); _updateList(); } } 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 000000000..8aaa46ac1 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 000000000..b749bfef0 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 000000000..88cfd48df 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 000000000..eb9b4d76e 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 000000000..d69c56691 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 000000000..b6b9dd234 --- /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 000000000..096edf8fe --- /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 000000000..d492d0d98 --- /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 000000000..1633297a0 --- /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 000000000..b2e4bd8d6 --- /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 000000000..8083d749b --- /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 000000000..dc139d85a --- /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 000000000..ba4a21757 --- /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 000000000..de2d8916b --- /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 000000000..79f0da9ac --- /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 000000000..b43b9095e --- /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 000000000..6da0652f0 --- /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 000000000..bcb57b0e2 --- /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 000000000..66a65d1e4 --- /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 000000000..c04e20caf 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 000000000..c977c4a42 --- /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 000000000..d19bdbbcc --- /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 000000000..3879d5475 --- /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 000000000..c10f08dc7 --- /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 000000000..17ba43112 --- /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 d807091c9..51be28e71 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 fa19a90f2..2f8838b5d 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'; @@ -16,6 +18,7 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// using Flutter's [BuildContext]. /// /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ +@Deprecated("Use 'StreamChannelListController' instead") class ChannelsBloc extends StatefulWidget { /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and /// not null. 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 96a761c1f..0c302c728 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'; @@ -11,6 +13,7 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// using Flutter's [BuildContext]. /// /// API docs: https://getstream.io/chat/docs/flutter-dart/send_message/ +@Deprecated("Use 'StreamMessageSearchListController' instead") class MessageSearchBloc extends StatefulWidget { /// Instantiate a new MessageSearchBloc const MessageSearchBloc({ 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 565743f2b..e86d020b2 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/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart new file mode 100644 index 000000000..0f9f75c65 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +/// A function that takes a [BuildContext] and returns a [TextStyle]. +typedef TextStyleBuilder = TextStyle? Function( + BuildContext context, + String text, +); + +/// Controller for the [StreamTextField] widget. +class MessageTextFieldController extends TextEditingController { + /// Returns a new MessageTextFieldController + MessageTextFieldController({ + String? text, + this.textPatternStyle, + }) : super(text: text); + + /// Returns a new MessageTextFieldController with the given text [value]. + MessageTextFieldController.fromValue( + TextEditingValue? value, { + this.textPatternStyle, + }) : super.fromValue(value); + + /// A map of style to apply to the text matching the RegExp patterns. + final Map? textPatternStyle; + + /// Builds a [TextSpan] from the current text, + /// highlighting the matches for [textPatternStyle]. + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + required bool withComposing, + }) { + final pattern = textPatternStyle; + if (pattern == null || pattern.isEmpty) { + return super.buildTextSpan( + context: context, + style: style, + withComposing: withComposing, + ); + } + + return TextSpan(text: text, style: style).splitMapJoin( + RegExp(pattern.keys.map((it) => it.pattern).join('|')), + onMatch: (match) { + final text = match[0]!; + final key = pattern.keys.firstWhere((it) => it.hasMatch(text)); + return TextSpan( + text: text, + style: pattern[key]?.call( + context, + text, + ), + ); + }, + ); + } +} + +extension _TextSpanX on TextSpan { + TextSpan splitMapJoin( + Pattern pattern, { + TextSpan Function(Match)? onMatch, + TextSpan Function(TextSpan)? onNonMatch, + }) { + final children = []; + + toPlainText().splitMapJoin( + pattern, + onMatch: (match) { + final span = TextSpan(text: match.group(0), style: style); + final updated = onMatch?.call(match); + children.add(updated ?? span); + return span.toPlainText(); + }, + onNonMatch: (text) { + final span = TextSpan(text: text, style: style); + final updatedSpan = onNonMatch?.call(span); + children.add(updatedSpan ?? span); + return span.toPlainText(); + }, + ); + + return TextSpan(style: style, children: children); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart new file mode 100644 index 000000000..9525fea40 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart @@ -0,0 +1,136 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:stream_chat/stream_chat.dart' show StreamChatError; + +part 'paged_value_notifier.freezed.dart'; + +/// Default initial page size multiplier. +const defaultInitialPagedLimitMultiplier = 3; + +/// Value listenable for paged data. +typedef PagedValueListenableBuilder + = ValueListenableBuilder>; + +/// A [PagedValueNotifier] that uses a [PagedListenable] to load data. +/// +/// This class is useful when you need to load data from a server +/// using a [PagedListenable] and want to keep the UI-driven refresh +/// signals in the [PagedListenable]. +/// +/// [PagedValueNotifier] is a [ValueNotifier] that emits a [PagedValue] +/// whenever the data is loaded or an error occurs. +abstract class PagedValueNotifier + extends ValueNotifier> { + /// Creates a [PagedValueNotifier] + PagedValueNotifier(this._initialValue) : super(_initialValue); + + /// Stores initialValue in case we need to call [refresh]. + final PagedValue _initialValue; + + /// Returns the currently loaded items + List get currentItems => value.asSuccess.items; + + /// Appends [newItems] to the previously loaded ones and replaces + /// the next page's key. + void appendPage({ + required List newItems, + required Key nextPageKey, + }) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems, nextPageKey: nextPageKey); + } + + /// Appends [newItems] to the previously loaded ones and sets the next page + /// key to `null`. + void appendLastPage(List newItems) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems); + } + + /// Retry any failed load requests. + /// + /// Unlike [refresh], this does not resets the whole [value], + /// it only retries the last failed load request. + Future retry() { + final lastValue = value.asSuccess; + assert(lastValue.hasError, ''); + + final nextPageKey = lastValue.nextPageKey; + // resetting the error + value = lastValue.copyWith(error: null); + // ignore: null_check_on_nullable_type_parameter + return loadMore(nextPageKey!); + } + + /// Refresh the data presented by this [PagedValueNotifier]. + /// + /// Resets the [value] to the initial value in case [resetValue] is true. + /// + /// Note: This API is intended for UI-driven refresh signals, + /// such as swipe-to-refresh. + Future refresh({bool resetValue = true}) { + if (resetValue) value = _initialValue; + return doInitialLoad(); + } + + /// Load initial data from the server. + Future doInitialLoad(); + + /// Load more data from the server using [nextPageKey]. + Future loadMore(Key nextPageKey); +} + +/// Paged value that can be used with [PagedValueNotifier]. +@freezed +abstract class PagedValue with _$PagedValue { + /// Represents the success state of the [PagedValue] + // @Assert( + // 'nextPageKey != null', + // 'Cannot set an error if all the pages are already fetched', + // ) + const factory PagedValue({ + /// List with all items loaded so far. + required List items, + + /// The key for the next page to be fetched. + Key? nextPageKey, + + /// The current error, if any. + StreamChatError? error, + }) = Success; + + const PagedValue._(); + + /// Represents the loading state of the [PagedValue]. + const factory PagedValue.loading() = Loading; + + /// Represents the error state of the [PagedValue]. + const factory PagedValue.error(StreamChatError error) = Error; + + /// Returns `true` if the [PagedValue] is [Success]. + bool get isSuccess => this is Success; + + /// Returns the [PagedValue] as [Success]. + Success get asSuccess { + assert( + isSuccess, + 'Cannot get asSuccess if the PagedValue is not in the Success state', + ); + return this as Success; + } + + /// Returns `true` if the [PagedValue] is [Success] + /// and has more items to load. + bool get hasNextPage => asSuccess.nextPageKey != null; + + /// Returns `true` if the [PagedValue] is [Success] and has an error. + bool get hasError => asSuccess.error != null; + + /// + int get itemCount { + final count = asSuccess.items.length; + if (hasNextPage || hasError) return count + 1; + return count; + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart new file mode 100644 index 000000000..a7ea0ea3c --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart @@ -0,0 +1,589 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// 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 'paged_value_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +class _$PagedValueTearOff { + const _$PagedValueTearOff(); + + Success call( + {required List items, Key? nextPageKey, StreamChatError? error}) { + return Success( + items: items, + nextPageKey: nextPageKey, + error: error, + ); + } + + Loading loading() { + return Loading(); + } + + Error error(StreamChatError error) { + return Error( + error, + ); + } +} + +/// @nodoc +const $PagedValue = _$PagedValueTearOff(); + +/// @nodoc +mixin _$PagedValue { + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PagedValueCopyWith { + factory $PagedValueCopyWith(PagedValue value, + $Res Function(PagedValue) then) = + _$PagedValueCopyWithImpl; +} + +/// @nodoc +class _$PagedValueCopyWithImpl + implements $PagedValueCopyWith { + _$PagedValueCopyWithImpl(this._value, this._then); + + final PagedValue _value; + // ignore: unused_field + final $Res Function(PagedValue) _then; +} + +/// @nodoc +abstract class $SuccessCopyWith { + factory $SuccessCopyWith( + Success value, $Res Function(Success) then) = + _$SuccessCopyWithImpl; + $Res call({List items, Key? nextPageKey, StreamChatError? error}); +} + +/// @nodoc +class _$SuccessCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $SuccessCopyWith { + _$SuccessCopyWithImpl( + Success _value, $Res Function(Success) _then) + : super(_value, (v) => _then(v as Success)); + + @override + Success get _value => super._value as Success; + + @override + $Res call({ + Object? items = freezed, + Object? nextPageKey = freezed, + Object? error = freezed, + }) { + return _then(Success( + items: items == freezed + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + nextPageKey: nextPageKey == freezed + ? _value.nextPageKey + : nextPageKey // ignore: cast_nullable_to_non_nullable + as Key?, + error: error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError?, + )); + } +} + +/// @nodoc + +class _$Success extends Success + with DiagnosticableTreeMixin { + const _$Success({required this.items, this.nextPageKey, this.error}) + : super._(); + + @override + + /// List with all items loaded so far. + final List items; + @override + + /// The key for the next page to be fetched. + final Key? nextPageKey; + @override + + /// The current error, if any. + final StreamChatError? error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>(items: $items, nextPageKey: $nextPageKey, error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>')) + ..add(DiagnosticsProperty('items', items)) + ..add(DiagnosticsProperty('nextPageKey', nextPageKey)) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Success && + const DeepCollectionEquality().equals(other.items, items) && + const DeepCollectionEquality() + .equals(other.nextPageKey, nextPageKey) && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(items), + const DeepCollectionEquality().hash(nextPageKey), + const DeepCollectionEquality().hash(error)); + + @JsonKey(ignore: true) + @override + $SuccessCopyWith> get copyWith => + _$SuccessCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return $default(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return $default?.call(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(items, nextPageKey, this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class Success extends PagedValue { + const factory Success( + {required List items, + Key? nextPageKey, + StreamChatError? error}) = _$Success; + const Success._() : super._(); + + /// List with all items loaded so far. + List get items; + + /// The key for the next page to be fetched. + Key? get nextPageKey; + + /// The current error, if any. + StreamChatError? get error; + @JsonKey(ignore: true) + $SuccessCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadingCopyWith { + factory $LoadingCopyWith( + Loading value, $Res Function(Loading) then) = + _$LoadingCopyWithImpl; +} + +/// @nodoc +class _$LoadingCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $LoadingCopyWith { + _$LoadingCopyWithImpl( + Loading _value, $Res Function(Loading) _then) + : super(_value, (v) => _then(v as Loading)); + + @override + Loading get _value => super._value as Loading; +} + +/// @nodoc + +class _$Loading extends Loading + with DiagnosticableTreeMixin { + const _$Loading() : super._(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.loading()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is Loading); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class Loading extends PagedValue { + const factory Loading() = _$Loading; + const Loading._() : super._(); +} + +/// @nodoc +abstract class $ErrorCopyWith { + factory $ErrorCopyWith( + Error value, $Res Function(Error) then) = + _$ErrorCopyWithImpl; + $Res call({StreamChatError error}); +} + +/// @nodoc +class _$ErrorCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $ErrorCopyWith { + _$ErrorCopyWithImpl( + Error _value, $Res Function(Error) _then) + : super(_value, (v) => _then(v as Error)); + + @override + Error get _value => super._value as Error; + + @override + $Res call({ + Object? error = freezed, + }) { + return _then(Error( + error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError, + )); + } +} + +/// @nodoc + +class _$Error extends Error + with DiagnosticableTreeMixin { + const _$Error(this.error) : super._(); + + @override + final StreamChatError error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.error(error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.error')) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Error && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(error)); + + @JsonKey(ignore: true) + @override + $ErrorCopyWith> get copyWith => + _$ErrorCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return error(this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return error?.call(this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class Error extends PagedValue { + const factory Error(StreamChatError error) = _$Error; + const Error._() : super._(); + + StreamChatError get error; + @JsonKey(ignore: true) + $ErrorCopyWith> get copyWith => + throw _privateConstructorUsedError; +} 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 new file mode 100644 index 000000000..00d96f562 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart @@ -0,0 +1,706 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +/// Signature for a function that creates a widget for a given index, e.g., in a +/// [PagedValueListView] and [PagedValueGridView]. +typedef PagedValueScrollViewIndexedWidgetBuilder = Widget Function( + BuildContext context, + List values, + int index, +); + +/// Signature for the item builder that creates the children of the +/// [PagedValueListView] and [PagedValueGridView]. +typedef PagedValueScrollViewLoadMoreErrorBuilder = Widget Function( + BuildContext context, + StreamChatError error, +); + +/// A [ListView] that loads more pages when the user scrolls to the end of the +/// list. +/// +/// Use [loadMoreTriggerIndex] to set the index of the item that triggers the +/// loading of the next page. +class PagedValueListView extends StatefulWidget { + /// Creates a new instance of [PagedValueListView] widget. + const PagedValueListView({ + Key? key, + required this.controller, + required this.itemBuilder, + required this.separatorBuilder, + required this.emptyBuilder, + required this.loadMoreErrorBuilder, + required this.loadMoreIndicatorBuilder, + required this.loadingBuilder, + required 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.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }) : super(key: key); + + /// The [PagedValueNotifier] used to control the list of items. + final PagedValueNotifier controller; + + /// A builder that is called to build items in the [ListView]. + /// + /// The `value` parameter is the [V] at this position in the list. + final PagedValueScrollViewIndexedWidgetBuilder 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. + final WidgetBuilder emptyBuilder; + + /// A builder that is called to build the load more error state of the list. + final PagedValueScrollViewLoadMoreErrorBuilder loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the list. + final WidgetBuilder loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder loadingBuilder; + + /// A builder that is called to build the error state of the list. + 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; + + /// 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; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, 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 [scrollController] is null. + final bool? primary; + + /// {@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; + + /// {@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; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@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 + State> createState() => + _PagedValueListViewState(); +} + +class _PagedValueListViewState extends State> { + PagedValueNotifier get _controller => widget.controller; + + // Avoids duplicate requests on rebuilds. + bool _hasRequestedNextPage = false; + + @override + void initState() { + super.initState(); + _controller.doInitialLoad(); + } + + @override + void didUpdateWidget(covariant PagedValueListView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller != oldWidget.controller) { + // reset duplicate requests flag + _hasRequestedNextPage = false; + _controller.doInitialLoad(); + } + } + + @override + Widget build(BuildContext context) => PagedValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, _) => value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return ListView.separated( + scrollDirection: widget.scrollDirection, + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + shrinkWrap: widget.shrinkWrap, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + addSemanticIndexes: widget.addSemanticIndexes, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + dragStartBehavior: widget.dragStartBehavior, + cacheExtent: widget.cacheExtent, + clipBehavior: widget.clipBehavior, + itemCount: value.itemCount, + separatorBuilder: (context, index) => + widget.separatorBuilder(context, items, index), + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = + items.length - widget.loadMoreTriggerIndex; + final isBuildingTriggerIndexItem = + index == newPageRequestTriggerIndex; + if (nextPageKey != null && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (error == null) { + await _controller.loadMore(nextPageKey); + } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } + + if (index == items.length) { + if (error != null) { + return widget.loadMoreErrorBuilder(context, error); + } + return widget.loadMoreIndicatorBuilder(context); + } + + return widget.itemBuilder(context, items, index); + }, + ); + }, + loading: () => widget.loadingBuilder(context), + error: (error) => widget.errorBuilder(context, error), + ), + ); +} + +/// A [GridView] that loads more pages when the user scrolls to the end of the +/// grid. +/// +/// Use [loadMoreTriggerIndex] to set the index of the item that triggers the +/// loading of the next page. +class PagedValueGridView extends StatefulWidget { + /// Creates a new instance of [PagedValueGridView] widget. + const PagedValueGridView({ + Key? key, + required this.controller, + required this.gridDelegate, + required this.itemBuilder, + required this.emptyBuilder, + required this.loadMoreErrorBuilder, + required this.loadMoreIndicatorBuilder, + required this.loadingBuilder, + required 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 [PagedValueNotifier] used to control the list of items. + final PagedValueNotifier 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 [V] at this position in the list. + final PagedValueScrollViewIndexedWidgetBuilder itemBuilder; + + /// A builder that is called to build the empty state of the list. + final WidgetBuilder emptyBuilder; + + /// A builder that is called to build the load more error state of the list. + final PagedValueScrollViewLoadMoreErrorBuilder loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the list. + final WidgetBuilder loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder loadingBuilder; + + /// A builder that is called to build the error state of the list. + 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 + State> createState() => + _PagedValueGridViewState(); +} + +class _PagedValueGridViewState extends State> { + PagedValueNotifier get _controller => widget.controller; + + // Avoids duplicate requests on rebuilds. + bool _hasRequestedNextPage = false; + + @override + void initState() { + super.initState(); + _controller.doInitialLoad(); + } + + @override + void didUpdateWidget(covariant PagedValueGridView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller != oldWidget.controller) { + // reset duplicate requests flag + _hasRequestedNextPage = false; + _controller.doInitialLoad(); + } + } + + @override + Widget build(BuildContext context) => PagedValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, _) => value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return GridView.builder( + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + padding: widget.padding, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + addSemanticIndexes: widget.addSemanticIndexes, + cacheExtent: widget.cacheExtent, + semanticChildCount: widget.semanticChildCount, + dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + clipBehavior: widget.clipBehavior, + itemCount: value.itemCount, + gridDelegate: widget.gridDelegate, + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = + items.length - widget.loadMoreTriggerIndex; + final isBuildingTriggerIndexItem = + index == newPageRequestTriggerIndex; + if (nextPageKey != null && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (error == null) { + await _controller.loadMore(nextPageKey); + } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } + + if (index == items.length) { + if (error != null) { + return widget.loadMoreErrorBuilder(context, error); + } + return widget.loadMoreIndicatorBuilder(context); + } + + return widget.itemBuilder(context, items, index); + }, + ); + }, + loading: () => widget.loadingBuilder(context), + error: (error) => widget.errorBuilder(context, error), + ), + ); +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index bcfb613eb..478227916 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -106,7 +106,9 @@ class StreamChannelState extends State { limit: limit, preferOffline: preferOffline, ); - if (state.messages.isEmpty || state.messages.length < limit) { + if (state.messages == null || + state.messages!.isEmpty || + state.messages!.length < limit) { _topPaginationEnded = true; } _queryTopMessagesController.safeAdd(false); @@ -137,7 +139,9 @@ class StreamChannelState extends State { limit: limit, preferOffline: preferOffline, ); - if (state.messages.isEmpty || state.messages.length < limit) { + if (state.messages == null || + state.messages!.isEmpty || + state.messages!.length < limit) { _bottomPaginationEnded = true; } _queryBottomMessagesController.safeAdd(false); @@ -299,7 +303,9 @@ class StreamChannelState extends State { ), preferOffline: preferOffline, ); - if (state.messages.isEmpty || state.messages.length < limit) { + if (state.messages == null || + state.messages!.isEmpty || + state.messages!.length < limit) { channel.state?.isUpToDate = true; } return state; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart new file mode 100644 index 000000000..9fe5594c9 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -0,0 +1,281 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +import 'package:stream_chat_flutter_core/src/stream_channel_list_event_handler.dart'; + +/// The default channel page limit to load. +const defaultChannelPagedLimit = 10; + +const _kDefaultBackendPaginationLimit = 30; + +/// A controller for a Channel list. +/// +/// This class lets you perform tasks such as: +/// * Load initial data. +/// * Use channel events handlers. +/// * Load more data using [loadMore]. +/// * Replace the previously loaded channels. +/// * Return/Create a new channel and start watching it. +/// * Pause and Resume all subscriptions added to this composite. +class StreamChannelListController extends PagedValueNotifier { + /// Creates a Stream channel list controller. + /// + /// * `client` is the Stream chat client to use for the channels list. + /// + /// * `channelEventHandlers` is the channel events to use for the channels + /// list. This class can be mixed in or extended to create custom overrides. + /// See [StreamChannelListEventHandler] for advice. + /// + /// * `filter` is the query filters to use. + /// + /// * `sort` is the sorting used for the channels matching the filters. + /// + /// * `presence` sets whether you'll receive user presence updates via the + /// websocket events. + /// + /// * `limit` is the limit to apply to the channel list. + /// + /// * `messageLimit` is the number of messages to fetch in each channel. + /// + /// * `memberLimit` is the number of members to fetch in each channel. + StreamChannelListController({ + required this.client, + StreamChannelListEventHandler? eventHandler, + this.filter, + this.sort, + this.presence = true, + this.limit = defaultChannelPagedLimit, + this.messageLimit, + this.memberLimit, + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), + super(const PagedValue.loading()); + + /// Creates a [StreamChannelListController] from the passed [value]. + StreamChannelListController.fromValue( + PagedValue value, { + required this.client, + StreamChannelListEventHandler? eventHandler, + this.filter, + this.sort, + this.presence = true, + this.limit = defaultChannelPagedLimit, + this.messageLimit, + this.memberLimit, + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), + super(value); + + /// The client to use for the channels list. + final StreamChatClient client; + + /// The channel event handlers to use for the channels list. + final StreamChannelListEventHandler _eventHandler; + + /// The query filters to use. + /// + /// You can query on any of the custom fields you've defined on the [Channel]. + /// + /// You can also filter other built-in channel fields. + final Filter? filter; + + /// The sorting used for the channels matching the filters. + /// + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// + /// You can sort based on last_updated, last_message_at, updated_at, + /// created_at or member_count. + /// + /// Direction can be ascending or descending. + final List>? sort; + + /// If true you’ll receive user presence updates via the websocket events + final bool presence; + + /// The limit to apply to the channel list. The default is set to + /// [defaultChannelPagedLimit]. + final int limit; + + /// Number of messages to fetch in each channel. + final int? messageLimit; + + /// Number of members to fetch in each channel. + final int? memberLimit; + + @override + Future doInitialLoad() async { + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + presence: presence, + paginationParams: PaginationParams(limit: limit), + )) { + final nextKey = channels.length < limit ? null : channels.length; + value = PagedValue( + items: channels, + nextPageKey: nextKey, + ); + } + // start listening to events + _subscribeToChannelListEvents(); + } on StreamChatError catch (error) { + value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); + } + } + + @override + Future loadMore(int nextPageKey) async { + final previousValue = value.asSuccess; + + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + presence: presence, + paginationParams: PaginationParams(limit: limit, offset: nextPageKey), + )) { + final previousItems = previousValue.items; + final newItems = previousItems + channels; + final nextKey = channels.length < limit ? null : newItems.length; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); + } + } + + /// Replaces the previously loaded channels with [channels] and updates + /// the nextPageKey. + set channels(List channels) { + value = PagedValue( + items: channels, + nextPageKey: channels.length, + ); + } + + /// Returns/Creates a new Channel and starts watching it. + Future getChannel({ + required String id, + required String type, + }) async { + final channel = client.channel(type, id: id); + await channel.watch(); + return channel; + } + + /// Leaves the [channel] and updates the list. + Future leaveChannel(Channel channel) async { + final user = client.state.currentUser; + assert(user != null, 'You must be logged in to leave a channel.'); + await channel.removeMembers([user!.id]); + } + + /// Deletes the [channel] and updates the list. + Future deleteChannel(Channel channel) async { + await channel.delete(); + } + + /// Mutes the [channel] and updates the list. + Future muteChannel(Channel channel) async { + await channel.mute(); + } + + /// Un-mutes the [channel] and updates the list. + Future unmuteChannel(Channel channel) async { + await channel.unmute(); + } + + /// Event listener, which can be set in order to listen + /// [client] web-socket events. + /// + /// Return `true` if the event is handled. Return `false` to + /// allow the event to be handled internally. + bool Function(Event event)? eventListener; + + StreamSubscription? _channelEventSubscription; + + // Subscribes to the channel list events. + void _subscribeToChannelListEvents() { + if (_channelEventSubscription != null) { + _unsubscribeFromChannelListEvents(); + } + + _channelEventSubscription = + client.on().skip(1) // Skipping the last emitted event. + // We only need to handle the latest events. + .listen((event) { + // Returns early if the event is already handled by the listener. + if (eventListener?.call(event) ?? false) return; + + final eventType = event.type; + if (eventType == EventType.channelDeleted) { + _eventHandler.onChannelDeleted(event, this); + } else if (eventType == EventType.channelHidden) { + _eventHandler.onChannelHidden(event, this); + } else if (eventType == EventType.channelTruncated) { + _eventHandler.onChannelTruncated(event, this); + } else if (eventType == EventType.channelUpdated) { + _eventHandler.onChannelUpdated(event, this); + } else if (eventType == EventType.channelVisible) { + _eventHandler.onChannelVisible(event, this); + } else if (eventType == EventType.connectionRecovered) { + _eventHandler.onConnectionRecovered(event, this); + } else if (eventType == EventType.messageNew) { + _eventHandler.onMessageNew(event, this); + } else if (eventType == EventType.notificationAddedToChannel) { + _eventHandler.onNotificationAddedToChannel(event, this); + } else if (eventType == EventType.notificationMessageNew) { + _eventHandler.onNotificationMessageNew(event, this); + } else if (eventType == EventType.notificationRemovedFromChannel) { + _eventHandler.onNotificationRemovedFromChannel(event, this); + } else if (eventType == 'user.presence.changed' || + eventType == EventType.userUpdated) { + _eventHandler.onUserPresenceChanged(event, this); + } + }); + } + + // Unsubscribes from all channel list events. + void _unsubscribeFromChannelListEvents() { + if (_channelEventSubscription != null) { + _channelEventSubscription!.cancel(); + _channelEventSubscription = null; + } + } + + /// Pauses all subscriptions added to this composite. + void pauseEventsSubscription([Future? resumeSignal]) { + _channelEventSubscription?.pause(resumeSignal); + } + + /// Resumes all subscriptions added to this composite. + void resumeEventsSubscription() { + _channelEventSubscription?.resume(); + } + + @override + void dispose() { + _unsubscribeFromChannelListEvents(); + super.dispose(); + } +} 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 new file mode 100644 index 000000000..92b5650bc --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -0,0 +1,199 @@ +import 'package:stream_chat/stream_chat.dart' show ChannelState, Event; +import 'package:stream_chat_flutter_core/src/stream_channel_list_controller.dart'; + +/// Contains handlers that are called from [StreamChannelListController] for +/// certain [Event]s. +/// +/// This class can be mixed in or extended to create custom overrides. +class StreamChannelListEventHandler { + /// Function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// This event is fired when a channel is deleted. + /// + /// By default, this removes the channel from the list of channels. + void onChannelDeleted(Event event, StreamChannelListController controller) { + final channels = [...controller.currentItems]; + + final updatedChannels = channels + ..removeWhere( + (it) => it.cid == (event.cid ?? event.channel?.cid), + ); + + controller.channels = updatedChannels; + } + + /// Function which gets called for the event + /// [EventType.channelHidden]. + /// + /// This event is fired when a channel is hidden. + /// + /// By default, this removes the channel from the list of channels. + void onChannelHidden(Event event, StreamChannelListController controller) { + onChannelDeleted(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// This event is fired when a channel is truncated. + /// + /// By default, this refreshes the whole channel list. + void onChannelTruncated(Event event, StreamChannelListController controller) { + controller.refresh(); + } + + /// Function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// This event is fired when a channel is updated. + /// + /// By default, this updates the channel received in the event. + // ignore: no-empty-block + void onChannelUpdated(Event event, StreamChannelListController controller) {} + + /// Function which gets called for the event + /// [EventType.channelVisible]. + /// + /// This event is fired when a channel is made visible. + /// + /// By default, this adds the channel to the list of channels. + void onChannelVisible( + Event event, + StreamChannelListController controller, + ) async { + final channelId = event.channelId; + final channelType = event.channelType; + + if (channelId == null || channelType == null) return; + + final channel = await controller.getChannel( + id: channelId, + type: channelType, + ); + + final currentChannels = [...controller.currentItems]; + + final updatedChannels = [ + channel, + ...currentChannels..removeWhere((it) => it.cid == channel.cid), + ]; + + controller.channels = updatedChannels; + } + + /// Function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// This event is fired when the client web-socket connection recovers. + /// + /// By default, this refreshes the whole channel list. + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + controller.refresh(); + } + + /// Function which gets called for the event [EventType.messageNew]. + /// + /// This event is fired when a new message is created in one of the channels + /// we are currently watching. + /// + /// By default, this moves the channel to the top of the list. + void onMessageNew(Event event, StreamChannelListController controller) { + final channelCid = event.cid; + if (channelCid == null) return; + + final channels = [...controller.currentItems]; + + final channelIndex = channels.indexWhere((it) => it.cid == channelCid); + if (channelIndex <= 0) return; + + final channel = channels.removeAt(channelIndex); + channels.insert(0, channel); + + controller.channels = [...channels]; + } + + /// Function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// This event is fired when a channel is added which we are not watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// This event is fired when a new message is created in a channel + /// which we are not currently watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationMessageNew( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// This event is fired when a user is removed from a channel which we are + /// not currently watching. + /// + /// By default, this removes the event channel from the list. + void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, + ) { + final channels = [...controller.currentItems]; + final updatedChannels = + channels.where((it) => it.cid != event.channel?.cid); + final listChanged = channels.length != updatedChannels.length; + + if (!listChanged) return; + + controller.channels = [...updatedChannels]; + } + + /// Function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// This event is fired when a user's presence changes or gets updated. + /// + /// By default, this updates the channel member with the event user. + void onUserPresenceChanged( + Event event, + StreamChannelListController controller, + ) { + final user = event.user; + if (user == null) return; + + final channels = [...controller.currentItems]; + + final updatedChannels = channels.map((channel) { + final members = [...channel.state!.members]; + final memberIndex = members.indexWhere( + (it) => user.id == (it.userId ?? it.user?.id), + ); + + if (memberIndex < 0) return channel; + + members[memberIndex] = members[memberIndex].copyWith(user: user); + final updatedState = ChannelState(members: [...members]); + channel.state!.updateChannelState(updatedState); + + return channel; + }); + + controller.channels = [...updatedChannels]; + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index 1f3f753d0..82f0a5d68 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -101,20 +101,6 @@ class StreamChatCoreState extends State return widget.child; } - // coverage:ignore-start - - /// The current user - @Deprecated('Use `.currentUser` instead, Will be removed in future releases') - User? get user => client.state.currentUser; - - /// The current user as a stream - @Deprecated( - 'Use `.currentUserStream` instead, Will be removed in future releases', - ) - Stream get userStream => client.state.currentUserStream; - - // coverage:ignore-end - /// The current user User? get currentUser => client.state.currentUser; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart new file mode 100644 index 000000000..b8f9107d0 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart @@ -0,0 +1,310 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart'; + +/// A value listenable builder related to a [Message]. +/// +/// Pass in a [StreamMessageInputController] as the `valueListenable`. +typedef StreamMessageValueListenableBuilder = ValueListenableBuilder; + +/// Controller for storing and mutating a [Message] value. +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 StreamMessageInputController({ + Message? message, + Map? textPatternStyle, + }) => + StreamMessageInputController._( + initialMessage: message ?? Message(), + textPatternStyle: textPatternStyle, + ); + + /// Creates a controller for an editable text field from an initial [text]. + factory StreamMessageInputController.fromText( + String? text, { + Map? textPatternStyle, + }) => + StreamMessageInputController._( + initialMessage: Message(text: text), + textPatternStyle: textPatternStyle, + ); + + /// Creates a controller for an editable text field from initial + /// [attachments]. + factory StreamMessageInputController.fromAttachments( + List attachments, { + Map? textPatternStyle, + }) => + StreamMessageInputController._( + initialMessage: Message(attachments: attachments), + textPatternStyle: textPatternStyle, + ); + + StreamMessageInputController._({ + required Message initialMessage, + Map? textPatternStyle, + }) : _textEditingController = MessageTextFieldController.fromValue( + initialMessage.text == null + ? const TextEditingValue() + : TextEditingValue( + text: initialMessage.text!, + composing: TextRange.collapsed(initialMessage.text!.length), + ), + textPatternStyle: textPatternStyle, + ), + _initialMessage = initialMessage, + super(initialMessage) { + addListener(_textEditingSyncer); + } + + void _textEditingSyncer() { + final cleanText = value.command == null + ? value.text + : value.text?.replaceFirst('/${value.command} ', ''); + + if (cleanText != _textEditingController.text) { + final previousOffset = _textEditingController.value.selection.start; + final previousText = _textEditingController.text; + final diff = (cleanText?.length ?? 0) - previousText.length; + _textEditingController + ..text = cleanText ?? '' + ..selection = TextSelection.collapsed( + offset: previousOffset + diff, + ); + } + } + + /// Returns the current message associated with this controller. + Message get message => value; + + /// Returns the controller of the text field linked to this controller. + MessageTextFieldController get textEditingController => + _textEditingController; + final MessageTextFieldController _textEditingController; + + /// Returns the text of the message. + String get text => _textEditingController.text; + + Message _initialMessage; + + /// Sets the message. + set message(Message message) { + value = message; + } + + /// Sets the message that's being quoted. + set quotedMessage(Message message) { + value = value.copyWith( + quotedMessage: message, + quotedMessageId: message.id, + ); + } + + /// Clears the quoted message. + void clearQuotedMessage() { + value = value.copyWith( + quotedMessageId: null, + quotedMessage: null, + ); + } + + /// Sets a command for the message. + set command(Command command) { + value = value.copyWith( + command: command.name, + text: '/${command.name} ', + ); + } + + /// Sets the text of the message. + set text(String newText) { + var newTextWithCommand = newText; + if (value.command != null) { + if (!newText.startsWith('/${value.command}')) { + newTextWithCommand = '/${value.command} $newText'; + } + } + value = value.copyWith(text: newTextWithCommand); + } + + /// Returns the baseOffset of the text field. + int get baseOffset => textEditingController.selection.baseOffset; + + /// Returns the start of the selection of the text field. + int get selectionStart => textEditingController.selection.start; + + /// Sets the [showInChannel] flag of the message. + set showInChannel(bool newValue) { + value = value.copyWith(showInChannel: newValue); + } + + /// Returns true if the message is in a thread and + /// should be shown in the main channel as well. + bool get showInChannel => value.showInChannel ?? false; + + /// Returns the attachments of the message. + List get attachments => value.attachments; + + /// Sets the list of [attachments] for the message. + set attachments(List attachments) { + value = value.copyWith(attachments: attachments); + } + + /// Adds a new attachment to the message. + void addAttachment(Attachment attachment) { + attachments = [...attachments, attachment]; + } + + /// Adds a new attachment at the specified [index]. + void addAttachmentAt(int index, Attachment attachment) { + attachments = [...attachments]..insert(index, attachment); + } + + /// Removes the specified [attachment] from the message. + void removeAttachment(Attachment attachment) { + attachments = [...attachments]..remove(attachment); + } + + /// Remove the attachment with the given [attachmentId]. + void removeAttachmentById(String attachmentId) { + attachments = [...attachments]..removeWhere((it) => it.id == attachmentId); + } + + /// Removes the attachment at the given [index]. + void removeAttachmentAt(int index) { + attachments = [...attachments]..removeAt(index); + } + + /// Clears the message attachments. + void clearAttachments() { + attachments = []; + } + + // Only used to store the value locally in order to remove it if we call + // [clearOGAttachment] or [setOGAttachment] again. + Attachment? _ogAttachment; + + /// Returns the og attachment of the message if set + Attachment? get ogAttachment => + attachments.firstWhereOrNull((it) => it.id == _ogAttachment?.id); + + /// Sets the og attachment in the message. + void setOGAttachment(Attachment attachment) { + attachments = [...attachments] + ..remove(_ogAttachment) + ..insert(0, attachment); + _ogAttachment = attachment; + } + + /// Removes the og attachment. + void clearOGAttachment() { + if (_ogAttachment != null) { + removeAttachment(_ogAttachment!); + } + _ogAttachment = null; + } + + /// Returns the list of mentioned users in the message. + List get mentionedUsers => value.mentionedUsers; + + /// Sets the mentioned users. + set mentionedUsers(List users) { + value = value.copyWith(mentionedUsers: users); + } + + /// Adds a user to the list of mentioned users. + void addMentionedUser(User user) { + mentionedUsers = [...mentionedUsers, user]; + } + + /// Removes the specified [user] from the mentioned users list. + void removeMentionedUser(User user) { + mentionedUsers = [...mentionedUsers]..remove(user); + } + + /// Removes the mentioned user with the given [userId]. + void removeMentionedUserById(String userId) { + mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId); + } + + /// Removes all mentioned users from the message. + void clearMentionedUsers() { + mentionedUsers = []; + } + + /// Sets the [message], or [value], to empty. + /// + /// After calling this function, [text], [attachments] and [mentionedUsers] + /// will all be empty. + /// + /// Calling this will notify all the listeners of this + /// [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. + void clear() { + value = Message(); + _textEditingController.clear(); + } + + /// Sets the [value] to the initial [Message] value. + void reset({bool resetId = true}) { + if (resetId) { + final newId = const Uuid().v4(); + _initialMessage = _initialMessage.copyWith(id: newId); + } + value = _initialMessage; + } + + @override + void dispose() { + removeListener(_textEditingSyncer); + _textEditingController.dispose(); + super.dispose(); + } +} + +/// A [RestorableProperty] that knows how to store and restore a +/// [StreamMessageInputController]. +/// +/// 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 StreamRestorableMessageInputController + extends RestorableChangeNotifier { + /// Creates a [StreamRestorableMessageInputController]. + /// + /// This constructor creates a default [Message] when no `message` argument + /// is supplied. + StreamRestorableMessageInputController({Message? message}) + : _initialValue = message ?? Message(); + + /// Creates a [StreamRestorableMessageInputController] from an initial + /// [text] value. + factory StreamRestorableMessageInputController.fromText(String? text) => + StreamRestorableMessageInputController(message: Message(text: text)); + + final Message _initialValue; + + @override + StreamMessageInputController createDefaultValue() => + StreamMessageInputController(message: _initialValue); + + @override + StreamMessageInputController fromPrimitives(Object? data) { + final message = Message.fromJson(json.decode(data! as String)); + return StreamMessageInputController(message: message); + } + + @override + String toPrimitives() => json.encode(value.value); +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart new file mode 100644 index 000000000..d5b8a2f30 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart @@ -0,0 +1,205 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +/// The default channel page limit to load. +const defaultMessageSearchPagedLimit = 10; + +const _kDefaultBackendPaginationLimit = 30; + +/// A controller for a user list. +/// +/// This class lets you perform tasks such as: +/// * Load initial data. +/// * Load more data using [loadMore]. +/// * Replace the previously loaded users. +class StreamMessageSearchListController + extends PagedValueNotifier { + /// Creates a Stream user list controller. + /// + /// * `client` is the Stream chat client to use for the channels list. + /// + /// * `filter` is the query filters to use. + /// + /// * `sort` is the sorting used for the users matching the filters. + /// + /// * `presence` sets whether you'll receive user presence updates via the + /// websocket events. + /// + /// * `limit` is the limit to apply to the user list. + StreamMessageSearchListController({ + required this.client, + required this.filter, + this.messageFilter, + this.searchQuery, + this.sort, + this.limit = defaultMessageSearchPagedLimit, + }) : assert( + messageFilter != null || searchQuery != null, + 'Either messageFilter or searchQuery must be provided', + ), + assert( + messageFilter == null || searchQuery == null, + 'Only one of messageFilter or searchQuery can be provided', + ), + _activeFilter = filter, + _activeMessageFilter = messageFilter, + _activeSearchQuery = searchQuery, + _activeSort = sort, + super(const PagedValue.loading()); + + /// Creates a [StreamUserListController] from the passed [value]. + StreamMessageSearchListController.fromValue( + PagedValue value, { + required this.client, + required this.filter, + this.messageFilter, + this.searchQuery, + this.sort, + this.limit = defaultMessageSearchPagedLimit, + }) : assert( + messageFilter != null || searchQuery != null, + 'Either messageFilter or searchQuery must be provided', + ), + assert( + messageFilter == null || searchQuery == null, + 'Only one of messageFilter or searchQuery can be provided', + ), + _activeFilter = filter, + _activeMessageFilter = messageFilter, + _activeSearchQuery = searchQuery, + _activeSort = sort, + super(value); + + /// The client to use for the channels list. + final StreamChatClient client; + + /// The query filters to use. + /// + /// You can query on any of the custom fields you've defined on the [User]. + /// + /// You can also filter other built-in channel fields. + final Filter filter; + Filter _activeFilter; + + /// The message query filters to use. + /// + /// You can query on any of the custom fields you've defined on the [Channel]. + /// + /// You can also filter other built-in channel fields. + final Filter? messageFilter; + Filter? _activeMessageFilter; + + /// Message String to search on. + final String? searchQuery; + String? _activeSearchQuery; + + /// The sorting used for the users matching the filters. + /// + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// + /// Direction can be ascending or descending. + final List? sort; + List? _activeSort; + + /// The limit to apply to the user list. The default is set to + /// [defaultUserPagedLimit]. + final int limit; + + /// Allows for the change of filters used for user queries. + /// + /// Use this if you need to support runtime filter changes, + /// through custom filters UI. + set filter(Filter value) => _activeFilter = value; + + /// Allows for the change of message filters used for user queries. + /// + /// Use this if you need to support runtime filter changes, + /// through custom filters UI. + set messageFilter(Filter? value) => _activeMessageFilter = value; + + /// Allows for the change of filters used for user queries. + /// + /// Use this if you need to support runtime filter changes, + /// through custom filters UI. + set searchQuery(String? value) => _activeSearchQuery = value; + + /// Allows for the change of the query sort used for user queries. + /// + /// Use this if you need to support runtime sort changes, + /// through custom sort UI. + set sort(List? value) => _activeSort = value; + + @override + Future doInitialLoad() async { + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); + try { + final response = await client.search( + _activeFilter, + sort: _activeSort, + query: _activeSearchQuery, + messageFilters: _activeMessageFilter, + paginationParams: PaginationParams(limit: limit), + ); + + final results = response.results; + final nextKey = response.next; + value = PagedValue( + items: results, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); + } + } + + @override + Future loadMore(String nextPageKey) async { + final previousValue = value.asSuccess; + + try { + final response = await client.search( + _activeFilter, + sort: _activeSort, + query: _activeSearchQuery, + messageFilters: _activeMessageFilter, + paginationParams: PaginationParams(limit: limit, next: nextPageKey), + ); + + final results = response.results; + final previousItems = previousValue.items; + final newItems = previousItems + results; + final next = response.next; + final nextKey = next != null && next.isNotEmpty ? next : null; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); + } + } + + @override + Future refresh({bool resetValue = true}) { + if (resetValue) { + _activeFilter = filter; + _activeMessageFilter = messageFilter; + _activeSearchQuery = searchQuery; + _activeSort = sort; + } + return super.refresh(resetValue: resetValue); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart new file mode 100644 index 000000000..1881e8683 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart @@ -0,0 +1,165 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +/// The default channel page limit to load. +const defaultUserPagedLimit = 10; + +const _kDefaultBackendPaginationLimit = 30; + +/// A controller for a user list. +/// +/// This class lets you perform tasks such as: +/// * Load initial data. +/// * Load more data using [loadMore]. +/// * Replace the previously loaded users. +class StreamUserListController extends PagedValueNotifier { + /// Creates a Stream user list controller. + /// + /// * `client` is the Stream chat client to use for the channels list. + /// + /// * `filter` is the query filters to use. + /// + /// * `sort` is the sorting used for the users matching the filters. + /// + /// * `presence` sets whether you'll receive user presence updates via the + /// websocket events. + /// + /// * `limit` is the limit to apply to the user list. + StreamUserListController({ + required this.client, + this.filter, + this.sort, + this.presence = true, + this.limit = defaultUserPagedLimit, + }) : _activeFilter = filter, + _activeSort = sort, + super(const PagedValue.loading()); + + /// Creates a [StreamUserListController] from the passed [value]. + StreamUserListController.fromValue( + PagedValue value, { + required this.client, + this.filter, + this.sort, + this.presence = true, + this.limit = defaultUserPagedLimit, + }) : _activeFilter = filter, + _activeSort = sort, + super(value); + + /// The client to use for the channels list. + final StreamChatClient client; + + /// The query filters to use. + /// + /// You can query on any of the custom fields you've defined on the [User]. + /// + /// You can also filter other built-in channel fields. + final Filter? filter; + Filter? _activeFilter; + + /// The sorting used for the users matching the filters. + /// + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// + /// Direction can be ascending or descending. + final List? sort; + List? _activeSort; + + /// If true you’ll receive user presence updates via the websocket events + final bool presence; + + /// The limit to apply to the user list. The default is set to + /// [defaultUserPagedLimit]. + final int limit; + + /// Allows for the change of filters used for user queries. + /// + /// Use this if you need to support runtime filter changes, + /// through custom filters UI. + set filter(Filter? value) => _activeFilter = value; + + /// Allows for the change of the query sort used for user queries. + /// + /// Use this if you need to support runtime sort changes, + /// through custom sort UI. + set sort(List? value) => _activeSort = value; + + @override + Future doInitialLoad() async { + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); + try { + final userResponse = await client.queryUsers( + filter: _activeFilter, + sort: _activeSort, + presence: presence, + pagination: PaginationParams(limit: limit), + ); + + final users = userResponse.users; + final nextKey = users.length < limit ? null : users.length; + value = PagedValue( + items: users, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); + } + } + + @override + Future loadMore(int nextPageKey) async { + final previousValue = value.asSuccess; + + try { + final userResponse = await client.queryUsers( + filter: _activeFilter, + sort: _activeSort, + presence: presence, + pagination: PaginationParams(limit: limit, offset: nextPageKey), + ); + + final users = userResponse.users; + final previousItems = previousValue.items; + final newItems = previousItems + users; + final nextKey = users.length < limit ? null : newItems.length; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); + } + } + + @override + Future refresh({bool resetValue = true}) { + if (resetValue) { + _activeFilter = filter; + _activeSort = sort; + } + return super.refresh(resetValue: resetValue); + } + + /// Replaces the previously loaded users with [users] and updates + /// the nextPageKey. + set users(List users) { + value = PagedValue( + items: users, + nextPageKey: users.length, + ); + } +} 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 61fbb860a..03159afb9 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 e0a09116d..c01242ec4 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'; @@ -9,6 +11,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// using Flutter's [BuildContext]. /// /// API docs: https://getstream.io/chat/docs/flutter-dart/init_and_users/ +@Deprecated("Use 'StreamUserListController' instead") class UsersBloc extends StatefulWidget { /// Instantiate a new [UsersBloc]. The parameter [child] must be supplied and /// not null. 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 8bd41c768..34cbaaa14 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 @@ -10,8 +10,17 @@ export 'src/lazy_load_scroll_view.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, PagedValue, PagedValueNotifier; +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'; export 'src/user_list_core.dart' hide UserListCoreState; export 'src/users_bloc.dart'; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 25acb90af..dd4ad795c 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,12 +1,12 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 3.6.1 +version: 4.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' flutter: ">=1.17.0" dependencies: @@ -14,14 +14,17 @@ dependencies: connectivity_plus: ^2.1.0 flutter: sdk: flutter + freezed_annotation: ^1.0.0 meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^3.6.1 + stream_chat: ^4.0.0 dev_dependencies: + build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 fake_async: ^1.2.0 flutter_test: sdk: flutter + freezed: ^1.0.0 mocktail: ^0.3.0 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 5f7c0e036..f94ab3c62 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 e63658686..630ef8810 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 764e629b0..c1465731a 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 d699faa00..c56e377bb 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 27f4cbe88..86ae601da 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 549f92916..ce28c2f80 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 a79e912ee..a23bf1a10 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,7 +1,11 @@ -## Upcoming +## 3.0.0 * 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). + ## 2.1.0 ✅ Added diff --git a/packages/stream_chat_localizations/example/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_localizations/example/android/app/src/main/AndroidManifest.xml index 55ca830c3..9b3997fe8 100644 --- a/packages/stream_chat_localizations/example/android/app/src/main/AndroidManifest.xml +++ b/packages/stream_chat_localizations/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. --> CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.pbxproj index 3ea4f8267..52fdc7407 100644 --- a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..3db53b6e1 100644 --- a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; @@ -393,6 +397,13 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; + @override String get viewLibrary => 'View library'; } @@ -492,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 { @@ -503,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 82d7e92cd..55354e50b 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 6f3115448..33ebaa99b 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/example/pubspec.yaml b/packages/stream_chat_localizations/example/pubspec.yaml index 9aaa23c68..db4476181 100644 --- a/packages/stream_chat_localizations/example/pubspec.yaml +++ b/packages/stream_chat_localizations/example/pubspec.yaml @@ -11,8 +11,15 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter: ^2.2.1 - stream_chat_localizations: ^1.1.0 + stream_chat_flutter: + path: ../../stream_chat_flutter + stream_chat_localizations: + path: ../ + + +dependency_overrides: + stream_chat_flutter: + path: ../../stream_chat_flutter dev_dependencies: flutter_test: 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 bc6de2444..1d1d82b38 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 @@ -60,6 +60,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; @@ -369,6 +373,13 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @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 526a11e67..b906843f1 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 @@ -61,6 +61,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { return 'Fijado por ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'No tienes permiso para enviar mensajes'; + @override String get emptyMessagesText => 'Actualmente no hay mensajes'; @@ -377,4 +381,11 @@ No es posible añadir más de $limit archivos adjuntos @override String get slowModeOnLabel => 'Modo lento activado'; + + @override + String get linkDisabledDetails => + 'No se permite enviar enlaces en esta conversación.'; + + @override + String get linkDisabledError => 'Los enlaces están deshabilitados'; } 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 8e73e0f9c..b5a44a51d 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 @@ -61,6 +61,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { return 'Épinglé par ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Vous n\'êtes pas autorisé à envoyer des messages'; + @override String get emptyMessagesText => "Il n'y a pas de messages actuellement"; @@ -376,4 +380,11 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get slowModeOnLabel => 'Mode lent activé'; + + @override + String get linkDisabledDetails => + 'L\'envoi de liens n\'est pas autorisé dans cette conversation.'; + + @override + String get linkDisabledError => 'Les liens sont désactivés'; } 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 e482aebf0..e07566bd4 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 @@ -60,6 +60,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { return '${pinnedBy.name} द्वारा पिन किया गया'; } + @override + String get sendMessagePermissionError => 'आपको संदेश भेजने की अनुमति नहीं है'; + @override String get emptyMessagesText => 'वर्तमान में कोई संदेश नहीं है'; @@ -371,4 +374,11 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'स्लो मोड चालू'; + + @override + String get linkDisabledDetails => + 'इस बातचीत में लिंक भेजने की अनुमति नहीं है.'; + + @override + String get linkDisabledError => 'लिंक भेजना प्रतिबंधित'; } 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 44cf8264e..ffbdde94b 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 @@ -60,6 +60,10 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { return 'Messo in evidenza da ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Non hai l\'autorizzazione per inviare messaggi'; + @override String get emptyMessagesText => 'Non c\'é nessun messaggio al momento'; @@ -373,4 +377,11 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get slowModeOnLabel => 'Slowmode attiva'; + + @override + String get linkDisabledDetails => + 'Non è permesso condividere link in questa convesazione.'; + + @override + String get linkDisabledError => 'I links sono disattivati'; } 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 cc94b2024..7729a6ca2 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 @@ -60,6 +60,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { return '${pinnedBy.name}のピン'; } + @override + String get sendMessagePermissionError => 'メッセージを送信する権限がありません'; + @override String get emptyMessagesText => '現在、メッセージはありません。'; @@ -357,4 +360,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String attachmentLimitExceedError(int limit) => ''' 添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません '''; + + @override + String get linkDisabledDetails => 'この会話では、リンクの送信は許可されていません。'; + + @override + String get linkDisabledError => 'リンクが無効になっています'; } 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 f31a1bb53..c7c858f1c 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 @@ -60,6 +60,9 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { return '${pinnedBy.name}의 핀'; } + @override + String get sendMessagePermissionError => '메시지를 보낼 수 있는 권한이 없습니다'; + @override String get emptyMessagesText => '현재 메시지가 없습니다'; @@ -355,6 +358,13 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get viewLibrary => '라이브러리 보기'; + @override String attachmentLimitExceedError(int limit) => '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; + + @override + String get linkDisabledDetails => '이 대화에서는 링크를 보낼 수 없습니다.'; + + @override + String get linkDisabledError => '링크가 비활성화되었습니다.'; } 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 d00f0818c..3a1a7696d 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 @@ -372,6 +372,17 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get slowModeOnLabel => 'Modo lento ativado'; + @override + String get linkDisabledDetails => + 'O envio de links não é permitido nesta conversa.'; + + @override + String get linkDisabledError => 'Os links estão desativados'; + + @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_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 00606a7b2..d30076631 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 2.1.0 +version: 3.0.0 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^3.4.0 + stream_chat_flutter: ^4.0.0 dev_dependencies: dart_code_metrics: ^4.4.0 diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index ea67fa880..6f3482ece 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,11 @@ +## 4.0.0 + +- Updated `stream_chat` dependency to [`4.0.0`](https://pub.dev/packages/stream_chat/changelog). + +## 4.0.0-beta.0 + +- Updated `stream_chat` dependency to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat/changelog). + ## 3.1.0 - Bump `drift` to `1.3.0`. diff --git a/packages/stream_chat_persistence/example/android/app/build.gradle b/packages/stream_chat_persistence/example/android/app/build.gradle index 3932aa910..31d3553ba 100644 --- a/packages/stream_chat_persistence/example/android/app/build.gradle +++ b/packages/stream_chat_persistence/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/packages/stream_chat_persistence/example/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_persistence/example/android/app/src/main/AndroidManifest.xml index 55ca830c3..9b3997fe8 100644 --- a/packages/stream_chat_persistence/example/android/app/src/main/AndroidManifest.xml +++ b/packages/stream_chat_persistence/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. --> snapshot, ) { if (snapshot.hasData && snapshot.data != null) { + final _messages = snapshot.data!.messages ?? []; return MessageView( - messages: snapshot.data!.messages.reversed.toList(), + messages: _messages.reversed.toList(), channel: channel, ); } else if (snapshot.hasError) { diff --git a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index 3a604ca1b..9f81879c1 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -34,14 +34,20 @@ class MemberDao extends DatabaseAccessor bulkUpdateMembers({cid: memberList}); /// Bulk updates the members data of multiple channels - Future bulkUpdateMembers(Map> channelWithMembers) { + Future bulkUpdateMembers( + Map?> channelWithMembers, + ) { final entities = channelWithMembers.entries - .map((entry) => entry.value.map( + .map((entry) => + (entry.value?.map( (member) => member.toEntity(cid: entry.key), - )) + )) ?? + []) .expand((it) => it) .toList(growable: false); - return batch((batch) => batch.insertAllOnConflictUpdate(members, entities)); + return batch( + (batch) => batch.insertAllOnConflictUpdate(members, entities), + ); } /// Deletes all the members whose [Members.channelCid] is present in [cids] diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index 31ca74ee1..ccaefced2 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -183,12 +183,14 @@ class MessageDao extends DatabaseAccessor /// Bulk updates the message data of multiple channels Future bulkUpdateMessages( - Map> channelWithMessages, + Map?> channelWithMessages, ) { final entities = channelWithMessages.entries - .map((entry) => entry.value.map( + .map((entry) => + entry.value?.map( (message) => message.toEntity(cid: entry.key), - )) + ) ?? + []) .expand((it) => it) .toList(growable: false); return batch( diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index 8712dea57..8be2399b6 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -183,12 +183,14 @@ class PinnedMessageDao extends DatabaseAccessor /// Bulk updates the message data of multiple channels Future bulkUpdateMessages( - Map> channelWithMessages, + Map?> channelWithMessages, ) { final entities = channelWithMessages.entries - .map((entry) => entry.value.map( + .map((entry) => + entry.value?.map( (message) => message.toPinnedEntity(cid: entry.key), - )) + ) ?? + []) .expand((it) => it) .toList(growable: false); return batch( diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index 0a578d699..9e46f818d 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -33,11 +33,13 @@ class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { bulkUpdateReads({cid: readList}); /// Bulk updates the reads data of multiple channels - Future bulkUpdateReads(Map> channelWithReads) { + Future bulkUpdateReads(Map?> channelWithReads) { final entities = channelWithReads.entries - .map((entry) => entry.value.map( + .map((entry) => + entry.value?.map( (read) => read.toEntity(cid: entry.key), - )) + ) ?? + []) .expand((it) => it) .toList(growable: false); return batch((batch) => batch.insertAllOnConflictUpdate(reads, entities)); diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart index 8561eed47..9b73312db 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart @@ -56,7 +56,7 @@ class DriftChatDatabase extends _$DriftChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 6; + int get schemaVersion => 7; @override MigrationStrategy get migration => MigrationStrategy( 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 f8dfe9e5f..6eb291399 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 @@ -294,21 +294,21 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } @override - Future bulkUpdateMembers(Map> members) { + Future bulkUpdateMembers(Map?> members) { assert(_debugIsConnected, ''); _logger.info('bulkUpdateMembers'); return _readProtected(() => db!.memberDao.bulkUpdateMembers(members)); } @override - Future bulkUpdateMessages(Map> messages) { + Future bulkUpdateMessages(Map?> messages) { assert(_debugIsConnected, ''); _logger.info('bulkUpdateMessages'); return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages)); } @override - Future bulkUpdatePinnedMessages(Map> messages) { + Future bulkUpdatePinnedMessages(Map?> messages) { assert(_debugIsConnected, ''); _logger.info('bulkUpdatePinnedMessages'); return _readProtected( @@ -333,7 +333,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { } @override - Future bulkUpdateReads(Map> reads) { + Future bulkUpdateReads(Map?> reads) { assert(_debugIsConnected, ''); _logger.info('bulkUpdateReads'); return _readProtected(() => db!.readDao.bulkUpdateReads(reads)); diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 06167a117..05872dab7 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 3.1.0 +version: 4.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -19,7 +19,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^3.4.0 + stream_chat: ^4.0.0 dev_dependencies: build_runner: ^2.0.1 diff --git a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart index e9718af90..d89687d35 100644 --- a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart @@ -61,10 +61,10 @@ void main() { ); expect(channelState, isA()); - expect(channelState.members.length, members.length); - expect(channelState.read.length, reads.length); - expect(channelState.messages.length, messages.length); - expect(channelState.pinnedMessages.length, messages.length); + expect(channelState.members?.length, members.length); + expect(channelState.read?.length, reads.length); + expect(channelState.messages?.length, messages.length); + expect(channelState.pinnedMessages?.length, messages.length); final channelModel = channelState.channel!; expect(channelModel.id, entity.id); diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index d2ea8b938..8a06f02d5 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -157,7 +157,6 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - status: MessageSendingStatus.sending, updatedAt: DateTime.now(), extraData: const {'extra_test_data': 'extraData'}, user: user, diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 9c2378fd0..9c4d99a20 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -147,7 +147,6 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - status: MessageSendingStatus.sending, updatedAt: DateTime.now(), extraData: const {'extra_test_data': 'extraData'}, user: user, diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 4900aab29..8d7ab8be2 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -219,10 +219,10 @@ void main() { .thenAnswer((_) async => messages); final fetchedChannelState = await client.getChannelStateByCid(cid); - expect(fetchedChannelState.messages.length, messages.length); - expect(fetchedChannelState.pinnedMessages.length, messages.length); - expect(fetchedChannelState.members.length, members.length); - expect(fetchedChannelState.read.length, reads.length); + expect(fetchedChannelState.messages?.length, messages.length); + expect(fetchedChannelState.pinnedMessages?.length, messages.length); + expect(fetchedChannelState.members?.length, members.length); + expect(fetchedChannelState.read?.length, reads.length); expect(fetchedChannelState.channel!.cid, channel.cid); verify(() => mockDatabase.memberDao.getMembersByCid(cid)).called(1); @@ -277,10 +277,10 @@ void main() { for (var i = 0; i < fetchedChannelStates.length; i++) { final original = channelStates[i]; final fetched = fetchedChannelStates[i]; - expect(fetched.members.length, original.members.length); - expect(fetched.messages.length, original.messages.length); - expect(fetched.pinnedMessages.length, original.pinnedMessages.length); - expect(fetched.read.length, original.read.length); + expect(fetched.members?.length, original.members?.length); + expect(fetched.messages?.length, original.messages?.length); + expect(fetched.pinnedMessages?.length, original.pinnedMessages?.length); + expect(fetched.read?.length, original.read?.length); expect(fetched.channel!.cid, original.channel!.cid); }