diff --git a/.github/workflows/pana.yml b/.github/workflows/pana.yml index bef2ec03..a6a7e822 100644 --- a/.github/workflows/pana.yml +++ b/.github/workflows/pana.yml @@ -31,8 +31,7 @@ jobs: TOTAL: ${{ steps.analysis.outputs.total }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | - PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 90 )) + if (( $TOTAL < 120 )) then echo Score too low! exit 1 @@ -53,7 +52,7 @@ jobs: TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 90 )) + if (( $TOTAL < 90 )) then echo Score too low! exit 1 @@ -73,8 +72,7 @@ jobs: TOTAL: ${{ steps.analysis.outputs.total }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | - PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 100 )) + if (( $TOTAL < 120 )) then echo Score too low! exit 1 @@ -94,8 +92,7 @@ jobs: TOTAL: ${{ steps.analysis.outputs.total }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | - PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 80 )) + if (( $TOTAL < 100 )) then echo Score too low! exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0faad799..3bb5a923 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,8 @@ Stream's Flutter code is kept in a single mono-repository consisting of multiple `stream_chat_persistence` - This package provides a persistence client for fetching and saving chat data locally. Stream Chat Persistence uses Moor as a disk cache. +`stream_chat_localizations` - This package provides a set of localizations for the SDK. + ### Local Setup Congratulations! ЁЯОЙ. You've successfully cloned our repo, and you are ready to make your first contribution. Before you can start making code changes, there are a few things to configure. diff --git a/README.md b/README.md index 6758b189..62358d57 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ 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. ## 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/stream_chat_v1). +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). ## Free for Makers diff --git a/docusaurus/docs/Flutter/assets/live_stream_1.jpg b/docusaurus/docs/Flutter/assets/live_stream_1.jpg new file mode 100644 index 00000000..bef68c6e Binary files /dev/null and b/docusaurus/docs/Flutter/assets/live_stream_1.jpg differ diff --git a/docusaurus/docs/Flutter/assets/live_stream_2.jpg b/docusaurus/docs/Flutter/assets/live_stream_2.jpg new file mode 100644 index 00000000..02a35132 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/live_stream_2.jpg differ diff --git a/docusaurus/docs/Flutter/assets/message_actions.png b/docusaurus/docs/Flutter/assets/message_actions.png new file mode 100644 index 00000000..157f3979 Binary files /dev/null and b/docusaurus/docs/Flutter/assets/message_actions.png differ diff --git a/docusaurus/docs/Flutter/basics/introduction.mdx b/docusaurus/docs/Flutter/basics/introduction.mdx index 756bd33f..99170774 100644 --- a/docusaurus/docs/Flutter/basics/introduction.mdx +++ b/docusaurus/docs/Flutter/basics/introduction.mdx @@ -8,13 +8,14 @@ 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 (and more) apps. +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). +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 of four different packages to choose from ranging from ones +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: @@ -30,28 +31,28 @@ reusable and customisable UI components. 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 since it contains UI widgets already -integrated with Stream's API. [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter) -is the fastest way to get up and running using Stream chat in your app. +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, -our [core package](https://pub.dev/packages/stream_chat_flutter) 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, etc are seamlessly integrated into -this package and accessible via providers and builders. +[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. +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 between two people and a group chat, +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) @@ -68,7 +69,7 @@ While this is a simplistic overview of the service, the Flutter SDK handles the 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 Javascript-based but the ideas are pretty much the same as Dart. +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. diff --git a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx new file mode 100644 index 00000000..9e2afb44 --- /dev/null +++ b/docusaurus/docs/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/docs/Flutter/guides/adding_local_data_persistence.mdx b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx new file mode 100644 index 00000000..e6283ca0 --- /dev/null +++ b/docusaurus/docs/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/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 5a8813c2..4ad5eede 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -1,7 +1,7 @@ --- id: adding_localization sidebar_position: 2 -title: Adding Localization +title: Adding Localization (l10n) / Internationalization (i18n) --- Adding Localization To UI Widgets @@ -14,7 +14,7 @@ We have a dedicated package for adding localization to our UI widgets. It's call ## 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**). +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. @@ -42,7 +42,7 @@ 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`. +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 diff --git a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx new file mode 100644 index 00000000..b1cc07a0 --- /dev/null +++ b/docusaurus/docs/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.png) + +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/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx new file mode 100644 index 00000000..9d169a80 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -0,0 +1,143 @@ +--- +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', true) +``` + +### 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/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 9bfe494f..52496a96 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,15 @@ +## Upcoming + +ЁЯРЮ Fixed + +- Fixed `channel.markAllRead` throwing failed host lookup. + +тЬЕ Added + +- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. +- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. +- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. +- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized. ## 2.1.1 ЁЯРЮ Fixed diff --git a/packages/stream_chat/example/lib/main.dart b/packages/stream_chat/example/lib/main.dart index 46030341..d2c5dcea 100644 --- a/packages/stream_chat/example/lib/main.dart +++ b/packages/stream_chat/example/lib/main.dart @@ -13,10 +13,9 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: const { - 'image': - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - }, + name: 'Cool Shadow', + image: + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', ); diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index a05587ab..3285db75 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -15,20 +15,77 @@ import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/stream_chat.dart'; -/// This a the class that manages a specific channel. +/// Class that manages a specific channel. +/// +/// #### Channel name +/// +/// {@template name} +/// If an optional [name] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'name'. +/// +/// ```dart +/// final channel = Channel(client, type, id, name: 'Channel name'); +/// print(channel.name == channel.extraData['name']); // true +/// ``` +/// +/// Before the channel is initialized the name can be set directly: +/// ```dart +/// channel.name = 'New channel name'; +/// ``` +/// +/// To update the name after the channel has been initialized, call: +/// ```dart +/// channel.updateName('Updated channel name'); +/// ``` +/// +/// This will do a partial update to update the name. +/// {@endtemplate} +/// +/// #### Channel image +/// +/// {@template image} +/// If an optional [image] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'image'. +/// +/// ```dart +/// final channel = Channel(client, type, id, image: 'https://getstream.io/image.png'); +/// print(channel.image == channel.extraData['image']); // true +/// ``` +/// +/// Before the channel is initialized the image can be set directly: +/// ```dart +/// channel.image = 'https://getstream.io/new-image'; +/// ``` +/// +/// To update the image after the channel has been initialized, call: +/// ```dart +/// channel.updateImage('https://getstream.io/new-image'); +/// ``` +/// +/// This will do a partial update to update the image. +/// {@endtemplate} class Channel { - /// Create a channel client instance. + /// Class that manages a specific channel. + /// + /// Optional [extraData] and [image] properties can be provided. The [image] + /// is exposed to easily set a key of 'image' on [extraData]. Channel( this._client, this._type, this._id, { + String? name, + String? image, Map? extraData, }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = extraData ?? {} { - _client.logger.info('New Channel instance not initialized created'); + _extraData = { + ...?extraData, + if (name != null) 'name': name, + if (image != null) 'image': image, + } { + _client.logger.info('New Channel instance created, not yet initialized'); } - /// Create a channel client instance from a [ChannelState] object + /// Create a channel client instance from a [ChannelState] object. Channel.fromState(this._client, ChannelState channelState) : assert( channelState.channel != null, @@ -40,7 +97,7 @@ class Channel { _extraData = channelState.channel!.extraData { state = ChannelClientState(this, channelState); _initializedCompleter.complete(true); - _client.logger.info('New Channel instance initialized created'); + _client.logger.info('New Channel instance initialized'); } /// This client state @@ -53,155 +110,173 @@ class Channel { String? _cid; final Map _extraData; + /// Shortcut to set channel name. + /// + /// {@macro name} + set name(String? name) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use `channel.updateName` ' + 'to update the channel name', + ); + } + _extraData.addAll({'name': name}); + } + + /// Shortcut to set channel image. + /// + /// {@macro image} + set image(String? image) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use `channel.updateImage` ' + 'to update the channel image', + ); + } + _extraData.addAll({'image': image}); + } + set extraData(Map extraData) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.update ' + 'Once the channel is initialized you should use `channel.update` ' 'to update channel data', ); } _extraData.addAll(extraData); } - /// Returns true if the channel is muted + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes .any((element) => element.channel.cid == cid) == true; - /// Returns true if the channel is muted as a stream - Stream? get isMutedStream => _client.state.currentUserStream + /// Returns true if the channel is muted, as a stream. + Stream get isMutedStream => _client.state.currentUserStream .map((event) => - event!.channelMutes.any((element) => element.channel.cid == cid) == + event?.channelMutes.any((element) => element.channel.cid == cid) == true) .distinct(); - /// True if the channel is a group + /// True if the channel is a group. bool get isGroup => memberCount != 2; - /// True if the channel is distinct + /// True if the channel is distinct. bool get isDistinct => id?.startsWith('!members') == true; - /// Channel configuration + /// Channel configuration. ChannelConfig? get config { _checkInitialized(); - return state?._channelState.channel?.config; + return state!._channelState.channel?.config; } - /// Channel configuration as a stream - Stream? get configStream { + /// Channel configuration as a stream. + Stream get configStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.config); + return state!.channelStateStream.map((cs) => cs.channel?.config); } - /// Channel user creator + /// Channel user creator. User? get createdBy { _checkInitialized(); - return state?._channelState.channel?.createdBy; + return state!._channelState.channel?.createdBy; } - /// Channel user creator as a stream - Stream? get createdByStream { + /// Channel user creator as a stream. + Stream get createdByStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.createdBy); + return state!.channelStateStream.map((cs) => cs.channel?.createdBy); } - /// Channel frozen status - bool? get frozen { + /// Channel frozen status. + bool get frozen { _checkInitialized(); - return state?._channelState.channel?.frozen; + return state!._channelState.channel?.frozen == true; } - /// Channel frozen status as a stream - Stream? get frozenStream { + /// Channel frozen status as a stream. + Stream get frozenStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.frozen); + return state!.channelStateStream.map((cs) => cs.channel?.frozen == true); } - /// Channel creation date + /// Channel creation date. DateTime? get createdAt { _checkInitialized(); - return state?._channelState.channel?.createdAt; + return state!._channelState.channel?.createdAt; } - /// Channel creation date as a stream - Stream? get createdAtStream { + /// Channel creation date as a stream. + Stream get createdAtStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.createdAt); + return state!.channelStateStream.map((cs) => cs.channel?.createdAt); } - /// Channel last message date + /// Channel last message date. DateTime? get lastMessageAt { _checkInitialized(); - - return state?._channelState.channel?.lastMessageAt; + return state!._channelState.channel?.lastMessageAt; } - /// Channel last message date as a stream - Stream? get lastMessageAtStream { + /// Channel last message date as a stream. + Stream get lastMessageAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.lastMessageAt); + return state!.channelStateStream.map((cs) => cs.channel?.lastMessageAt); } - /// Channel updated date + /// Channel updated date. DateTime? get updatedAt { _checkInitialized(); - - return state?._channelState.channel?.updatedAt; + return state!._channelState.channel?.updatedAt; } - /// Channel updated date as a stream - Stream? get updatedAtStream { + /// Channel updated date as a stream. + Stream get updatedAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.updatedAt); + return state!.channelStateStream.map((cs) => cs.channel?.updatedAt); } - /// Channel deletion date + /// Channel deletion date. DateTime? get deletedAt { _checkInitialized(); - - return state?._channelState.channel?.deletedAt; + return state!._channelState.channel?.deletedAt; } - /// Channel deletion date as a stream - Stream? get deletedAtStream { + /// Channel deletion date as a stream. + Stream get deletedAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.deletedAt); + return state!.channelStateStream.map((cs) => cs.channel?.deletedAt); } - /// Channel member count + /// Channel member count. int? get memberCount { _checkInitialized(); - - return state?._channelState.channel?.memberCount; + return state!._channelState.channel?.memberCount; } - /// Channel member count as a stream - Stream? get memberCountStream { + /// Channel member count as a stream. + Stream get memberCountStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.memberCount); + return state!.channelStateStream.map((cs) => cs.channel?.memberCount); } - /// Channel id + /// Channel id. String? get id => state?._channelState.channel?.id ?? _id; - /// Channel type + /// Channel type. String get type => state?._channelState.channel?.type ?? _type; - /// Channel cid + /// Channel cid. String? get cid => state?._channelState.channel?.cid ?? _cid; - /// Channel team + /// Channel team. String? get team { _checkInitialized(); - return state?._channelState.channel?.team; + return state!._channelState.channel?.team; } - /// Channel extra data + /// Channel extra data. Map get extraData { var data = state?._channelState.channel?.extraData; if (data == null || data.isEmpty) { @@ -210,23 +285,54 @@ class Channel { return data; } - /// Channel extra data as a stream - Stream> get extraDataStream { + /// Channel extra data as a stream. + Stream> get extraDataStream { _checkInitialized(); return state!.channelStateStream.map( (cs) => cs.channel?.extraData ?? _extraData, ); } - /// The main Stream chat client + /// Shortcut to get channel name. + /// + /// {@macro name} + String? get name => extraData['name'] as String?; + + /// Channel [name] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro name} + Stream get nameStream { + _checkInitialized(); + return extraDataStream.map((it) => it['name'] as String?); + } + + /// Shortcut to get channel image. + /// + /// {@macro image} + String? get image => extraData['image'] as String?; + + /// Channel [image] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro image} + Stream get imageStream { + _checkInitialized(); + return extraDataStream.map((it) => it['image'] as String?); + } + + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; final Completer _initializedCompleter = Completer(); - /// True if this is initialized + /// True if this is initialized. + /// /// Call [watch] to initialize the client or instantiate it using - /// [Channel.fromState] + /// [Channel.fromState]. Future get initialized => _initializedCompleter.future; final _cancelableAttachmentUploadRequest = {}; @@ -362,7 +468,9 @@ class Channel { } /// Send a [message] to this channel. - /// If [skipPush] is true the message will not send a push notification + /// + /// If [skipPush] is true the message will not send a push notification. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually sending the message. Future sendMessage( @@ -427,6 +535,7 @@ class Channel { } /// Updates the [message] in this channel. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually updating the message. Future updateMessage(Message message) async { @@ -489,8 +598,10 @@ class Channel { } /// Partially updates the [message] in this channel. - /// Use [set] to define values to be set - /// Use [unset] to define values to be unset + /// + /// Use [set] to define values to be set. + /// + /// Use [unset] to define values to be unset. Future partialUpdateMessage( Message message, { Map? set, @@ -590,7 +701,7 @@ class Channel { ); } - /// Unpins provided message + /// Unpins provided message. Future unpinMessage(Message message) => partialUpdateMessage( message, @@ -599,7 +710,7 @@ class Channel { }, ); - /// Send a file to this channel + /// Send a file to this channel. Future sendFile( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -615,7 +726,7 @@ class Channel { ); } - /// Send an image to this channel + /// Send an image to this channel. Future sendImage( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -631,7 +742,7 @@ class Channel { ); } - /// A message search. + /// Search for a message with the given options. Future search({ String? query, Filter? messageFilters, @@ -648,7 +759,7 @@ class Channel { ); } - /// Delete a file from this channel + /// Delete a file from this channel. Future deleteFile( String url, { CancelToken? cancelToken, @@ -662,7 +773,7 @@ class Channel { ); } - /// Delete an image from this channel + /// Delete an image from this channel. Future deleteImage( String url, { CancelToken? cancelToken, @@ -676,14 +787,15 @@ class Channel { ); } - /// Send an event on this channel + /// Send an event on this channel. Future sendEvent(Event event) { _checkInitialized(); return _client.sendEvent(id!, type, event); } - /// Send a reaction to this channel - /// Set [enforceUnique] to true to remove the existing user reaction + /// Send a reaction to this channel. + /// + /// Set [enforceUnique] to true to remove the existing user reaction. Future sendReaction( Message message, String type, { @@ -746,7 +858,7 @@ class Channel { } } - /// Delete a reaction from this channel + /// Delete a reaction from this channel. Future deleteReaction( Message message, Reaction reaction) async { final type = reaction.type; @@ -792,7 +904,49 @@ class Channel { } } - /// Edit the channel custom data + /// Update the channel's [name]. + /// + /// This is the same as calling [updatePartial] and providing a map with a + /// 'name' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'name': 'Updated channel name'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateName('Updated channel name'); + /// ``` + Future updateName(String name) => + updatePartial(set: {'name': name}); + + /// Update the channel's [image]. + /// + /// This is the same as calling [updatePartial] and providing a map with an + /// 'image' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'image': 'https://getstream.io/new-image'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateImage('https://getstream.io/new-image'); + /// ``` + Future updateImage(String image) => + updatePartial(set: {'image': image}); + + /// Update the channel custom data. This replaces all of the channel data + /// with the given [channelData]. + /// + /// If you instead want to do a partial update, use [updatePartial]. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future update( Map channelData, [ Message? updateMessage, @@ -806,7 +960,18 @@ class Channel { ); } - /// Edit the channel custom data + /// A partial update can be used to set and unset specific custom data fields + /// when it is necessary to retain additional custom data fields on the + /// object. + /// + /// - [set] will add, or update existing attributes. + /// - [unset] will remove the attributes with the provided list of + /// values (keys). + /// + /// If you want to do a full update/replacement, use [update] instead. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future updatePartial({ Map? set, List? unset, @@ -821,25 +986,25 @@ class Channel { return _client.deleteChannel(id!, type); } - /// Removes all messages from the channel + /// Removes all messages from the channel. Future truncate() async { _checkInitialized(); return _client.truncateChannel(id!, type); } - /// Accept invitation to the channel + /// Accept invitation to the channel. Future acceptInvite([Message? message]) async { _checkInitialized(); return _client.acceptChannelInvite(id!, type, message: message); } - /// Reject invitation to the channel + /// Reject invitation to the channel. Future rejectInvite([Message? message]) async { _checkInitialized(); return _client.rejectChannelInvite(id!, type, message: message); } - /// Add members to the channel + /// Add members to the channel. Future addMembers( List memberIds, [ Message? message, @@ -848,7 +1013,7 @@ class Channel { return _client.addChannelMembers(id!, type, memberIds, message: message); } - /// Invite members to the channel + /// Invite members to the channel. Future inviteMembers( List memberIds, [ Message? message, @@ -857,7 +1022,7 @@ class Channel { return _client.inviteChannelMembers(id!, type, memberIds, message: message); } - /// Remove members from the channel + /// Remove members from the channel. Future removeMembers( List memberIds, [ Message? message, @@ -866,7 +1031,7 @@ class Channel { return _client.removeChannelMembers(id!, type, memberIds, message: message); } - /// Send action for a specific message of this channel + /// Send action for a specific message of this channel. Future sendAction( Message message, Map formData, @@ -913,9 +1078,10 @@ class Channel { return res; } - /// Mark all messages as read + /// Mark all messages as read. + /// /// Optionally provide a [messageId] if you want to mark a - /// particular message as read + /// particular message as read. Future markRead({String? messageId}) async { _checkInitialized(); client.state.totalUnreadCount = @@ -924,7 +1090,7 @@ class Channel { return _client.markChannelRead(id!, type, messageId: messageId); } - /// Loads the initial channel state and watches for changes + /// Loads the initial channel state and watches for changes. Future watch() async { ChannelState response; @@ -955,15 +1121,16 @@ class Channel { } } - /// Stop watching the channel + /// Stop watching the channel. Future stopWatching() async { _checkInitialized(); return _client.stopChannelWatching(id!, type); } - /// List the message replies for a parent message + /// List the message replies for a parent message. + /// /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// in the offline storage. Future getReplies( String parentId, { PaginationParams? options, @@ -987,7 +1154,7 @@ class Channel { return repliesResponse; } - /// List the reactions for a message in the channel + /// List the reactions for a message in the channel. Future getReactions( String messageId, { PaginationParams? pagination, @@ -997,7 +1164,7 @@ class Channel { pagination: pagination, ); - /// Retrieves a list of messages by ID + /// Retrieves a list of messages by given [messageIDs]. Future getMessagesById( List messageIDs, ) async { @@ -1008,7 +1175,7 @@ class Channel { return res; } - /// Retrieves a list of messages by ID + /// Translate a message by given [messageId] and [language]. Future translateMessage( String messageId, String language, @@ -1018,12 +1185,13 @@ class Channel { language, ); - /// Creates a new channel + /// Creates a new channel. Future create() async => query(state: false); - /// Query the API, get messages, members or other channel fields - /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// Query the API, get messages, members or other channel fields. + /// + /// Set [preferOffline] to true to avoid the API call if the data is already + /// in the offline storage. Future query({ bool state = true, bool watch = false, @@ -1077,7 +1245,7 @@ class Channel { } } - /// Query channel members + /// Query channel members. Future queryMembers({ Filter? filter, List? sort, @@ -1092,19 +1260,19 @@ class Channel { pagination: pagination, ); - /// Mutes the channel + /// Mutes the channel. Future mute({Duration? expiration}) { _checkInitialized(); return _client.muteChannel(cid!, expiration: expiration); } - /// Unmutes the channel + /// Unmute the channel. Future unmute() { _checkInitialized(); return _client.unmuteChannel(cid!); } - /// Bans a user from the channel + /// Bans the user with given [userID] from the channel. Future banUser( String userID, Map options, @@ -1118,7 +1286,7 @@ class Channel { return _client.banUser(userID, opts); } - /// Remove the ban for a user in the channel + /// Remove the ban for the user with given [userID] in the channel. Future unbanUser(String userID) async { _checkInitialized(); return _client.unbanUser(userID, { @@ -1127,7 +1295,7 @@ class Channel { }); } - /// Shadow bans a user from the channel + /// Shadow bans the user with the given [userID] from the channel. Future shadowBan( String userID, Map options, @@ -1141,7 +1309,7 @@ class Channel { return _client.shadowBan(userID, opts); } - /// Remove the shadow ban for a user in the channel + /// Remove the shadow ban for the user with the given [userID] in the channel. Future removeShadowBan(String userID) async { _checkInitialized(); return _client.removeShadowBan(userID, { @@ -1151,8 +1319,10 @@ class Channel { } /// Hides the channel from [StreamChatClient.queryChannels] for the user - /// until a message is added If [clearHistory] is set to true - all messages - /// will be removed for the user + /// until a message is added. + /// + /// If [clearHistory] is set to true - all messages + /// will be removed for the user. Future hide({bool clearHistory = false}) async { _checkInitialized(); final response = await _client.hideChannel( @@ -1170,7 +1340,7 @@ class Channel { return response; } - /// Removes the hidden status for the channel + /// Removes the hidden status for the channel. Future show() async { _checkInitialized(); return _client.showChannel(id!, type); @@ -1178,7 +1348,7 @@ class Channel { /// Stream of [Event] coming from websocket connection specific for the /// channel. Pass an eventType as parameter in order to filter just a type - /// of event + /// of event. Stream on([ String? eventType, String? eventType2, @@ -1216,7 +1386,7 @@ class Channel { } } - /// Sets last typing to null and sends the typing.stop event + /// Sets last typing to null and sends the typing.stop event. Future stopTyping([String? parentId]) async { if (config?.typingEvents == false) { return; @@ -1230,7 +1400,7 @@ class Channel { )); } - /// Call this method to dispose the channel client + /// Call this method to dispose the channel client. void dispose() { state?.dispose(); } @@ -1244,9 +1414,9 @@ class Channel { } } -/// The class that handles the state of the channel listening to the events +/// The class that handles the state of the channel listening to the events. class ChannelClientState { - /// Creates a new instance listening to events and updating the state + /// Creates a new instance listening to events and updating the state. ChannelClientState( this._channel, ChannelState channelState, @@ -1393,23 +1563,25 @@ class ChannelClientState { } /// Flag which indicates if [ChannelClientState] contain latest/recent messages or not. + /// /// This flag should be managed by UI sdks. - /// When false, any new message (received by WebSocket event - /// - [EventType.messageNew]) will not be pushed on to message list. + /// + /// When false, any new message received by WebSocket event + /// [EventType.messageNew] will not be pushed on to message list. bool get isUpToDate => _isUpToDateController.value; set isUpToDate(bool isUpToDate) => _isUpToDateController.add(isUpToDate); - /// [isUpToDate] flag count as a stream + /// [isUpToDate] flag count as a stream. Stream get isUpToDateStream => _isUpToDateController.stream; final BehaviorSubject _isUpToDateController = BehaviorSubject.seeded(true); - /// The retry queue associated to this channel + /// The retry queue associated to this channel. late final RetryQueue _retryQueue; - /// Retry failed message + /// Retry failed message. Future retryFailedMessages() async { final failedMessages = [...messages, ...threads.values.expand((v) => v)] @@ -1502,7 +1674,7 @@ class ChannelClientState { })); } - /// Add a message to this channel + /// Add a message to this channel. void addMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { final newMessages = List.from(_channelState.messages); @@ -1548,9 +1720,9 @@ class ChannelClientState { (event) { final readList = List.from(_channelState.read); final userReadIndex = - read?.indexWhere((r) => r.user.id == event.user!.id); + read.indexWhere((r) => r.user.id == event.user!.id); - if (userReadIndex != null && userReadIndex != -1) { + if (userReadIndex != -1) { final userRead = readList.removeAt(userReadIndex); if (userRead.user.id == _channel._client.state.currentUser!.id) { unreadCount = 0; @@ -1567,36 +1739,36 @@ class ChannelClientState { ); } - /// Channel message list + /// Channel message list. List get messages => _channelState.messages; - /// Channel message list as a stream - Stream?> get messagesStream => channelStateStream + /// Channel message list as a stream. + Stream> get messagesStream => channelStateStream .map((cs) => cs.messages) .distinct(const ListEquality().equals); - /// Channel pinned message list - List? get pinnedMessages => _channelState.pinnedMessages.toList(); + /// Channel pinned message list. + List get pinnedMessages => _channelState.pinnedMessages.toList(); - /// Channel pinned message list as a stream - Stream?> get pinnedMessagesStream => + /// Channel pinned message list as a stream. + Stream> get pinnedMessagesStream => channelStateStream.map((cs) => cs.pinnedMessages.toList()); - /// Get channel last message + /// Get channel last message. Message? get lastMessage => _channelState.messages.isNotEmpty == true ? _channelState.messages.last : null; - /// Get channel last message - Stream get lastMessageStream => messagesStream - .map((event) => event?.isNotEmpty == true ? event!.last : null); + /// Get channel last message. + Stream get lastMessageStream => + messagesStream.map((event) => event.isNotEmpty ? event.last : null); - /// Channel members list + /// Channel members list. List get members => _channelState.members .map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id])) .toList(); - /// Channel members list as a stream + /// Channel members list as a stream. Stream> get membersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.members), @@ -1605,19 +1777,19 @@ class ChannelClientState { members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(), ).distinct(const ListEquality().equals); - /// Channel watcher count + /// Channel watcher count. int? get watcherCount => _channelState.watcherCount; - /// Channel watcher count as a stream + /// Channel watcher count as a stream. Stream get watcherCountStream => channelStateStream.map((cs) => cs.watcherCount); - /// Channel watchers list + /// Channel watchers list. List get watchers => _channelState.watchers .map((e) => _channel.client.state.users[e.id] ?? e) .toList(); - /// Channel watchers list as a stream + /// Channel watchers list as a stream. Stream> get watchersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.watchers), @@ -1625,20 +1797,20 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); - /// Channel read list - List? get read => _channelState.read; + /// Channel read list. + List get read => _channelState.read; - /// Channel read list as a stream - Stream?> get readStream => channelStateStream.map((cs) => cs.read); + /// Channel read list as a stream. + Stream> get readStream => channelStateStream.map((cs) => cs.read); final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); set unreadCount(int value) => _unreadCountController.add(value); - /// Unread count getter as a stream + /// Unread count getter as a stream. Stream get unreadCountStream => _unreadCountController.stream.distinct(); - /// Unread count getter + /// Unread count getter. int get unreadCount => _unreadCountController.value; bool _countMessageAsUnread(Message message) { @@ -1654,7 +1826,7 @@ class ChannelClientState { !userIsMuted; } - /// Update threads with updated information about messages + /// Update threads with updated information about messages. void updateThreadInfo(String parentId, List messages) { final newThreads = Map>.from(threads); @@ -1676,7 +1848,7 @@ class ChannelClientState { _threads = newThreads; } - /// Delete all channel messages + /// Delete all channel messages. void truncate() { _channelState = _channelState.copyWith( messages: [], @@ -1685,7 +1857,7 @@ class ChannelClientState { final List _updatedMessagesIds = []; - /// Update channelState with updated information + /// Update channelState with updated information. void updateChannelState(ChannelState updatedState) { final newMessages = [ ...updatedState.messages, @@ -1737,13 +1909,13 @@ class ChannelClientState { int _sortByCreatedAt(Message a, Message b) => a.createdAt.compareTo(b.createdAt); - /// The channel state related to this client + /// The channel state related to this client. ChannelState get _channelState => _channelStateController.value; - /// The channel state related to this client as a stream + /// The channel state related to this client as a stream. Stream get channelStateStream => _channelStateController.stream; - /// The channel state related to this client + /// The channel state related to this client. ChannelState get channelState => _channelStateController.value; late BehaviorSubject _channelStateController; @@ -1754,11 +1926,11 @@ class ChannelClientState { _debouncedUpdatePersistenceChannelState.call([v]); } - /// The channel threads related to this channel + /// The channel threads related to this channel. Map> get threads => _threadsController.value.map((key, value) => MapEntry(key, value)); - /// The channel threads related to this channel as a stream + /// The channel threads related to this channel as a stream. Stream>> get threadsStream => _threadsController.stream; final BehaviorSubject>> _threadsController = @@ -1772,10 +1944,10 @@ class ChannelClientState { _threadsController.add(v); } - /// Channel related typing users last value + /// Channel related typing users last value. Map get typingEvents => _typingEventsController.value; - /// Channel related typing users stream + /// Channel related typing users stream. Stream> get typingEventsStream => _typingEventsController.stream; @@ -1880,7 +2052,7 @@ class ChannelClientState { .toList(); updateChannelState(_channelState.copyWith( - pinnedMessages: pinnedMessages!.where(_pinIsValid()).toList(), + pinnedMessages: pinnedMessages.where(_pinIsValid()).toList(), messages: expiredMessages, )); } @@ -1903,7 +2075,7 @@ class ChannelClientState { }); } - /// Call this method to dispose this object + /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); _unreadCountController.close(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 104b23fc..74abcb39 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -767,7 +767,9 @@ class StreamChatClient { cancelToken: cancelToken, ); - /// Replaces the [channelId] of type [ChannelType] data with [data] + /// Replaces the [channelId] of type [ChannelType] data with [data]. + /// + /// Use [updateChannelPartial] for a partial update. Future updateChannel( String channelId, String channelType, @@ -781,7 +783,10 @@ class StreamChatClient { message: message, ); - /// Updates the [channelId] of type [ChannelType] data with [data] + /// Partial update for the [channelId] of type [ChannelType]. Sets the + /// data provided in [set], and removes the attributes given in [unset]. + /// + /// Use [updateChannel] for a full update. Future updateChannelPartial( String channelId, String channelType, { diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 1ba7f333..c04f9683 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -84,7 +84,7 @@ class ChannelApi { /// Mark all channels for this user as read Future markAllRead() async { - final response = await _client.post('channels/read'); + final response = await _client.post('/channels/read'); return EmptyResponse.fromJson(response.data); } diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index fb5b1caf..a65052f4 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -7,11 +7,12 @@ import 'package:stream_chat/stream_chat.dart'; part 'own_user.g.dart'; -/// The class that defines the own user model -/// This object can be found in [Event] +/// The class that defines the own user model. +/// +/// This object can be found in [Event]. @JsonSerializable(createToJson: false) class OwnUser extends User { - /// Constructor used for json serialization + /// Constructor used for json serialization. OwnUser({ this.devices = const [], this.mutes = const [], @@ -20,6 +21,8 @@ class OwnUser extends User { this.channelMutes = const [], required String id, String? role, + String? name, + String? image, DateTime? createdAt, DateTime? updatedAt, DateTime? lastActive, @@ -31,6 +34,8 @@ class OwnUser extends User { }) : super( id: id, role: role, + name: name, + image: image, createdAt: createdAt, updatedAt: updatedAt, lastActive: lastActive, @@ -41,14 +46,16 @@ class OwnUser extends User { language: language, ); - /// Create a new instance from a json + /// Create a new instance from json. factory OwnUser.fromJson(Map json) => _$OwnUserFromJson( Serializer.moveToExtraDataFromRoot(json, topLevelFields)); - /// Create a new instance from [User] object + /// Create a new instance from [User] object. factory OwnUser.fromUser(User user) => OwnUser( id: user.id, role: user.role, + name: user.name, + image: user.image, createdAt: user.createdAt, updatedAt: user.updatedAt, lastActive: user.lastActive, @@ -64,6 +71,8 @@ class OwnUser extends User { OwnUser copyWith({ String? id, String? role, + String? name, + String? image, DateTime? createdAt, DateTime? updatedAt, DateTime? lastActive, @@ -80,8 +89,12 @@ class OwnUser extends User { }) => OwnUser( id: id ?? this.id, - banned: banned ?? this.banned, role: role ?? this.role, + /* if null, it will be retrieved from extraData['name']*/ + name: name, + /* if null, it will be retrieved from extraData['image']*/ + image: image, + banned: banned ?? this.banned, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, lastActive: lastActive ?? this.lastActive, @@ -101,16 +114,18 @@ class OwnUser extends User { OwnUser merge(OwnUser? other) { if (other == null) return this; return copyWith( + id: other.id, + role: other.role, + name: other.name, + image: other.image, banned: other.banned, channelMutes: other.channelMutes, createdAt: other.createdAt, devices: other.devices, extraData: other.extraData, - id: other.id, lastActive: other.lastActive, mutes: other.mutes, online: other.online, - role: other.role, teams: other.teams, totalUnreadCount: other.totalUnreadCount, unreadChannels: other.unreadChannels, @@ -119,27 +134,28 @@ class OwnUser extends User { ); } - /// List of user devices + /// List of user devices. @JsonKey(includeIfNull: false, defaultValue: []) final List devices; - /// List of users muted by the user + /// List of users muted by the user. @JsonKey(includeIfNull: false, defaultValue: []) final List mutes; - /// List of users muted by the user + /// List of users muted by the user. @JsonKey(includeIfNull: false, defaultValue: []) final List channelMutes; - /// Total unread messages by the user + /// Total unread messages by the user. @JsonKey(includeIfNull: false, defaultValue: 0) final int totalUnreadCount; - /// Total unread channels by the user + /// Total unread channels by the user. @JsonKey(includeIfNull: false) final int? unreadChannels; /// Known top level fields. + /// /// Useful for [Serializer] methods. static final topLevelFields = [ 'devices', diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index a7e7a51e..5a5ef8e5 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -4,29 +4,60 @@ import 'package:stream_chat/src/core/util/serializer.dart'; part 'user.g.dart'; -/// The class that defines the user model +/// Class that defines a Stream Chat User. @JsonSerializable() class User extends Equatable { - /// Constructor used for json serialization + /// Creates a new user. + /// + /// {@template name} + /// If an [name] is provided it will be set on [extraData] with a `key` + /// of 'name'. + /// + /// For example: + /// ```dart + /// final user = User(id: 'id', name: 'Sahil Kumar'); + /// print(user.name == user.extraData['name']); // true + /// ``` + /// {@endtemplate} + /// + /// {@template image} + /// If an [image] is provided it will be set on [extraData] with a `key` + /// of 'image'. + /// + /// For example: + /// ```dart + /// final user = User(id: 'id', image: 'https://getstream.io/image.png'); + /// print(user.image == user.extraData['image']); // true + /// ``` + /// {@endtemplate} User({ required this.id, this.role, + String? name, + String? image, DateTime? createdAt, DateTime? updatedAt, this.lastActive, + Map extraData = const {}, this.online = false, - this.extraData = const {}, this.banned = false, this.teams = const [], this.language, }) : createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + updatedAt = updatedAt ?? DateTime.now(), + /*For backwards compatibility, set 'name', 'image' in [extraData].*/ + extraData = { + ...extraData, + if (name != null) 'name': name, + if (image != null) 'image': image, + }; - /// Create a new instance from a json + /// Create a new instance from json. factory User.fromJson(Map json) => _$UserFromJson(Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// Known top level fields. + /// /// Useful for [Serializer] methods. static const topLevelFields = [ 'id', @@ -40,57 +71,13 @@ class User extends Equatable { 'language', ]; - /// User id + /// User id. final String id; - /// User role - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final String? role; - - /// User role - @JsonKey( - includeIfNull: false, - toJson: Serializer.readOnly, - defaultValue: [], - ) - final List teams; - - /// Date of user creation - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime createdAt; - - /// Date of last user update - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime updatedAt; - - /// Date of last user connection - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime? lastActive; - - /// True if user is online - @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) - final bool online; - - /// True if user is banned from the chat - @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) - final bool banned; - - /// Map of custom user extraData - @JsonKey( - includeIfNull: false, - defaultValue: {}, - ) - final Map extraData; - - /// The language this user prefers. + /// Shortcut for user name. /// - /// Defaults to 'en'. - @JsonKey(includeIfNull: false) - final String? language; - - /// Shortcut for user name + /// {@macro name} + @JsonKey(ignore: true) String get name { if (extraData.containsKey('name')) { final name = extraData['name']! as String; @@ -99,11 +86,62 @@ class User extends Equatable { return id; } - /// List of users to list of userIds + /// Shortcut for user image. + /// + /// {@macro image} + @JsonKey(ignore: true) + String? get image => extraData['image'] as String?; + + /// User role. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final String? role; + + /// User teams + @JsonKey( + includeIfNull: false, + toJson: Serializer.readOnly, + defaultValue: [], + ) + final List teams; + + /// Date of user creation. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final DateTime createdAt; + + /// Date of last user update. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final DateTime updatedAt; + + /// Date of last user connection. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final DateTime? lastActive; + + /// True if user is online. + @JsonKey( + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) + final bool online; + + /// True if user is banned from the chat. + @JsonKey( + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) + final bool banned; + + /// Map of custom user extraData. + @JsonKey( + includeIfNull: false, + defaultValue: {}, + ) + final Map extraData; + + /// The language this user prefers. + @JsonKey(includeIfNull: false) + final String? language; + + /// List of users to list of userIds. static List? toIds(List? users) => users?.map((u) => u.id).toList(); - /// Serialize to json + /// Serialize to json. Map toJson() => Serializer.moveFromExtraDataToRoot( _$UserToJson(this), ); @@ -112,6 +150,8 @@ class User extends Equatable { User copyWith({ String? id, String? role, + String? name, + String? image, DateTime? createdAt, DateTime? updatedAt, DateTime? lastActive, @@ -124,6 +164,10 @@ class User extends Equatable { User( id: id ?? this.id, role: role ?? this.role, + /* if null, it will be retrieved from extraData['name']*/ + name: name, + /* if null, it will be retrieved from extraData['image']*/ + image: image, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, lastActive: lastActive ?? this.lastActive, @@ -135,5 +179,5 @@ class User extends Equatable { ); @override - List get props => [id]; + List get props => [id, role]; } diff --git a/packages/stream_chat/lib/src/core/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart index dd3183d5..ab2f04d5 100644 --- a/packages/stream_chat/lib/src/core/models/user.g.dart +++ b/packages/stream_chat/lib/src/core/models/user.g.dart @@ -19,8 +19,8 @@ User _$UserFromJson(Map json) { lastActive: json['last_active'] == null ? null : DateTime.parse(json['last_active'] as String), - online: json['online'] as bool? ?? false, extraData: json['extra_data'] as Map? ?? {}, + online: json['online'] as bool? ?? false, banned: json['banned'] as bool? ?? false, teams: (json['teams'] as List?)?.map((e) => e as String).toList() ?? diff --git a/packages/stream_chat/test/fixtures/user.json b/packages/stream_chat/test/fixtures/user.json index b22c8553..146a4242 100644 --- a/packages/stream_chat/test/fixtures/user.json +++ b/packages/stream_chat/test/fixtures/user.json @@ -2,5 +2,16 @@ "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", "role": "test-role", "name": "John", + "image": "https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow", + "extraDataStringTest": "Extra data test", + "extraDataIntTest": 1, + "extraDataDoubleTest": 1.1, + "extraDataBoolTest": true, + "banned": true, + "online": true, + "teams": ["team-1", "team-2"], + "created_at": "2021-08-03 12:39:21.817646", + "updated_at": "2021-08-04 12:39:21.817646", + "last_active" : "2021-08-05 12:39:21.817646", "language": "en" } \ No newline at end of file diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 5be55491..7c5386c6 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -72,6 +72,40 @@ void main() { expect(channel.extraData.containsKey('name'), isTrue); expect(channel.extraData['name'], 'test-channel-name'); }); + + test('should be able to get and set `image`', () { + expect(channel.extraData.isEmpty, isTrue); + + const imageUrl = 'https://getstream.io/some-image'; + channel.image = imageUrl; + + expect(channel.image, imageUrl); + expect(channel.extraData['image'], imageUrl); + + const newImage = 'https://getstream.io/new-image'; + final newChannelInstance = + Channel(client, channelType, channelId, image: newImage); + + expect(newChannelInstance.image, newImage); + expect(newChannelInstance.extraData['image'], newImage); + }); + + test('should be able to get and set `name`', () { + expect(channel.extraData.isEmpty, isTrue); + + const name = 'Channel name'; + channel.name = name; + + expect(channel.name, name); + expect(channel.extraData['name'], name); + + const newName = 'New channel name'; + final newChannelInstance = + Channel(client, channelType, channelId, name: newName); + + expect(newChannelInstance.name, newName); + expect(newChannelInstance.extraData['name'], newName); + }); }); // TODO : test all persistence related logic in this group @@ -192,6 +226,22 @@ void main() { } }); + test('should throw if trying to set `image`', () { + try { + channel.image = 'https://stream.io/some-image'; + } catch (e) { + expect(e, isA()); + } + }); + + test('should throw if trying to set `name`', () { + try { + channel.name = 'New name'; + } catch (e) { + expect(e, isA()); + } + }); + group('`.sendMessage`', () { test('should work fine', () async { final message = Message(id: 'test-message-id'); @@ -1192,6 +1242,62 @@ void main() { message: any(named: 'message'))).called(1); }); + test('`.updateImage`', () async { + const image = 'https://getstream.io/new-image'; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: {'image': image}, + ); + + when(() => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + )).thenAnswer( + (_) async => PartialUpdateChannelResponse()..channel = channelModel, + ); + + final res = await channel.updateImage(image); + + expect(res, isNotNull); + expect(res.channel.extraData['image'], image); + + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + )).called(1); + }); + + test('`.updateName`', () async { + const name = 'Name'; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: {'name': name}, + ); + + when(() => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + )).thenAnswer( + (_) async => PartialUpdateChannelResponse()..channel = channelModel, + ); + + final res = await channel.updateName(name); + + expect(res, isNotNull); + expect(res.channel.extraData['name'], name); + + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + )).called(1); + }); + test('`.updatePartial`', () async { const set = { 'name': 'Stream Team', 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 c43f161f..8ffd778d 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 @@ -176,7 +176,7 @@ void main() { }); test('markAllRead', () async { - const path = 'channels/read'; + const path = '/channels/read'; when(() => client.post(path)).thenAnswer( (_) async => successResponse(path, data: {})); diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index 7aef859d..e1d12ada 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -1,10 +1,23 @@ +import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; import '../../utils.dart'; +class MockMute extends Mock implements Mute {} + +class MockDevice extends Mock implements Device {} + void main() { + final devices = [MockDevice(), MockDevice()]; + final mutes = [MockMute(), MockMute()]; + final channelMutes = [MockMute()]; + final createdAt = DateTime.parse('2021-05-03 12:39:21.817646'); + final updatedAt = DateTime.parse('2021-04-03 12:39:21.817646'); + final lastActive = DateTime.parse('2021-03-03 12:39:21.817646'); + group('src/models/own_user', () { test('should parse json correctly', () { final ownUser = OwnUser.fromJson(jsonFixture('own_user.json')); @@ -22,6 +35,7 @@ void main() { expect(ownUser.channelMutes.length, 1); expect(ownUser.totalUnreadCount, 0); expect(ownUser.unreadChannels, 0); + expect(ownUser.image, 'https://placehold.jp/150x150.png'); expect(ownUser.extraData['image'], 'https://placehold.jp/150x150.png'); expect(ownUser.extraData['name'], 'Proud darkness'); expect(ownUser.extraData['username'], 'Rioland'); @@ -40,6 +54,7 @@ void main() { expect(ownUser.online, user.online); expect(ownUser.banned, user.banned); expect(ownUser.extraData, user.extraData); + expect(ownUser.image, user.image); }); test('copyWith', () { @@ -49,35 +64,116 @@ void main() { expect(newUser.id, user.id); expect(newUser.role, user.role); expect(newUser.name, user.name); + expect(newUser.devices, user.devices); + expect(newUser.mutes, user.mutes); + expect(newUser.totalUnreadCount, user.totalUnreadCount); + expect(newUser.channelMutes, user.channelMutes); + expect(newUser.createdAt, user.createdAt); + expect(newUser.updatedAt, user.updatedAt); + expect(newUser.lastActive, user.lastActive); + expect(newUser.online, user.online); + expect(newUser.extraData, user.extraData); + expect(newUser.banned, user.banned); + expect(newUser.teams, user.teams); + expect(newUser.language, user.language); + expect(newUser.image, user.image); newUser = user.copyWith( id: 'test', role: 'test', + image: 'https://getstream.io/image-new', extraData: { 'name': 'test', }, + devices: devices, + mutes: mutes, + totalUnreadCount: 10, + unreadChannels: 5, + channelMutes: channelMutes, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: true, + banned: true, + teams: ['team1', 'team2'], + language: 'fr', ); expect(newUser.id, 'test'); expect(newUser.role, 'test'); expect(newUser.name, 'test'); + expect(newUser.image, 'https://getstream.io/image-new'); + expect( + newUser.extraData, + { + 'name': 'test', + 'image': 'https://getstream.io/image-new', + }, + reason: 'Should get image from user.image', + ); + expect(newUser.devices, devices); + expect(newUser.mutes, mutes); + expect(newUser.totalUnreadCount, 10); + expect(newUser.unreadChannels, 5); + expect(newUser.channelMutes, channelMutes); + expect(newUser.createdAt, createdAt); + expect(newUser.updatedAt, updatedAt); + expect(newUser.createdAt, createdAt); + expect(newUser.online, true); + expect(newUser.banned, true); + expect(newUser.teams, ['team1', 'team2']); + expect(newUser.language, 'fr'); }); test('merge', () { final user = OwnUser.fromJson(jsonFixture('own_user.json')); - final newUser = user.merge(OwnUser( - id: 'test', - role: 'test', - extraData: const { - 'name': 'test', - }, - banned: true, - )); + final newUser = user.merge( + OwnUser( + id: 'test', + role: 'test', + extraData: const { + 'name': 'test', + }, + image: 'https://getstream.io/image-new', + devices: devices, + mutes: mutes, + totalUnreadCount: 10, + unreadChannels: 5, + channelMutes: channelMutes, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: true, + banned: true, + teams: const ['team1', 'team2'], + language: 'fr', + ), + ); expect(newUser.id, 'test'); expect(newUser.role, 'test'); expect(newUser.name, 'test'); + expect(newUser.image, 'https://getstream.io/image-new'); + expect( + newUser.extraData, + { + 'name': 'test', + 'image': 'https://getstream.io/image-new', + }, + reason: 'Should get image from user.image', + ); + expect(newUser.devices, devices); + expect(newUser.mutes, mutes); + expect(newUser.totalUnreadCount, 10); + expect(newUser.unreadChannels, 5); + expect(newUser.channelMutes, channelMutes); + expect(newUser.createdAt, createdAt); + expect(newUser.updatedAt, updatedAt); + expect(newUser.createdAt, createdAt); + expect(newUser.online, true); expect(newUser.banned, true); + expect(newUser.teams, ['team1', 'team2']); + expect(newUser.language, 'fr'); }); }); } diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index 0891b548..fbe493ef 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -13,10 +13,11 @@ void main() { expect(reaction.type, 'wow'); expect( reaction.user?.toJson(), - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' - }).toJson(), + User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ).toJson(), ); expect(reaction.score, 1); expect(reaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); @@ -28,11 +29,11 @@ void main() { messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), type: 'wow', - user: - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' - }), + user: User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ), userId: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: {'bananas': 'yes'}, score: 1, @@ -58,10 +59,11 @@ void main() { expect(newReaction.type, 'wow'); expect( newReaction.user?.toJson(), - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan', - }).toJson(), + User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ).toJson(), ); expect(newReaction.score, 1); expect(newReaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index ce488cab..c8e04ede 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -4,21 +4,73 @@ import 'package:test/test.dart'; import '../../utils.dart'; void main() { + const id = 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'; + const role = 'test-role'; + const name = 'John'; + const image = + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow'; + const extraDataStringTest = 'Extra data test'; + const extraDataIntTest = 1; + const extraDataDoubleTest = 1.1; + const extraDataBoolTest = true; + const online = true; + const banned = true; + const teams = ['team-1', 'team-2']; + const createdAtString = '2021-08-03 12:39:21.817646'; + const updatedAtString = '2021-08-04 12:39:21.817646'; + const lastActiveString = '2021-08-05 12:39:21.817646'; + group('src/models/user', () { test('should parse json correctly', () { final user = User.fromJson(jsonFixture('user.json')); - expect(user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); - expect(user.name, 'John'); + expect(user.id, id); + expect(user.role, role); + expect(user.name, name); + expect(user.image, image); + expect(user.extraData['image'], image); + expect(user.extraData['extraDataStringTest'], extraDataStringTest); + expect(user.extraData['extraDataIntTest'], extraDataIntTest); + expect(user.extraData['extraDataDoubleTest'], extraDataDoubleTest); + expect(user.extraData['extraDataBoolTest'], extraDataBoolTest); + expect(user.online, online); + expect(user.banned, banned); + expect(user.teams, teams); + expect(user.createdAt, DateTime.parse(createdAtString)); + expect(user.updatedAt, DateTime.parse(updatedAtString)); + expect(user.lastActive, DateTime.parse(lastActiveString)); + expect(user.language, 'en'); }); test('should serialize to json correctly', () { final user = User( - id: 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', - role: 'abc', + id: id, + role: role, + name: name, + image: image, + extraData: const { + 'extraDataStringTest': extraDataStringTest, + 'extraDataIntTest': extraDataIntTest, + 'extraDataDoubleTest': extraDataDoubleTest, + 'extraDataBoolTest': extraDataBoolTest, + }, + createdAt: DateTime.parse(createdAtString), + updatedAt: DateTime.parse(updatedAtString), + lastActive: DateTime.parse(lastActiveString), + banned: online, + online: banned, + teams: const ['team-1', 'team-2'], + language: 'fr', ); expect(user.toJson(), { - 'id': 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', + 'id': id, + 'name': name, + 'image': image, + 'extraDataStringTest': extraDataStringTest, + 'extraDataIntTest': extraDataIntTest, + 'extraDataDoubleTest': extraDataDoubleTest, + 'extraDataBoolTest': extraDataBoolTest, + 'language': 'fr', }); }); @@ -29,18 +81,141 @@ void main() { expect(newUser.id, user.id); expect(newUser.role, user.role); expect(newUser.name, user.name); + expect(newUser.image, user.image); + expect(newUser.online, user.online); + expect(newUser.banned, user.banned); + expect(newUser.teams, user.teams); + expect(newUser.createdAt, user.createdAt); + expect(newUser.updatedAt, user.updatedAt); + expect(newUser.lastActive, user.lastActive); + expect(newUser.language, user.language); newUser = user.copyWith( id: 'test', role: 'test', - extraData: { - 'name': 'test', - }, + name: 'test', + image: 'https://stream.io/new-image', + online: false, + banned: false, + teams: ['new-team1', 'new-team2'], + createdAt: DateTime.parse('2021-05-03 12:39:21.817646'), + updatedAt: DateTime.parse('2021-05-04 12:39:21.817646'), + lastActive: DateTime.parse('2021-05-06 12:39:21.817646'), + language: 'it', ); expect(newUser.id, 'test'); expect(newUser.role, 'test'); expect(newUser.name, 'test'); + expect(newUser.image, 'https://stream.io/new-image'); + expect(newUser.extraData['image'], 'https://stream.io/new-image'); + expect(newUser.online, false); + expect(newUser.banned, false); + expect(newUser.teams, ['new-team1', 'new-team2']); + expect(newUser.createdAt, DateTime.parse('2021-05-03 12:39:21.817646')); + expect(newUser.updatedAt, DateTime.parse('2021-05-04 12:39:21.817646')); + expect(newUser.lastActive, DateTime.parse('2021-05-06 12:39:21.817646')); + expect(newUser.language, 'it'); + }); + + test('name property and extraData manipulation', () { + final user = User(id: id, name: name); + + expect(user.name, name); + expect(user.extraData['name'], name); + expect(user.toJson(), {'id': id, 'name': name}); + expect(User.fromJson(user.toJson()).toJson(), {'id': id, 'name': name}); + + const nameOne = 'Name One'; + var newUser = user.copyWith( + extraData: {'name': nameOne}, + ); + + expect(newUser.extraData['name'], nameOne); + expect(newUser.name, nameOne); + + const nameTwo = 'Name Two'; + newUser = user.copyWith( + name: nameTwo, + ); + + expect(newUser.extraData['name'], nameTwo); + expect(newUser.name, nameTwo); + + const nameThree = 'Name Three'; + newUser = user.copyWith( + name: nameThree, + extraData: {'name': nameThree}, + ); + + expect(newUser.extraData['name'], nameThree); + expect(newUser.name, nameThree); + }); + + test('image property and extraData manipulation', () { + final user = User(id: id, image: image); + + expect(user.image, image); + expect(user.extraData['image'], image); + expect(user.toJson(), {'id': id, 'image': image}); + expect(User.fromJson(user.toJson()).toJson(), {'id': id, 'image': image}); + + const imageURLOne = 'https://stream.io/image-one'; + var newUser = user.copyWith( + extraData: {'image': imageURLOne}, + ); + + expect(newUser.extraData['image'], imageURLOne); + expect(newUser.image, imageURLOne); + + const imageURLTwo = 'https://stream.io/image-two'; + newUser = user.copyWith( + image: imageURLTwo, + ); + + expect(newUser.extraData['image'], imageURLTwo); + expect(newUser.image, imageURLTwo); + + const imageURLThree = 'https://stream.io/image-three'; + newUser = user.copyWith( + image: imageURLThree, + extraData: {'image': imageURLThree}, + ); + + expect(newUser.extraData['image'], imageURLThree); + expect(newUser.image, imageURLThree); + }); + + test('default values, constructor', () { + final user = User(id: id); + + expect(user.id, id); + expect(user.role, null); + expect(user.name, id, reason: 'if a name is not supplied, default to id'); + expect(user.image, null); + expect(user.extraData, const {}); + expect(user.online, false); + expect(user.banned, false); + expect(user.teams, []); + expect(user.lastActive, null); + expect(user.createdAt, isNotNull); + expect(user.updatedAt, isNotNull); + }); + + test('default values, parse json', () { + final user = User.fromJson(const {'id': id}); + + expect(user.id, id); + expect(user.role, null); + expect(user.name, id, reason: 'if a name is not supplied, default to id'); + expect(user.image, null); + expect(user.extraData, const {}); + expect(user.online, false); + expect(user.banned, false); + expect(user.teams, []); + expect(user.lastActive, null); + expect(user.createdAt, isNotNull); + expect(user.updatedAt, isNotNull); }); }); } diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index de55a3ca..b21acc7a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,59 @@ +## Upcoming + +тЬЕ Added + +- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): + Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image + is loading +- Added a `backgroundColor` property to the following widgets: + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `GalleryFooter` + - `ThreadHeader` + +- Added `MessageInput.attachmentButtonBuilder` and `MessageInput.commandButtonBuilder` for more + customizations. + +```dart +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, + ); +``` + +> **_NOTE:_** The last parameter is the default `ActionButton` +You can call `.copyWith` to customize just a subset of properties. + +ЁЯФД Changed + +Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with +them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming +breakdown: + +* `AvatarTheme` is now `AvatarThemeData` +* `ChannelHeaderTheme` is now `ChannelHeaderThemeData` +* `ChannelListHeaderTheme` is now `ChannelListHeaderThemeData` +* `ChannelListViewTheme` is now `ChannelListViewThemeData` +* `ChannelPreviewTheme` is now `ChannelPreviewThemeData` +* `MessageInputTheme` is now `MessageInputThemeData` +* `MessageListViewTheme` is now `MessageListViewTheme` +* `MessageSearchListViewTheme` is now `MessageSearchListViewThemeData` +* `MessageTheme` is now `MessageThemeData` +* `UserListViewTheme` is now `UserListViewThemeData` + +ЁЯРЮ Fixed + +- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. +- Fixed date dividers position/alignment in non reversed `MessageListView`. + +## 2.1.2 + +ЁЯРЮ Fixed + +- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no + members when sending message + ## 2.1.1 - Updated core dependency @@ -14,7 +70,8 @@ ЁЯФД Changed - `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`. -- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`. +- `StreamChat.of(context).userStream` is now deprecated in favor + of `StreamChat.of(context).currentUserStream`. ЁЯРЮ Fixed @@ -28,17 +85,17 @@ - Renamed `ChannelImage` to `ChannelAvatar` - Updated `StreamChatThemeData.reactionIcons` to accept custom builder - Renamed `ColorTheme` properties to reflect the purpose of the colors - - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` - - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` - - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` - - `ColorTheme.greyWhisper` -> `ColorTheme.borders` - - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` - - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` - - `ColorTheme.white` -> `ColorTheme.barsBg` - - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` - - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` - - `ColorTheme.accentRed` -> `ColorTheme.accentError` - - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + - `ColorTheme.greyWhisper` -> `ColorTheme.borders` + - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + - `ColorTheme.white` -> `ColorTheme.barsBg` + - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + - `ColorTheme.accentRed` -> `ColorTheme.accentError` + - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` - `ChannelListCore` options property is removed in favor of individual properties - `options.state` -> bool state @@ -59,7 +116,7 @@ typedef MessageBuilder = Widget Function( ); ``` -the last parameter is the default `MessageWidget` +> **_NOTE:_** the last parameter is the default `MessageWidget` You can call `.copyWith` to customize just a subset of properties @@ -67,7 +124,8 @@ You can call `.copyWith` to customize just a subset of properties - Added video compress options (frame and quality) to `MessageInput` - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView + header/footer - `MessageWidget` accepts a `userAvatarBuilder` - Added pinMessage ui support - Added `MessageListView.threadSeparatorBuilder` property @@ -76,10 +134,12 @@ You can call `.copyWith` to customize just a subset of properties ЁЯРЮ Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing - message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text + box when editing message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator + use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without + a reload - `MessageListView` not rendering if the user is not a member of the channel - Fix `MessageInput` overflow when there are no actions - Minor fixes and improvements @@ -89,17 +149,17 @@ You can call `.copyWith` to customize just a subset of properties ЁЯЫСя╕П Breaking Changes from `2.0.0-nullsafety.8` - Renamed `ColorTheme` properties to reflect the purpose of the colors - - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` - - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` - - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` - - `ColorTheme.greyWhisper` -> `ColorTheme.borders` - - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` - - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` - - `ColorTheme.white` -> `ColorTheme.barsBg` - - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` - - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` - - `ColorTheme.accentRed` -> `ColorTheme.accentError` - - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + - `ColorTheme.greyWhisper` -> `ColorTheme.borders` + - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + - `ColorTheme.white` -> `ColorTheme.barsBg` + - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + - `ColorTheme.accentRed` -> `ColorTheme.accentError` + - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` тЬЕ Added @@ -126,21 +186,24 @@ typedef MessageBuilder = Widget Function( ); ``` -the last parameter is the default `MessageWidget` -You can call `.copyWith` to customize just a subset of properties +> **_NOTE:_** The last parameter is the default `MessageWidget` +You can call `.copyWith` to customize just a subset of properties. тЬЕ Added - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView + header/footer - `MessageWidget` accepts a `userAvatarBuilder` ЁЯРЮ Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing - message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text + box when editing message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator + use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without + a reload - `MessageListView` not rendering if the user is not a member of the channel ## 2.0.0-nullsafety.7 @@ -210,7 +273,8 @@ You can call `.copyWith` to customize just a subset of properties - Show error messages as system and keep them in the message input - Remove notification badge logic - Use shimmer while loading images -- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput` +- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated + to `MessageInput` - Add possibility to specify custom message actions using `MessageWidget.customActions` - Added `MessageListView.onAttachmentTap` callback - Fixed message newline issue @@ -267,7 +331,8 @@ You can call `.copyWith` to customize just a subset of properties - Improved api documentation - Updated `stream_chat` dependency to `^1.0.0-beta` - Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples) -- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) +- Reimplemented existing widgets + using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) ## 0.2.21 @@ -284,8 +349,8 @@ You can call `.copyWith` to customize just a subset of properties ## 0.2.20+2 -- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message - arrives +- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the + list when a new message arrives ## 0.2.20+1 @@ -319,7 +384,8 @@ You can call `.copyWith` to customize just a subset of properties ## 0.2.16 -- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation +- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress + implementation - Make public autofocus field of the TextField of message_input ## 0.2.15 @@ -504,10 +570,11 @@ You can call `.copyWith` to customize just a subset of properties ## 0.2.1-alpha+1 -- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget - as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of - your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to - every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more +- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have + the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to + your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in + the usual way, but you need to add a `StreamChat` widget to every route of your app. + Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information. ```dart @@ -525,6 +592,7 @@ Widget build(BuildContext context) { }, home: ChannelListPage(), ); +} ``` - Fix reaction bubble going below previous message on iOS @@ -608,8 +676,8 @@ Widget build(BuildContext context) { - Add gesture (vertical drag down) to close the keyboard -- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the - keyboard) +- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will + even close the keyboard) The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261 diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 1589c909..eb03f89f 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -22,8 +22,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); 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 9658eec4..824c0efa 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -118,13 +118,12 @@ class ChannelListPage extends StatelessWidget { channel: channel, ), title: ChannelName( - textStyle: - StreamChatTheme.of(context).channelPreviewTheme.title!.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(opacity), - ), + textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(opacity), + ), ), subtitle: Text(subtitle), trailing: channel.state!.unreadCount > 0 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 cf94b568..b64281cb 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -58,17 +58,17 @@ class MyApp extends StatelessWidget { final defaultTheme = StreamChatThemeData.fromTheme(themeData); final colorTheme = defaultTheme.colorTheme; final customTheme = defaultTheme.merge(StreamChatThemeData( - channelPreviewTheme: ChannelPreviewTheme( - avatarTheme: AvatarTheme( + channelPreviewTheme: ChannelPreviewThemeData( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), - otherMessageTheme: MessageTheme( + otherMessageTheme: MessageThemeData( messageBackgroundColor: colorTheme.textHighEmphasis, - messageText: TextStyle( + messageTextStyle: TextStyle( color: colorTheme.barsBg, ), - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), 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 b068a8f9..a7d4d2e3 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -12,7 +13,7 @@ class AttachmentTitle extends StatelessWidget { }) : super(key: key); /// Theme to apply to text - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// Attachment data to display final Attachment attachment; @@ -34,7 +35,7 @@ class AttachmentTitle extends StatelessWidget { Text( attachment.title!, overflow: TextOverflow.ellipsis, - style: messageTheme.messageText?.copyWith( + style: messageTheme.messageTextStyle?.copyWith( color: StreamChatTheme.of(context).colorTheme.accentPrimary, fontWeight: FontWeight.bold, ), @@ -50,7 +51,7 @@ class AttachmentTitle extends StatelessWidget { .toList() .reversed .join('.'), - style: messageTheme.messageText, + style: messageTheme.messageTextStyle, ), ], ), 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 20174567..95229418 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -4,6 +4,7 @@ import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -27,8 +28,8 @@ class ImageAttachment extends AttachmentWidget { size: size, ); - /// [MessageTheme] for showing image title - final MessageTheme messageTheme; + /// [MessageThemeData] for showing image title + final MessageThemeData messageTheme; /// Flag for showing title final bool showTitle; 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 b743cda1..ce4ac804 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; 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/src/full_screen_media.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -24,8 +25,8 @@ class VideoAttachment extends AttachmentWidget { size: size, ); - /// [MessageTheme] for showing title - final MessageTheme messageTheme; + /// [MessageThemeData] for showing title + final MessageThemeData messageTheme; /// Callback when show message is tapped final ShowMessageCallback? onShowMessage; 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 a06da0a8..5723234d 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -19,6 +19,7 @@ class _ChannelBottomSheetState extends State { bool _showActions = true; late StreamChannelState _streamChannelState; + late ChannelPreviewThemeData _channelPreviewThemeData; late StreamChatThemeData _streamChatThemeData; late StreamChatState _streamChatState; @@ -64,8 +65,7 @@ class _ChannelBottomSheetState extends State { child: ChannelInfo( showTypingIndicator: false, channel: _streamChannelState.channel, - textStyle: - _streamChatThemeData.channelPreviewTheme.subtitle, + textStyle: _channelPreviewThemeData.subtitleStyle, ), ), const SizedBox( @@ -213,6 +213,7 @@ class _ChannelBottomSheetState extends State { void didChangeDependencies() { _streamChannelState = StreamChannel.of(context); _streamChatThemeData = StreamChatTheme.of(context); + _channelPreviewThemeData = ChannelPreviewTheme.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 5f6275a8..ada04271 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -67,6 +67,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { this.subtitle, this.leading, this.actions, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -102,10 +103,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the [ChannelAvatar] final List? actions; + /// The background color for this [ChannelHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; - final chatThemeData = StreamChatTheme.of(context); + final channelHeaderTheme = ChannelHeaderTheme.of(context); final leadingWidget = leading ?? (showBackButton @@ -141,18 +145,17 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { brightness: Theme.of(context).brightness, elevation: 1, leading: leadingWidget, - backgroundColor: - chatThemeData.channelTheme.channelHeaderTheme.color, + backgroundColor: backgroundColor ?? channelHeaderTheme.color, actions: actions ?? [ Padding( padding: const EdgeInsets.only(right: 10), child: Center( child: ChannelAvatar( - borderRadius: chatThemeData.channelTheme - .channelHeaderTheme.avatarTheme?.borderRadius, - constraints: chatThemeData.channelTheme - .channelHeaderTheme.avatarTheme?.constraints, + borderRadius: + channelHeaderTheme.avatarTheme?.borderRadius, + constraints: + channelHeaderTheme.avatarTheme?.constraints, onTap: onImageTap, ), ), @@ -169,16 +172,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { children: [ title ?? ChannelName( - textStyle: chatThemeData - .channelTheme.channelHeaderTheme.title, + textStyle: channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), subtitle ?? ChannelInfo( showTypingIndicator: showTypingIndicator, channel: channel, - textStyle: chatThemeData - .channelTheme.channelHeaderTheme.subtitle, + 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 67a59782..de9d74b9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -65,10 +65,7 @@ class ChannelInfo extends StatelessWidget { } alternativeWidget = Text( text, - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .subtitle, + style: ChannelHeaderTheme.of(context).subtitleStyle, ); } else { final userId = StreamChat.of(context).currentUser?.id; 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 835f51a2..4ce8b0d2 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -2,10 +2,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for title typedef TitleBuilder = Widget Function( @@ -46,7 +46,7 @@ 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 [ChannelListHeaderTheme] property. +/// [StreamChatTheme] and on its [ChannelListHeaderThemeData] property. /// Modify it to change the widget appearance. class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// Instantiates a ChannelListHeader @@ -61,6 +61,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { this.subtitle, this.leading, this.actions, + this.backgroundColor, }) : super(key: key); /// Pass this if you don't have a [StreamChatClient] in your widget tree. @@ -93,6 +94,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the new chat button final List? actions; + /// The background color for this [ChannelListHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; @@ -116,6 +120,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } final chatThemeData = StreamChatTheme.of(context); + final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); return InfoTile( showMessage: showConnectionStateTile && showStatus, message: statusString, @@ -123,7 +128,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, - backgroundColor: chatThemeData.channelListHeaderTheme.color, + backgroundColor: + backgroundColor ?? channelListHeaderThemeData.color, centerTitle: true, leading: leading ?? Center( @@ -138,10 +144,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } Scaffold.of(context).openDrawer(); }, - borderRadius: chatThemeData - .channelListHeaderTheme.avatarTheme?.borderRadius, - constraints: chatThemeData - .channelListHeaderTheme.avatarTheme?.constraints, + borderRadius: channelListHeaderThemeData + .avatarTheme?.borderRadius, + constraints: channelListHeaderThemeData + .avatarTheme?.constraints, ) : const Offstage(), ), @@ -227,10 +233,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { const SizedBox(width: 10), Text( context.translations.searchingForNetworkText, - style: StreamChatTheme.of(context) - .channelListHeaderTheme - .title - ?.copyWith( + style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, ), @@ -243,12 +246,13 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { StreamChatClient client, ) { final chatThemeData = StreamChatTheme.of(context); + final channelListHeaderTheme = ChannelListHeaderTheme.of(context); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( context.translations.offlineLabel, - style: chatThemeData.channelListHeaderTheme.title?.copyWith( + style: channelListHeaderTheme.titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, ), @@ -259,7 +263,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ..openConnection(), child: Text( context.translations.tryAgainLabel, - style: chatThemeData.channelListHeaderTheme.title?.copyWith( + style: channelListHeaderTheme.titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, color: chatThemeData.colorTheme.accentPrimary, 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 42c7420b..d6cd454b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -5,6 +5,7 @@ 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/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/extension.dart'; @@ -583,11 +584,16 @@ class _ChannelListViewState extends State { ), ], child: widget.channelPreviewBuilder?.call(context, channel) ?? - ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), - onTap: (channel) => onTap(channel, widget.channelWidget), + DecoratedBox( + decoration: BoxDecoration( + color: chatThemeData.channelListViewTheme.backgroundColor, + ), + child: ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: () => widget.onImageTap?.call(channel), + onTap: (channel) => onTap(channel, widget.channelWidget), + ), ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 7fc59c7f..e01cd008 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -3,10 +3,10 @@ import 'package:collection/collection.dart' import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) @@ -69,7 +69,7 @@ class ChannelPreview extends StatelessWidget { @override Widget build(BuildContext context) { - final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme; + final channelPreviewTheme = ChannelPreviewTheme.of(context); final streamChatState = StreamChat.of(context); return BetterStreamBuilder( stream: channel.isMutedStream, @@ -91,7 +91,7 @@ class ChannelPreview extends StatelessWidget { Flexible( child: title ?? ChannelName( - textStyle: channelPreviewTheme.title, + textStyle: channelPreviewTheme.titleStyle, ), ), BetterStreamBuilder?>( @@ -132,7 +132,7 @@ class ChannelPreview extends StatelessWidget { message: lastMessage!, size: channelPreviewTheme.indicatorIconSize, isMessageRead: channel.state!.read - ?.where((element) => + .where((element) => element.user.id != channel .client.state.currentUser!.id) @@ -183,14 +183,13 @@ class ChannelPreview extends StatelessWidget { return Text( stringDate, - style: - StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, + style: ChannelPreviewTheme.of(context).lastMessageAtStyle, ); }, ); Widget _buildSubtitle(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); if (channel.isMuted) { return Row( crossAxisAlignment: CrossAxisAlignment.end, @@ -200,7 +199,7 @@ class ChannelPreview extends StatelessWidget { ), Text( ' ${context.translations.channelIsMutedText}', - style: chatThemeData.channelPreviewTheme.subtitle, + style: channelPreviewTheme.subtitleStyle, ), ], ); @@ -208,7 +207,7 @@ class ChannelPreview extends StatelessWidget { return TypingIndicator( channel: channel, alternativeWidget: _buildLastMessage(context), - style: chatThemeData.channelPreviewTheme.subtitle, + style: channelPreviewTheme.subtitleStyle, ); } @@ -243,19 +242,19 @@ class ChannelPreview extends StatelessWidget { text = parts.join(' '); - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text, lastMessage.mentionedUsers, lastMessage.attachments, - chatThemeData.channelPreviewTheme.subtitle?.copyWith( - color: chatThemeData.channelPreviewTheme.subtitle?.color, + channelPreviewTheme.subtitleStyle?.copyWith( + color: channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal), - chatThemeData.channelPreviewTheme.subtitle?.copyWith( - color: chatThemeData.channelPreviewTheme.subtitle?.color, + channelPreviewTheme.subtitleStyle?.copyWith( + color: channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 0a6c337a..09d725bb 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.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/theme/themes.dart'; /// Widget to display deleted message class DeletedMessage extends StatelessWidget { @@ -15,7 +16,7 @@ class DeletedMessage extends StatelessWidget { }) : super(key: key); /// The theme of the message - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// The border radius of the message text final BorderRadiusGeometry? borderRadiusGeometry; @@ -51,9 +52,9 @@ class DeletedMessage extends StatelessWidget { ), child: Text( context.translations.messageDeletedLabel, - style: messageTheme.messageText?.copyWith( + style: messageTheme.messageTextStyle?.copyWith( fontStyle: FontStyle.italic, - color: messageTheme.createdAt?.color, + color: messageTheme.createdAtStyle?.color, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index b975941e..19bf4523 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -46,9 +46,9 @@ extension PlatformFileX on PlatformFile { ); } -/// +/// Extension on [InputDecoration] extension InputDecorationX on InputDecoration { - /// + /// Merges this [InputDecoration] with the [other] InputDecoration merge(InputDecoration? other) { if (other == null) return this; return copyWith( @@ -123,3 +123,50 @@ extension FlipBorder on BorderRadius { bottomRight: bottomLeft) : this; } + +/// Extension on [IconButton] +extension IconButtonX on IconButton { + /// Creates a copy of [IconButton] with specified attributes overridden. + IconButton copyWith({ + double? iconSize, + VisualDensity? visualDensity, + EdgeInsetsGeometry? padding, + AlignmentGeometry? alignment, + double? splashRadius, + Color? color, + Color? focusColor, + Color? hoverColor, + Color? highlightColor, + Color? splashColor, + Color? disabledColor, + void Function()? onPressed, + MouseCursor? mouseCursor, + FocusNode? focusNode, + bool? autofocus, + String? tooltip, + bool? enableFeedback, + BoxConstraints? constraints, + Widget? icon, + }) => + IconButton( + iconSize: iconSize ?? this.iconSize, + visualDensity: visualDensity ?? this.visualDensity, + padding: padding ?? this.padding, + alignment: alignment ?? this.alignment, + splashRadius: splashRadius ?? this.splashRadius, + color: color ?? this.color, + focusColor: focusColor ?? this.focusColor, + hoverColor: hoverColor ?? this.hoverColor, + highlightColor: highlightColor ?? this.highlightColor, + splashColor: splashColor ?? this.splashColor, + disabledColor: disabledColor ?? this.disabledColor, + onPressed: onPressed ?? this.onPressed, + mouseCursor: mouseCursor ?? this.mouseCursor, + focusNode: focusNode ?? this.focusNode, + autofocus: autofocus ?? this.autofocus, + tooltip: tooltip ?? this.tooltip, + enableFeedback: enableFeedback ?? this.enableFeedback, + constraints: constraints ?? this.constraints, + icon: icon ?? this.icon, + ); +} 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 5e498a32..bef2a531 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -124,10 +124,7 @@ class _FullScreenMediaState extends State ), backgroundDecoration: BoxDecoration( color: ColorTween( - begin: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .color, + begin: ChannelHeaderTheme.of(context).color, end: Colors.black, ).lerp(_controller.value), ), diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index 6df301d3..2b2858f8 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -26,6 +27,7 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { this.totalPages = 0, this.mediaAttachments = const [], this.mediaSelectedCallBack, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -54,6 +56,9 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// Callback when media is selected final ValueChanged? mediaSelectedCallBack; + /// The background color of this [GalleryFooter]. + final Color? backgroundColor; + @override _GalleryFooterState createState() => _GalleryFooterState(); @@ -89,7 +94,8 @@ class _GalleryFooterState extends State { context: context, removeTop: true, child: BottomAppBar( - color: galleryFooterThemeData.backgroundColor, + color: + widget.backgroundColor ?? galleryFooterThemeData.backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index e0852800..74eeb4ee 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/attachment_actions_modal.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_core/stream_chat_flutter_core.dart'; /// Header/AppBar widget for media display screen @@ -18,6 +19,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { this.onImageTap, this.userName = '', this.sentAt = '', + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -49,6 +51,9 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Stores the current index of media shown final int currentIndex; + /// The background color of this [GalleryHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final galleryHeaderThemeData = GalleryHeaderTheme.of(context); @@ -65,7 +70,8 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { onPressed: onBackPressed, ) : const SizedBox(), - backgroundColor: galleryHeaderThemeData.backgroundColor, + backgroundColor: + backgroundColor ?? galleryHeaderThemeData.backgroundColor, actions: [ if (!message.isEphemeral) IconButton( diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index 4647fe57..627406d0 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -25,8 +26,8 @@ class ImageGroup extends StatelessWidget { /// Message which images are attached to final Message message; - /// [MessageTheme] to apply to message - final MessageTheme messageTheme; + /// [MessageThemeData] to apply to message + final MessageThemeData messageTheme; /// Size of iamges final Size size; 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 d392c5c5..387e31a1 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/message_action.dart'; import 'package:stream_chat_flutter/src/reaction_picker.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/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -49,8 +50,8 @@ class MessageActionsModal extends StatefulWidget { /// Message in focus for actions final Message message; - /// [MessageTheme] for message - final MessageTheme messageTheme; + /// [MessageThemeData] for message + final MessageThemeData messageTheme; /// Flag for showing reactions final bool showReactions; @@ -116,7 +117,7 @@ class _MessageActionsModalState extends State { } } final roughSentenceSize = messageTextLength * - (widget.messageTheme.messageText?.fontSize ?? 1) * + (widget.messageTheme.messageTextStyle?.fontSize ?? 1) * 1.2; final divFactor = widget.message.attachments.isNotEmpty == true ? 1 @@ -565,7 +566,7 @@ class _MessageActionsModalState extends State { elevation: 2, clipBehavior: Clip.hardEdge, isScrollControlled: true, - backgroundColor: streamChatThemeData.messageInputTheme.inputBackground, + backgroundColor: MessageInputTheme.of(context).inputBackgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index feb5bd6d..2adae0bd 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -50,6 +50,15 @@ typedef MentionTileBuilder = Widget Function( Member member, ); +/// 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, +); + /// Location for actions on the [MessageInput] enum ActionsLocation { /// Align to left @@ -164,6 +173,8 @@ class MessageInput extends StatefulWidget { this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoFrameRate = 30, this.onError, + this.attachmentButtonBuilder, + this.commandButtonBuilder, }) : super(key: key); /// Message to edit @@ -247,6 +258,18 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; + /// 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; + @override MessageInputState createState() => MessageInputState(); @@ -288,6 +311,7 @@ class MessageInputState extends State { late final TextEditingController textEditingController; late StreamChatThemeData _streamChatTheme; + late MessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => widget.quotedMessage != null; @@ -328,7 +352,7 @@ class MessageInputState extends State { Widget build(BuildContext context) { Widget child = DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.messageInputTheme.inputBackground, + color: _messageInputTheme.inputBackgroundColor, ), child: SafeArea( child: GestureDetector( @@ -402,11 +426,11 @@ class MessageInputState extends State { children: [ if (!_commandEnabled && widget.actionsLocation == ActionsLocation.left) - _buildExpandActionsButton(), + _buildExpandActionsButton(context), _buildTextInput(context), if (!_commandEnabled && widget.actionsLocation == ActionsLocation.right) - _buildExpandActionsButton(), + _buildExpandActionsButton(context), if (widget.sendButtonLocation == SendButtonLocation.outside) _animateSendButton(context), ], @@ -484,12 +508,12 @@ class MessageInputState extends State { : CrossFadeState.showSecond, firstChild: sendButton, secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), - duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, + duration: _messageInputTheme.sendAnimationDuration!, alignment: Alignment.center, ); } - Widget _buildExpandActionsButton() { + Widget _buildExpandActionsButton(BuildContext context) { final channel = StreamChannel.of(context).channel; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -509,7 +533,7 @@ class MessageInputState extends State { ? pi : 0, child: StreamSvgIcon.emptyCircleLeft( - color: _streamChatTheme.messageInputTheme.expandButtonColor, + color: _messageInputTheme.expandButtonColor, ), ), padding: const EdgeInsets.all(0), @@ -527,12 +551,13 @@ class MessageInputState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (!widget.disableAttachments) _buildAttachmentButton(), + if (!widget.disableAttachments) + _buildAttachmentButton(context), if (widget.showCommandsButton && widget.editMessage == null && channel.state != null && channel.config?.commands.isNotEmpty == true) - _buildCommandButton(), + _buildCommandButton(context), ...widget.actions ?? [], ].insertBetween(const SizedBox(width: 8)), ), @@ -555,17 +580,17 @@ class MessageInputState extends State { clipBehavior: Clip.hardEdge, margin: margin, decoration: BoxDecoration( - borderRadius: _streamChatTheme.messageInputTheme.borderRadius, + borderRadius: _messageInputTheme.borderRadius, gradient: _focusNode.hasFocus - ? _streamChatTheme.messageInputTheme.activeBorderGradient - : _streamChatTheme.messageInputTheme.idleBorderGradient, + ? _messageInputTheme.activeBorderGradient + : _messageInputTheme.idleBorderGradient, ), child: Padding( padding: const EdgeInsets.all(1.5), child: DecoratedBox( decoration: BoxDecoration( - borderRadius: _streamChatTheme.messageInputTheme.borderRadius, - color: _streamChatTheme.messageInputTheme.inputBackground, + borderRadius: _messageInputTheme.borderRadius, + color: _messageInputTheme.inputBackgroundColor, ), child: Column( mainAxisSize: MainAxisSize.min, @@ -583,7 +608,7 @@ class MessageInputState extends State { keyboardType: widget.keyboardType, controller: textEditingController, focusNode: _focusNode, - style: _streamChatTheme.messageInputTheme.inputTextStyle, + style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, textAlignVertical: TextAlignVertical.center, decoration: _getInputDecoration(context), @@ -599,11 +624,11 @@ class MessageInputState extends State { } InputDecoration _getInputDecoration(BuildContext context) { - final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration; + final passedDecoration = _messageInputTheme.inputDecoration; return InputDecoration( isDense: true, hintText: _getHint(context), - hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( + hintStyle: _messageInputTheme.inputTextStyle!.copyWith( color: _streamChatTheme.colorTheme.textLowEmphasis, ), border: const OutlineInputBorder( @@ -668,9 +693,7 @@ class MessageInputState extends State { : (widget.actionsLocation == ActionsLocation.leftInside ? Row( mainAxisSize: MainAxisSize.min, - children: [ - _buildExpandActionsButton(), - ], + children: [_buildExpandActionsButton(context)], ) : null), suffixIconConstraints: const BoxConstraints.tightFor(height: 40), @@ -696,7 +719,7 @@ class MessageInputState extends State { ), if (!_commandEnabled && widget.actionsLocation == ActionsLocation.rightInside) - _buildExpandActionsButton(), + _buildExpandActionsButton(context), if (widget.sendButtonLocation == SendButtonLocation.inside) _animateSendButton(context), ], @@ -1680,16 +1703,15 @@ class MessageInputState extends State { } } - Widget _buildCommandButton() { + Widget _buildCommandButton(BuildContext context) { final s = textEditingController.text.trim(); - - return IconButton( + final defaultButton = IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty ? _streamChatTheme.colorTheme.disabled : (_commandsOverlay != null - ? _streamChatTheme.messageInputTheme.actionButtonColor - : _streamChatTheme.messageInputTheme.actionButtonIdleColor), + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor), ), padding: const EdgeInsets.all(0), constraints: const BoxConstraints.tightFor( @@ -1721,38 +1743,46 @@ class MessageInputState extends State { } }, ); + + return widget.commandButtonBuilder?.call(context, defaultButton) ?? + defaultButton; } - Widget _buildAttachmentButton() => IconButton( - icon: StreamSvgIcon.attach( - color: _openFilePickerSection - ? _streamChatTheme.messageInputTheme.actionButtonColor - : _streamChatTheme.messageInputTheme.actionButtonIdleColor, - ), - padding: const EdgeInsets.all(0), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - _emojiOverlay?.remove(); - _emojiOverlay = null; - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; + 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 { + _emojiOverlay?.remove(); + _emojiOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); - } else { - showAttachmentModal(); - } - }, - ); + if (_openFilePickerSection) { + setState(() { + _openFilePickerSection = false; + _filePickerSize = _kMinMediaPickerSize; + }); + } else { + showAttachmentModal(); + } + }, + ); + + return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } /// Show the attachment modal, making the user choose where to /// pick a media from @@ -1868,15 +1898,14 @@ class MessageInputState extends State { } else if (fileType == DefaultAttachmentTypes.video) { pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); } - if (pickedFile == null) { - return; + if (pickedFile != null) { + final bytes = await pickedFile.readAsBytes(); + file = AttachmentFile( + size: bytes.length, + path: pickedFile.path, + bytes: bytes, + ); } - final bytes = await pickedFile.readAsBytes(); - file = AttachmentFile( - size: bytes.length, - path: pickedFile.path, - bytes: bytes, - ); } else { late FileType type; if (fileType == DefaultAttachmentTypes.image) { @@ -1963,7 +1992,7 @@ class MessageInputState extends State { padding: const EdgeInsets.all(8), child: StreamSvgIcon( assetName: _getIdleSendIcon(), - color: _streamChatTheme.messageInputTheme.sendButtonIdleColor, + color: _messageInputTheme.sendButtonIdleColor, ), ); @@ -1979,7 +2008,7 @@ class MessageInputState extends State { ), icon: StreamSvgIcon( assetName: _getSendIcon(), - color: _streamChatTheme.messageInputTheme.sendButtonColor, + color: _messageInputTheme.sendButtonColor, ), ), ); @@ -2186,6 +2215,7 @@ class MessageInputState extends State { @override void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); + _messageInputTheme = MessageInputTheme.of(context); if (widget.editMessage != null && !_initialized) { FocusScope.of(context).requestFocus(_focusNode); _initialized = true; 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 5ae6f9ea..75d45707 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/src/message_widget.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/swipeable.dart'; import 'package:stream_chat_flutter/src/system_message.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -504,8 +505,14 @@ class _MessageListViewState extends State { if (i == 1 || i == itemCount - 4) return const Offstage(); - final message = messages[i - 1]; - final nextMessage = messages[i - 2]; + late final Message message, nextMessage; + if (widget.reverse) { + message = messages[i - 1]; + nextMessage = messages[i - 2]; + } else { + message = messages[i - 2]; + nextMessage = messages[i - 1]; + } if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), Units.DAY, @@ -628,15 +635,14 @@ class _MessageListViewState extends State { child: Text( context.translations.threadSeparatorText(replyCount), textAlign: TextAlign.center, - style: _streamTheme.channelTheme.channelHeaderTheme.subtitle, + style: ChannelHeaderTheme.of(context).subtitleStyle, ), ), ); } Positioned _buildFloatingDateDivider(int itemCount) => Positioned( - top: widget.reverse ? 20 : null, - bottom: widget.reverse ? null : 20, + top: 20, left: 0, right: 0, child: BetterStreamBuilder>( @@ -646,19 +652,36 @@ class _MessageListViewState extends State { if (a == null || b == null) { return false; } - final aTop = _getTopElementIndex(a); - final bTop = _getTopElementIndex(b); - return aTop == bTop; + if (widget.reverse) { + final aTop = _getTopElementIndex(a); + final bTop = _getTopElementIndex(b); + return aTop == bTop; + } else { + final aBottom = _getBottomElementIndex(a); + final bBottom = _getBottomElementIndex(b); + return aBottom == bBottom; + } }, builder: (context, values) { if (values.isEmpty || messages.isEmpty) { return const Offstage(); } - final index = _getTopElementIndex(values); + int? index; + if (widget.reverse) { + index = _getTopElementIndex(values); + } else { + index = _getBottomElementIndex(values); + } - if (index == null || index <= 2 || index >= itemCount - 3) { - return const Offstage(); + if (index == null) return const Offstage(); + + if (index <= 2 || index >= itemCount - 3) { + if (widget.reverse) { + index = itemCount - 4; + } else { + index = 2; + } } final message = messages[index - 2]; @@ -684,6 +707,15 @@ class _MessageListViewState extends State { .index; } + int? _getBottomElementIndex(Iterable values) { + final inView = values.where((position) => position.itemLeadingEdge < 1); + if (inView.isEmpty) return null; + return inView + .reduce((min, position) => + position.itemLeadingEdge < min.itemLeadingEdge ? position : min) + .index; + } + Widget _buildScrollToBottom() => StreamBuilder>( stream: Rx.combineLatest2( streamChannel!.channel.state!.isUpToDateStream.distinct(), @@ -914,7 +946,7 @@ class _MessageListViewState extends State { } final channel = streamChannel!.channel; - final readList = channel.state?.read?.where((read) { + final readList = channel.state?.read.where((read) { if (read.user.id == userId) return false; return read.lastRead.isAfter(message.createdAt) || read.lastRead.isAtSameMomentAs(message.createdAt); @@ -965,7 +997,7 @@ class _MessageListViewState extends State { final currentUser = StreamChat.of(context).currentUser; final members = StreamChannel.of(context).channel.state?.members ?? []; final currentUserMember = - members.firstWhere((e) => e.user!.id == currentUser!.id); + members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); Widget messageWidget = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), @@ -1072,7 +1104,8 @@ class _MessageListViewState extends State { } FocusScope.of(context).unfocus(); }, - showPinButton: widget.pinPermissions.contains(currentUserMember.role), + showPinButton: currentUserMember != null && + widget.pinPermissions.contains(currentUserMember.role), ); if (widget.messageBuilder != null) { @@ -1201,8 +1234,7 @@ class _MessageListViewState extends State { MaterialPageRoute( builder: (_) => BetterStreamBuilder( stream: streamChannel!.channel.state!.messagesStream.map( - (messages) => - messages!.firstWhere((m) => m.id == message.id)), + (messages) => messages.firstWhere((m) => m.id == message.id)), initialData: message, builder: (_, data) => StreamChannel( channel: streamChannel!.channel, 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 b7e37bad..d2c69e4b 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -28,8 +29,8 @@ class MessageReactionsModal extends StatelessWidget { /// Message to display reactions of final Message message; - /// [MessageTheme] to apply to [message] - final MessageTheme messageTheme; + /// [MessageThemeData] to apply to [message] + final MessageThemeData messageTheme; /// Flag to reverse message final bool reverse; @@ -56,8 +57,9 @@ class MessageReactionsModal extends StatelessWidget { messageTextLength = quotedMessageLength; } } - final roughSentenceSize = - messageTextLength * (messageTheme.messageText?.fontSize ?? 1) * 1.2; + final roughSentenceSize = messageTextLength * + (messageTheme.messageTextStyle?.fontSize ?? 1) * + 1.2; final divFactor = message.attachments.isNotEmpty == true ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); 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 31c53d5a..ab41532c 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -36,7 +36,7 @@ class MessageSearchItem extends StatelessWidget { final channel = getMessageResponse.channel; final channelName = channel?.extraData['name']; final user = message.user!; - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); return ListTile( onTap: onTap, leading: UserAvatar( @@ -53,18 +53,18 @@ class MessageSearchItem extends StatelessWidget { user.id == StreamChat.of(context).currentUser?.id ? context.translations.youText : user.name, - style: chatThemeData.channelPreviewTheme.title, + style: channelPreviewTheme.titleStyle, ), if (channelName != null) ...[ Text( ' ${context.translations.inText} ', - style: chatThemeData.channelPreviewTheme.title?.copyWith( + style: channelPreviewTheme.titleStyle?.copyWith( fontWeight: FontWeight.normal, ), ), Text( channelName as String, - style: chatThemeData.channelPreviewTheme.title, + style: channelPreviewTheme.titleStyle, ), ], ], @@ -94,7 +94,7 @@ class MessageSearchItem extends StatelessWidget { return Text( stringDate, - style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, + style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAtStyle, ); } @@ -122,18 +122,18 @@ class MessageSearchItem extends StatelessWidget { text = parts.join(' '); } - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text!, message.mentionedUsers, message.attachments, - chatThemeData.channelPreviewTheme.subtitle?.copyWith( + channelPreviewTheme.subtitleStyle?.copyWith( fontStyle: (message.isSystem || message.isDeleted) ? FontStyle.italic : FontStyle.normal, ), - chatThemeData.channelPreviewTheme.subtitle?.copyWith( + channelPreviewTheme.subtitleStyle?.copyWith( fontStyle: (message.isSystem || message.isDeleted) ? FontStyle.italic : FontStyle.normal, 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 1eae16cd..0fd45ea1 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,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/extension.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 345b0096..c1aad231 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -1,6 +1,7 @@ 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/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -24,8 +25,8 @@ class MessageText extends StatelessWidget { /// Callback for when link is tapped final void Function(String)? onLinkTap; - /// [MessageTheme] whose text theme is to be applied - final MessageTheme messageTheme; + /// [MessageThemeData] whose text theme is to be applied + final MessageThemeData messageTheme; @override Widget build(BuildContext context) { @@ -66,16 +67,16 @@ class MessageText extends StatelessWidget { styleSheet: MarkdownStyleSheet.fromTheme( themeData.copyWith( textTheme: themeData.textTheme.apply( - bodyColor: messageTheme.messageText?.color, - decoration: messageTheme.messageText?.decoration, - decorationColor: messageTheme.messageText?.decorationColor, - decorationStyle: messageTheme.messageText?.decorationStyle, - fontFamily: messageTheme.messageText?.fontFamily, + bodyColor: messageTheme.messageTextStyle?.color, + decoration: messageTheme.messageTextStyle?.decoration, + decorationColor: messageTheme.messageTextStyle?.decorationColor, + decorationStyle: messageTheme.messageTextStyle?.decorationStyle, + fontFamily: messageTheme.messageTextStyle?.fontFamily, ), ), ).copyWith( - a: messageTheme.messageLinks, - p: messageTheme.messageText, + a: messageTheme.messageLinksStyle, + p: messageTheme.messageTextStyle, ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 6982d0a0..19af5b2a 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -290,7 +291,7 @@ class MessageWidget extends StatefulWidget { final Message message; /// The message theme - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -422,7 +423,7 @@ class MessageWidget extends StatefulWidget { Widget Function(BuildContext, Message)? deletedBottomRowBuilder, void Function(BuildContext, Message)? onMessageActions, Message? message, - MessageTheme? messageTheme, + MessageThemeData? messageTheme, bool? reverse, ShapeBorder? shape, ShapeBorder? attachmentShape, @@ -893,14 +894,14 @@ class _MessageWidgetState extends State ), InkWell( onTap: widget.onThreadTap != null ? onThreadTap : null, - child: Text(msg, style: widget.messageTheme.replies), + child: Text(msg, style: widget.messageTheme.repliesStyle), ), ], if (showUsername) _buildUsername(usernameKey), if (showTimeStamp) Text( Jiffy(widget.message.createdAt.toLocal()).jm, - style: widget.messageTheme.createdAt, + style: widget.messageTheme.createdAtStyle, ), if (showSendingIndicator) _buildSendingIndicator(), ]); @@ -917,7 +918,7 @@ class _MessageWidgetState extends State Container( margin: EdgeInsets.only( bottom: context.textScaleFactor * - ((widget.messageTheme.replies?.fontSize ?? 1) / 2), + ((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2), ), child: CustomPaint( size: const Size(16, 32) * context.textScaleFactor, @@ -944,7 +945,7 @@ class _MessageWidgetState extends State Container( margin: EdgeInsets.only( bottom: context.textScaleFactor * - ((widget.messageTheme.replies?.fontSize ?? 1) / 2), + ((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2), ), child: CustomPaint( size: const Size(16, 32) * context.textScaleFactor, @@ -967,7 +968,7 @@ class _MessageWidgetState extends State widget.message.user!.name, maxLines: 1, key: usernameKey, - style: widget.messageTheme.messageAuthor, + style: widget.messageTheme.messageAuthorStyle, overflow: TextOverflow.ellipsis, ); } @@ -1191,7 +1192,7 @@ class _MessageWidgetState extends State } Widget _buildSendingIndicator() { - final style = widget.messageTheme.createdAt; + final style = widget.messageTheme.createdAtStyle; final message = widget.message; if (hasNonUrlAttachments && @@ -1271,8 +1272,8 @@ class _MessageWidgetState extends State onMentionTap: widget.onMentionTap, messageTheme: isOnlyEmoji ? widget.messageTheme.copyWith( - messageText: - widget.messageTheme.messageText!.copyWith( + messageTextStyle: + widget.messageTheme.messageTextStyle!.copyWith( fontSize: 42, )) : widget.messageTheme, 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 22ca4d4c..ad3d936a 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:video_player/video_player.dart'; @@ -73,7 +74,7 @@ class QuotedMessageWidget extends StatelessWidget { final Message message; /// The message theme - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -138,11 +139,11 @@ class QuotedMessageWidget extends StatelessWidget { message: msg, messageTheme: isOnlyEmoji && _containsText ? messageTheme.copyWith( - messageText: messageTheme.messageText?.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( fontSize: 32, )) : messageTheme.copyWith( - messageText: messageTheme.messageText?.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( fontSize: 12, )), ), 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 9d2df266..f7a05d7e 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,11 +1,10 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/channel_header.dart'; +import 'package:flutter/material.dart' hide TextTheme; import 'package:stream_chat_flutter/src/channel_preview.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/gradient_avatar.dart'; import 'package:stream_chat_flutter/src/message_input.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -48,13 +47,14 @@ class StreamChatThemeData { Brightness? brightness, TextTheme? textTheme, ColorTheme? colorTheme, - ChannelListHeaderTheme? channelListHeaderTheme, - ChannelPreviewTheme? channelPreviewTheme, - ChannelTheme? channelTheme, - MessageTheme? otherMessageTheme, - MessageTheme? ownMessageTheme, - MessageInputTheme? messageInputTheme, + ChannelListHeaderThemeData? channelListHeaderTheme, + ChannelPreviewThemeData? channelPreviewTheme, + ChannelHeaderThemeData? channelHeaderTheme, + MessageThemeData? otherMessageTheme, + MessageThemeData? ownMessageTheme, + MessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, + Widget Function(BuildContext, User)? placeholderUserImage, IconThemeData? primaryIconTheme, List? reactionIcons, GalleryHeaderThemeData? imageHeaderTheme, @@ -69,7 +69,7 @@ class StreamChatThemeData { textTheme ??= isDark ? TextTheme.dark() : TextTheme.light(); colorTheme ??= isDark ? ColorTheme.dark() : ColorTheme.light(); - final defaultData = fromColorAndTextTheme( + final defaultData = StreamChatThemeData.fromColorAndTextTheme( colorTheme, textTheme, ); @@ -77,11 +77,12 @@ class StreamChatThemeData { final customizedData = defaultData.copyWith( channelListHeaderTheme: channelListHeaderTheme, channelPreviewTheme: channelPreviewTheme, - channelTheme: channelTheme, + channelHeaderTheme: channelHeaderTheme, otherMessageTheme: otherMessageTheme, ownMessageTheme: ownMessageTheme, messageInputTheme: messageInputTheme, defaultUserImage: defaultUserImage, + placeholderUserImage: placeholderUserImage, primaryIconTheme: primaryIconTheme, reactionIcons: reactionIcons, galleryHeaderTheme: imageHeaderTheme, @@ -109,11 +110,12 @@ class StreamChatThemeData { required this.colorTheme, required this.channelListHeaderTheme, required this.channelPreviewTheme, - required this.channelTheme, + required this.channelHeaderTheme, required this.otherMessageTheme, required this.ownMessageTheme, required this.messageInputTheme, required this.defaultUserImage, + this.placeholderUserImage, required this.primaryIconTheme, required this.reactionIcons, required this.galleryHeaderTheme, @@ -136,170 +138,42 @@ class StreamChatThemeData { return defaultTheme.merge(customizedTheme); } - /// The text themes used in the widgets - final TextTheme textTheme; - - /// The color themes used in the widgets - final ColorTheme colorTheme; - - /// Theme of the [ChannelPreview] - final ChannelPreviewTheme channelPreviewTheme; - - /// Theme of the [ChannelListHeader] - final ChannelListHeaderTheme channelListHeaderTheme; - - /// Theme of the chat widgets dedicated to a channel - final ChannelTheme channelTheme; - - /// The default style for [GalleryHeader]s below the overall - /// [StreamChatTheme]. - final GalleryHeaderThemeData galleryHeaderTheme; - - /// The default style for [GalleryFooter]s below the overall - /// [StreamChatTheme]. - final GalleryFooterThemeData galleryFooterTheme; - - /// Theme of the current user messages - final MessageTheme ownMessageTheme; - - /// Theme of other users messages - final MessageTheme otherMessageTheme; - - /// Theme dedicated to the [MessageInput] widget - final MessageInputTheme messageInputTheme; - - /// The widget that will be built when the user image is unavailable - final Widget Function(BuildContext, User) defaultUserImage; - - /// Primary icon theme - final IconThemeData primaryIconTheme; - - /// Assets used for rendering reactions - final List reactionIcons; - - /// Theme configuration for the [MessageListView] widget. - final MessageListViewThemeData messageListViewTheme; - - /// Theme configuration for the [ChannelListView] widget. - final ChannelListViewThemeData channelListViewTheme; - - /// Theme configuration for the [UserListView] widget. - final UserListViewThemeData userListViewTheme; - - /// Theme configuration for the [] widget. - final MessageSearchListViewThemeData messageSearchListViewTheme; - - /// Creates a copy of [StreamChatThemeData] with specified attributes - /// overridden. - StreamChatThemeData copyWith({ - TextTheme? textTheme, - ColorTheme? colorTheme, - ChannelPreviewTheme? channelPreviewTheme, - ChannelTheme? channelTheme, - MessageTheme? ownMessageTheme, - MessageTheme? otherMessageTheme, - MessageInputTheme? messageInputTheme, - Widget Function(BuildContext, User)? defaultUserImage, - IconThemeData? primaryIconTheme, - ChannelListHeaderTheme? channelListHeaderTheme, - List? reactionIcons, - GalleryHeaderThemeData? galleryHeaderTheme, - GalleryFooterThemeData? galleryFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, - }) => - StreamChatThemeData.raw( - channelListHeaderTheme: - this.channelListHeaderTheme.merge(channelListHeaderTheme), - textTheme: this.textTheme.merge(textTheme), - colorTheme: this.colorTheme.merge(colorTheme), - primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), - defaultUserImage: defaultUserImage ?? this.defaultUserImage, - channelPreviewTheme: - this.channelPreviewTheme.merge(channelPreviewTheme), - channelTheme: this.channelTheme.merge(channelTheme), - ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), - otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), - messageInputTheme: this.messageInputTheme.merge(messageInputTheme), - reactionIcons: reactionIcons ?? this.reactionIcons, - galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, - galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, - messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, - channelListViewTheme: channelListViewTheme ?? this.channelListViewTheme, - userListViewTheme: userListViewTheme ?? this.userListViewTheme, - messageSearchListViewTheme: - messageSearchListViewTheme ?? this.messageSearchListViewTheme, - ); - - /// Merge themes - StreamChatThemeData merge(StreamChatThemeData? other) { - if (other == null) return this; - return copyWith( - channelListHeaderTheme: - channelListHeaderTheme.merge(other.channelListHeaderTheme), - textTheme: textTheme.merge(other.textTheme), - colorTheme: colorTheme.merge(other.colorTheme), - primaryIconTheme: other.primaryIconTheme, - defaultUserImage: other.defaultUserImage, - channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme), - channelTheme: channelTheme.merge(other.channelTheme), - ownMessageTheme: ownMessageTheme.merge(other.ownMessageTheme), - otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), - messageInputTheme: messageInputTheme.merge(other.messageInputTheme), - reactionIcons: other.reactionIcons, - galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), - galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), - messageListViewTheme: - messageListViewTheme.merge(other.messageListViewTheme), - channelListViewTheme: - channelListViewTheme.merge(other.channelListViewTheme), - userListViewTheme: userListViewTheme.merge(other.userListViewTheme), - messageSearchListViewTheme: - messageSearchListViewTheme.merge(other.messageSearchListViewTheme), - ); - } - /// Create theme from color and text theme - // ignore: prefer_constructors_over_static_methods - static StreamChatThemeData fromColorAndTextTheme( + factory StreamChatThemeData.fromColorAndTextTheme( ColorTheme colorTheme, TextTheme textTheme, ) { final accentColor = colorTheme.accentPrimary; final iconTheme = IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); - final channelTheme = ChannelTheme( - channelHeaderTheme: ChannelHeaderTheme( - avatarTheme: AvatarTheme( - borderRadius: BorderRadius.circular(20), - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - color: colorTheme.barsBg, - title: textTheme.headlineBold, - subtitle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), - ), - ), - ); - final channelPreviewTheme = ChannelPreviewTheme( - unreadCounterColor: colorTheme.accentError, - avatarTheme: AvatarTheme( + final channelHeaderTheme = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - title: textTheme.bodyBold, - subtitle: textTheme.footnote.copyWith( + color: colorTheme.barsBg, + titleStyle: textTheme.headlineBold, + subtitleStyle: textTheme.footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAt: textTheme.footnote.copyWith( + ); + final channelPreviewTheme = ChannelPreviewThemeData( + unreadCounterColor: colorTheme.accentError, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + titleStyle: textTheme.bodyBold, + subtitleStyle: textTheme.footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAtStyle: textTheme.footnote.copyWith( color: colorTheme.textHighEmphasis.withOpacity(.5), ), indicatorIconSize: 16, @@ -315,8 +189,8 @@ class StreamChatThemeData { ), ), channelPreviewTheme: channelPreviewTheme, - channelListHeaderTheme: ChannelListHeaderTheme( - avatarTheme: AvatarTheme( + channelListHeaderTheme: ChannelListHeaderThemeData( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -324,48 +198,48 @@ class StreamChatThemeData { ), ), color: colorTheme.barsBg, - title: textTheme.headlineBold, + titleStyle: textTheme.headlineBold, ), - channelTheme: channelTheme, - ownMessageTheme: MessageTheme( - messageAuthor: + channelHeaderTheme: channelHeaderTheme, + ownMessageTheme: MessageThemeData( + messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - messageText: textTheme.body, - createdAt: + messageTextStyle: textTheme.body, + createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - replies: textTheme.footnoteBold.copyWith(color: accentColor), + repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), messageBackgroundColor: colorTheme.disabled, reactionsBackgroundColor: colorTheme.barsBg, reactionsBorderColor: colorTheme.borders, reactionsMaskColor: colorTheme.appBg, messageBorderColor: colorTheme.disabled, - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, width: 32, ), ), - messageLinks: TextStyle( + messageLinksStyle: TextStyle( color: accentColor, ), ), - otherMessageTheme: MessageTheme( + otherMessageTheme: MessageThemeData( reactionsBackgroundColor: colorTheme.disabled, reactionsBorderColor: colorTheme.barsBg, reactionsMaskColor: colorTheme.appBg, - messageText: textTheme.body, - createdAt: + messageTextStyle: textTheme.body, + createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - messageAuthor: + messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - replies: textTheme.footnoteBold.copyWith(color: accentColor), - messageLinks: TextStyle( + repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), + messageLinksStyle: TextStyle( color: accentColor, ), messageBackgroundColor: colorTheme.barsBg, messageBorderColor: colorTheme.borders, - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -373,7 +247,7 @@ class StreamChatThemeData { ), ), ), - messageInputTheme: MessageInputTheme( + messageInputTheme: MessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), actionButtonColor: colorTheme.accentPrimary, @@ -381,7 +255,7 @@ class StreamChatThemeData { expandButtonColor: colorTheme.accentPrimary, sendButtonColor: colorTheme.accentPrimary, sendButtonIdleColor: colorTheme.disabled, - inputBackground: colorTheme.barsBg, + inputBackgroundColor: colorTheme.barsBg, inputTextStyle: textTheme.body, idleBorderGradient: LinearGradient( colors: [ @@ -460,10 +334,10 @@ class StreamChatThemeData { ], galleryHeaderTheme: GalleryHeaderThemeData( closeButtonColor: colorTheme.textHighEmphasis, - backgroundColor: channelTheme.channelHeaderTheme.color, + backgroundColor: channelHeaderTheme.color, iconMenuPointColor: colorTheme.textHighEmphasis, titleTextStyle: textTheme.headlineBold, - subtitleTextStyle: channelPreviewTheme.subtitle, + subtitleTextStyle: channelPreviewTheme.subtitleStyle, bottomSheetBarrierColor: colorTheme.overlay, ), galleryFooterTheme: GalleryFooterThemeData( @@ -490,1704 +364,135 @@ class StreamChatThemeData { ), ); } -} -/// Class for holding text theme -class TextTheme { - /// Initialise light text theme - TextTheme.light({ - this.title = const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - this.headlineBold = const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - this.headline = const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - this.bodyBold = const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - this.body = const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - this.footnoteBold = const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - this.footnote = const TextStyle( - fontSize: 12, - color: Colors.black, - ), - this.captionBold = const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - }); + /// The text themes used in the widgets + final TextTheme textTheme; - /// Initialise with dark theme - TextTheme.dark({ - this.title = const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - this.headlineBold = const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - this.headline = const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - this.bodyBold = const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - this.body = const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - this.footnoteBold = const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - this.footnote = const TextStyle( - fontSize: 12, - color: Colors.white, - ), - this.captionBold = const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - }); + /// The color themes used in the widgets + final ColorTheme colorTheme; - /// Text theme for title - final TextStyle title; + /// Theme of the [ChannelPreview] + final ChannelPreviewThemeData channelPreviewTheme; - /// Body Text theme for headline - final TextStyle headlineBold; + /// Theme of the [ChannelListHeader] + final ChannelListHeaderThemeData channelListHeaderTheme; - /// Text theme for headline - final TextStyle headline; + /// Theme of the chat widgets dedicated to a channel header + final ChannelHeaderThemeData channelHeaderTheme; - /// Bold Text theme for body - final TextStyle bodyBold; + /// The default style for [GalleryHeader]s below the overall + /// [StreamChatTheme]. + final GalleryHeaderThemeData galleryHeaderTheme; - /// Text theme body - final TextStyle body; + /// The default style for [GalleryFooter]s below the overall + /// [StreamChatTheme]. + final GalleryFooterThemeData galleryFooterTheme; - /// Bold Text theme for footnote - final TextStyle footnoteBold; + /// Theme of the current user messages + final MessageThemeData ownMessageTheme; - /// Text theme for footnote - final TextStyle footnote; + /// Theme of other users messages + final MessageThemeData otherMessageTheme; - /// Bold Text theme for caption - final TextStyle captionBold; + /// Theme dedicated to the [MessageInput] widget + final MessageInputThemeData messageInputTheme; - /// Copy with theme - TextTheme copyWith({ - Brightness brightness = Brightness.light, - TextStyle? body, - TextStyle? title, - TextStyle? headlineBold, - TextStyle? headline, - TextStyle? bodyBold, - TextStyle? footnoteBold, - TextStyle? footnote, - TextStyle? captionBold, + /// The widget that will be built when the user image is unavailable + final Widget Function(BuildContext, User) defaultUserImage; + + /// The widget that will be built when the user image is loading + final Widget Function(BuildContext, User)? placeholderUserImage; + + /// Primary icon theme + final IconThemeData primaryIconTheme; + + /// Assets used for rendering reactions + final List reactionIcons; + + /// Theme configuration for the [MessageListView] widget. + final MessageListViewThemeData messageListViewTheme; + + /// Theme configuration for the [ChannelListView] widget. + final ChannelListViewThemeData channelListViewTheme; + + /// Theme configuration for the [UserListView] widget. + final UserListViewThemeData userListViewTheme; + + /// Theme configuration for the [MessageSearchListView] widget. + final MessageSearchListViewThemeData 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, + 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, }) => - brightness == Brightness.light - ? TextTheme.light( - body: body ?? this.body, - title: title ?? this.title, - headlineBold: headlineBold ?? this.headlineBold, - headline: headline ?? this.headline, - bodyBold: bodyBold ?? this.bodyBold, - footnoteBold: footnoteBold ?? this.footnoteBold, - footnote: footnote ?? this.footnote, - captionBold: captionBold ?? this.captionBold, - ) - : TextTheme.dark( - body: body ?? this.body, - title: title ?? this.title, - headlineBold: headlineBold ?? this.headlineBold, - headline: headline ?? this.headline, - bodyBold: bodyBold ?? this.bodyBold, - footnoteBold: footnoteBold ?? this.footnoteBold, - footnote: footnote ?? this.footnote, - captionBold: captionBold ?? this.captionBold, - ); - - /// Merge text theme - TextTheme merge(TextTheme? other) { - if (other == null) return this; - return copyWith( - body: body.merge(other.body), - title: title.merge(other.title), - headlineBold: headlineBold.merge(other.headlineBold), - headline: headline.merge(other.headline), - bodyBold: bodyBold.merge(other.bodyBold), - footnoteBold: footnoteBold.merge(other.footnoteBold), - footnote: footnote.merge(other.footnote), - captionBold: captionBold.merge(other.captionBold), - ); - } -} - -/// Theme that holds colors -class ColorTheme { - /// Initialise with light theme - ColorTheme.light({ - this.textHighEmphasis = const Color(0xff000000), - this.textLowEmphasis = const Color(0xff7a7a7a), - this.disabled = const Color(0xffdbdbdb), - this.borders = const Color(0xffecebeb), - this.inputBg = const Color(0xfff2f2f2), - this.appBg = const Color(0xfffcfcfc), - this.barsBg = const Color(0xffffffff), - this.linkBg = const Color(0xffe9f2ff), - this.accentPrimary = const Color(0xff005FFF), - this.accentError = const Color(0xffFF3842), - this.accentInfo = const Color(0xff20E070), - this.highlight = const Color(0xfffbf4dd), - this.overlay = const Color.fromRGBO(0, 0, 0, 0.2), - this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6), - this.bgGradient = const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xfff7f7f7), Color(0xfffcfcfc)], - stops: [0, 1], - ), - this.borderTop = const Effect( - sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08), - this.borderBottom = const Effect( - sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08), - this.shadowIconButton = const Effect( - sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4), - this.modalShadow = const Effect( - sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8), - }) : brightness = Brightness.light; - - /// Initialise with dark theme - ColorTheme.dark({ - this.textHighEmphasis = const Color(0xffffffff), - this.textLowEmphasis = const Color(0xff7a7a7a), - this.disabled = const Color(0xff2d2f2f), - this.borders = const Color(0xff1c1e22), - this.inputBg = const Color(0xff13151b), - this.appBg = const Color(0xff070A0D), - this.barsBg = const Color(0xff101418), - this.linkBg = const Color(0xff00193D), - this.accentPrimary = const Color(0xff005FFF), - this.accentError = const Color(0xffFF3742), - this.accentInfo = const Color(0xff20E070), - this.borderTop = const Effect( - sigmaX: 0, - sigmaY: -1, - color: Color(0xff141924), - blur: 0, - ), - this.borderBottom = const Effect( - sigmaX: 0, - sigmaY: 1, - color: Color(0xff141924), - blur: 0, - alpha: 1, - ), - this.shadowIconButton = const Effect( - sigmaX: 0, - sigmaY: 2, - color: Color(0xff000000), - alpha: 0.5, - blur: 4, - ), - this.modalShadow = const Effect( - sigmaX: 0, - sigmaY: 0, - color: Color(0xff000000), - alpha: 1, - blur: 8, - ), - this.highlight = const Color(0xff302d22), - this.overlay = const Color.fromRGBO(0, 0, 0, 0.4), - this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6), - this.bgGradient = const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xff101214), - Color(0xff070a0d), - ], - stops: [0, 1], - ), - }) : brightness = Brightness.dark; - - /// - final Color textHighEmphasis; - - /// - final Color textLowEmphasis; - - /// - final Color disabled; - - /// - final Color borders; - - /// - final Color inputBg; - - /// - final Color appBg; - - /// - final Color barsBg; - - /// - final Color linkBg; - - /// - final Color accentPrimary; - - /// - final Color accentError; - - /// - final Color accentInfo; - - /// - final Effect borderTop; - - /// - final Effect borderBottom; - - /// - final Effect shadowIconButton; - - /// - final Effect modalShadow; - - /// - final Color highlight; - - /// - final Color overlay; - - /// - final Color overlayDark; - - /// - final Gradient bgGradient; - - /// - final Brightness brightness; - - /// Copy with theme - ColorTheme copyWith({ - Brightness brightness = Brightness.light, - Color? textHighEmphasis, - Color? textLowEmphasis, - Color? disabled, - Color? borders, - Color? inputBg, - Color? appBg, - Color? barsBg, - Color? linkBg, - Color? accentPrimary, - Color? accentError, - Color? accentInfo, - Effect? borderTop, - Effect? borderBottom, - Effect? shadowIconButton, - Effect? modalShadow, - Color? highlight, - Color? overlay, - Color? overlayDark, - Gradient? bgGradient, - }) => - brightness == Brightness.light - ? ColorTheme.light( - textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, - textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, - disabled: disabled ?? this.disabled, - borders: borders ?? this.borders, - inputBg: inputBg ?? this.inputBg, - appBg: appBg ?? this.appBg, - barsBg: barsBg ?? this.barsBg, - linkBg: linkBg ?? this.linkBg, - accentPrimary: accentPrimary ?? this.accentPrimary, - accentError: accentError ?? this.accentError, - accentInfo: accentInfo ?? this.accentInfo, - borderTop: borderTop ?? this.borderTop, - borderBottom: borderBottom ?? this.borderBottom, - shadowIconButton: shadowIconButton ?? this.shadowIconButton, - modalShadow: modalShadow ?? this.modalShadow, - highlight: highlight ?? this.highlight, - overlay: overlay ?? this.overlay, - overlayDark: overlayDark ?? this.overlayDark, - bgGradient: bgGradient ?? this.bgGradient, - ) - : ColorTheme.dark( - textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, - textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, - disabled: disabled ?? this.disabled, - borders: borders ?? this.borders, - inputBg: inputBg ?? this.inputBg, - appBg: appBg ?? this.appBg, - barsBg: barsBg ?? this.barsBg, - linkBg: linkBg ?? this.linkBg, - accentPrimary: accentPrimary ?? this.accentPrimary, - accentError: accentError ?? this.accentError, - accentInfo: accentInfo ?? this.accentInfo, - borderTop: borderTop ?? this.borderTop, - borderBottom: borderBottom ?? this.borderBottom, - shadowIconButton: shadowIconButton ?? this.shadowIconButton, - modalShadow: modalShadow ?? this.modalShadow, - highlight: highlight ?? this.highlight, - overlay: overlay ?? this.overlay, - overlayDark: overlayDark ?? this.overlayDark, - bgGradient: bgGradient ?? this.bgGradient, - ); - - /// Merge color theme - ColorTheme merge(ColorTheme? other) { - if (other == null) return this; - return copyWith( - textHighEmphasis: other.textHighEmphasis, - textLowEmphasis: other.textLowEmphasis, - disabled: other.disabled, - borders: other.borders, - inputBg: other.inputBg, - appBg: other.appBg, - barsBg: other.barsBg, - linkBg: other.linkBg, - accentPrimary: other.accentPrimary, - accentError: other.accentError, - accentInfo: other.accentInfo, - highlight: other.highlight, - overlay: other.overlay, - overlayDark: other.overlayDark, - bgGradient: other.bgGradient, - borderTop: other.borderTop, - borderBottom: other.borderBottom, - shadowIconButton: other.shadowIconButton, - modalShadow: other.modalShadow, - ); - } -} - -/// Channel theme data -class ChannelTheme { - /// Constructor for creating [ChannelTheme] - ChannelTheme({ - required this.channelHeaderTheme, - }); - - /// Theme of the [ChannelHeader] widget - final ChannelHeaderTheme channelHeaderTheme; - - /// Creates a copy of [ChannelTheme] with specified attributes overridden. - ChannelTheme copyWith({ - ChannelHeaderTheme? channelHeaderTheme, - }) => - ChannelTheme( - channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, + StreamChatThemeData.raw( + channelListHeaderTheme: + this.channelListHeaderTheme.merge(channelListHeaderTheme), + textTheme: this.textTheme.merge(textTheme), + colorTheme: this.colorTheme.merge(colorTheme), + primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), + defaultUserImage: defaultUserImage ?? this.defaultUserImage, + placeholderUserImage: placeholderUserImage ?? this.placeholderUserImage, + channelPreviewTheme: + this.channelPreviewTheme.merge(channelPreviewTheme), + channelHeaderTheme: this.channelHeaderTheme.merge(channelHeaderTheme), + ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), + otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), + messageInputTheme: this.messageInputTheme.merge(messageInputTheme), + reactionIcons: reactionIcons ?? this.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, + galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, + messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, + channelListViewTheme: channelListViewTheme ?? this.channelListViewTheme, + userListViewTheme: userListViewTheme ?? this.userListViewTheme, + messageSearchListViewTheme: + messageSearchListViewTheme ?? this.messageSearchListViewTheme, ); - /// Merge with theme - ChannelTheme merge(ChannelTheme? other) { + /// Merge themes + StreamChatThemeData merge(StreamChatThemeData? other) { if (other == null) return this; return copyWith( + channelListHeaderTheme: + channelListHeaderTheme.merge(other.channelListHeaderTheme), + textTheme: textTheme.merge(other.textTheme), + colorTheme: colorTheme.merge(other.colorTheme), + primaryIconTheme: other.primaryIconTheme, + defaultUserImage: other.defaultUserImage, + placeholderUserImage: other.placeholderUserImage, + channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme), channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), + ownMessageTheme: ownMessageTheme.merge(other.ownMessageTheme), + otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), + messageInputTheme: messageInputTheme.merge(other.messageInputTheme), + reactionIcons: other.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), + galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), + messageListViewTheme: + messageListViewTheme.merge(other.messageListViewTheme), + channelListViewTheme: + channelListViewTheme.merge(other.channelListViewTheme), + userListViewTheme: userListViewTheme.merge(other.userListViewTheme), + messageSearchListViewTheme: + messageSearchListViewTheme.merge(other.messageSearchListViewTheme), ); } } - -/// Theme for avatar -class AvatarTheme { - /// Constructor for creating [AvatarTheme] - AvatarTheme({ - BoxConstraints? constraints, - BorderRadius? borderRadius, - }) : _constraints = constraints, - _borderRadius = borderRadius; - - final BoxConstraints? _constraints; - final BorderRadius? _borderRadius; - - /// Get constraints for avatar - BoxConstraints get constraints => - _constraints ?? - const BoxConstraints.tightFor( - height: 32, - width: 32, - ); - - /// Get border radius - BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); - - /// Copy with another theme - AvatarTheme copyWith({ - BoxConstraints? constraints, - BorderRadius? borderRadius, - }) => - AvatarTheme( - constraints: constraints ?? _constraints, - borderRadius: borderRadius ?? _borderRadius, - ); - - /// Merge with another AvatarTheme - AvatarTheme merge(AvatarTheme? other) { - if (other == null) return this; - return copyWith( - constraints: other._constraints, - borderRadius: other._borderRadius, - ); - } -} - -/// Class for getting message theme -class MessageTheme { - /// Constructor into [MessageTheme] - const MessageTheme({ - this.replies, - this.messageText, - this.messageAuthor, - this.messageLinks, - this.messageBackgroundColor, - this.messageBorderColor, - this.reactionsBackgroundColor, - this.reactionsBorderColor, - this.reactionsMaskColor, - this.avatarTheme, - this.createdAt, - }); - - /// Text style for message text - final TextStyle? messageText; - - /// Text style for message author - final TextStyle? messageAuthor; - - /// Text style for message links - final TextStyle? messageLinks; - - /// Text style for created at text - final TextStyle? createdAt; - - /// Text style for replies - final TextStyle? replies; - - /// Color for messageBackgroundColor - final Color? messageBackgroundColor; - - /// Color for message border color - final Color? messageBorderColor; - - /// Color for reactions - final Color? reactionsBackgroundColor; - - /// Colors reaction border - final Color? reactionsBorderColor; - - /// Color for reaction mask - final Color? reactionsMaskColor; - - /// Theme of the avatar - final AvatarTheme? avatarTheme; - - /// Copy with a theme - MessageTheme copyWith({ - TextStyle? messageText, - TextStyle? messageAuthor, - TextStyle? messageLinks, - TextStyle? createdAt, - TextStyle? replies, - Color? messageBackgroundColor, - Color? messageBorderColor, - AvatarTheme? avatarTheme, - Color? reactionsBackgroundColor, - Color? reactionsBorderColor, - Color? reactionsMaskColor, - }) => - MessageTheme( - messageText: messageText ?? this.messageText, - messageAuthor: messageAuthor ?? this.messageAuthor, - messageLinks: messageLinks ?? this.messageLinks, - createdAt: createdAt ?? this.createdAt, - messageBackgroundColor: - messageBackgroundColor ?? this.messageBackgroundColor, - messageBorderColor: messageBorderColor ?? this.messageBorderColor, - avatarTheme: avatarTheme ?? this.avatarTheme, - replies: replies ?? this.replies, - reactionsBackgroundColor: - reactionsBackgroundColor ?? this.reactionsBackgroundColor, - reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, - reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, - ); - - /// Merge with a theme - MessageTheme merge(MessageTheme? other) { - if (other == null) return this; - return copyWith( - messageText: messageText?.merge(other.messageText) ?? other.messageText, - messageAuthor: - messageAuthor?.merge(other.messageAuthor) ?? other.messageAuthor, - messageLinks: - messageLinks?.merge(other.messageLinks) ?? other.messageLinks, - createdAt: createdAt?.merge(other.createdAt) ?? other.createdAt, - replies: replies?.merge(other.replies) ?? other.replies, - messageBackgroundColor: other.messageBackgroundColor, - messageBorderColor: other.messageBorderColor, - avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, - reactionsBackgroundColor: other.reactionsBackgroundColor, - reactionsBorderColor: other.reactionsBorderColor, - reactionsMaskColor: other.reactionsMaskColor, - ); - } -} - -/// Theme for channel preview -class ChannelPreviewTheme { - /// Constructor for creating [ChannelPreviewTheme] - const ChannelPreviewTheme({ - this.title, - this.subtitle, - this.lastMessageAt, - this.avatarTheme, - this.unreadCounterColor, - this.indicatorIconSize, - }); - - /// Theme for title - final TextStyle? title; - - /// Theme for subtitle - final TextStyle? subtitle; - - /// Theme of last message at - final TextStyle? lastMessageAt; - - /// Avatar theme - final AvatarTheme? avatarTheme; - - /// Unread counter color - final Color? unreadCounterColor; - - /// Indicator icon size - final double? indicatorIconSize; - - /// Copy with theme - ChannelPreviewTheme copyWith({ - TextStyle? title, - TextStyle? subtitle, - TextStyle? lastMessageAt, - AvatarTheme? avatarTheme, - Color? unreadCounterColor, - double? indicatorIconSize, - }) => - ChannelPreviewTheme( - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - lastMessageAt: lastMessageAt ?? this.lastMessageAt, - avatarTheme: avatarTheme ?? this.avatarTheme, - unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, - indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, - ); - - /// Merge with theme - ChannelPreviewTheme merge(ChannelPreviewTheme? other) { - if (other == null) return this; - return copyWith( - title: title?.merge(other.title) ?? other.title, - subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle, - lastMessageAt: - lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt, - avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, - unreadCounterColor: other.unreadCounterColor, - ); - } -} - -/// Theme for [ChannelHeader] -class ChannelHeaderTheme { - /// Constructor for creating a [ChannelHeaderTheme] - const ChannelHeaderTheme({ - this.title, - this.subtitle, - this.avatarTheme, - this.color, - }); - - /// Theme for title - final TextStyle? title; - - /// Theme for subtitle - final TextStyle? subtitle; - - /// Theme for avatar - final AvatarTheme? avatarTheme; - - /// Color for [ChannelHeaderTheme] - final Color? color; - - /// Copy with theme - ChannelHeaderTheme copyWith({ - TextStyle? title, - TextStyle? subtitle, - AvatarTheme? avatarTheme, - Color? color, - }) => - ChannelHeaderTheme( - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - avatarTheme: avatarTheme ?? this.avatarTheme, - color: color ?? this.color, - ); - - /// Merge with other [ChannelHeaderTheme] - ChannelHeaderTheme merge(ChannelHeaderTheme? other) { - if (other == null) return this; - return copyWith( - title: title?.merge(other.title) ?? other.title, - subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle, - avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, - color: other.color, - ); - } -} - -/// Theme dedicated to the [ChannelListHeader] -class ChannelListHeaderTheme { - /// Returns a new [ChannelListHeaderTheme] - const ChannelListHeaderTheme({ - this.title, - this.avatarTheme, - this.color, - }); - - /// Style of the title text - final TextStyle? title; - - /// Theme dedicated to the userAvatar - final AvatarTheme? avatarTheme; - - /// Background color of the appbar - final Color? color; - - /// Returns a new [ChannelListHeaderTheme] replacing some of its properties - ChannelListHeaderTheme copyWith({ - TextStyle? title, - AvatarTheme? avatarTheme, - Color? color, - }) => - ChannelListHeaderTheme( - title: title ?? this.title, - avatarTheme: avatarTheme ?? this.avatarTheme, - color: color ?? this.color, - ); - - /// Merges [this] [ChannelListHeaderTheme] with the [other] - ChannelListHeaderTheme merge(ChannelListHeaderTheme? other) { - if (other == null) return this; - return copyWith( - title: title?.merge(other.title) ?? other.title, - avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, - color: other.color, - ); - } -} - -/// Defines the theme dedicated to the [MessageInput] widget -class MessageInputTheme { - /// Returns a new [MessageInputTheme] - const MessageInputTheme({ - this.sendAnimationDuration, - this.actionButtonColor, - this.sendButtonColor, - this.actionButtonIdleColor, - this.sendButtonIdleColor, - this.inputBackground, - this.inputTextStyle, - this.inputDecoration, - this.activeBorderGradient, - this.idleBorderGradient, - this.borderRadius, - this.expandButtonColor, - }); - - /// Duration of the [MessageInput] send button animation - final Duration? sendAnimationDuration; - - /// Background color of [MessageInput] send button - final Color? sendButtonColor; - - /// Background color of [MessageInput] action buttons - final Color? actionButtonColor; - - /// Background color of [MessageInput] send button - final Color? sendButtonIdleColor; - - /// Background color of [MessageInput] action buttons - final Color? actionButtonIdleColor; - - /// Background color of [MessageInput] expand button - final Color? expandButtonColor; - - /// Background color of [MessageInput] - final Color? inputBackground; - - /// TextStyle of [MessageInput] - final TextStyle? inputTextStyle; - - /// InputDecoration of [MessageInput] - final InputDecoration? inputDecoration; - - /// Border gradient when the [MessageInput] is not focused - final Gradient? idleBorderGradient; - - /// Border gradient when the [MessageInput] is focused - final Gradient? activeBorderGradient; - - /// Border radius of [MessageInput] - final BorderRadius? borderRadius; - - /// Returns a new [MessageInputTheme] replacing some of its properties - MessageInputTheme copyWith({ - Duration? sendAnimationDuration, - Color? inputBackground, - Color? actionButtonColor, - Color? sendButtonColor, - Color? actionButtonIdleColor, - Color? sendButtonIdleColor, - Color? expandButtonColor, - TextStyle? inputTextStyle, - InputDecoration? inputDecoration, - Gradient? activeBorderGradient, - Gradient? idleBorderGradient, - BorderRadius? borderRadius, - }) => - MessageInputTheme( - sendAnimationDuration: - sendAnimationDuration ?? this.sendAnimationDuration, - inputBackground: inputBackground ?? this.inputBackground, - actionButtonColor: actionButtonColor ?? this.actionButtonColor, - sendButtonColor: sendButtonColor ?? this.sendButtonColor, - actionButtonIdleColor: - actionButtonIdleColor ?? this.actionButtonIdleColor, - expandButtonColor: expandButtonColor ?? this.expandButtonColor, - inputTextStyle: inputTextStyle ?? this.inputTextStyle, - sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, - inputDecoration: inputDecoration ?? this.inputDecoration, - activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, - idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, - borderRadius: borderRadius ?? this.borderRadius, - ); - - /// Merges [this] [MessageInputTheme] with the [other] - MessageInputTheme merge(MessageInputTheme? other) { - if (other == null) return this; - return copyWith( - sendAnimationDuration: other.sendAnimationDuration, - inputBackground: other.inputBackground, - actionButtonColor: other.actionButtonColor, - actionButtonIdleColor: other.actionButtonIdleColor, - sendButtonColor: other.sendButtonColor, - sendButtonIdleColor: other.sendButtonIdleColor, - inputTextStyle: - inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, - inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? - other.inputDecoration, - activeBorderGradient: other.activeBorderGradient, - idleBorderGradient: other.idleBorderGradient, - borderRadius: other.borderRadius, - expandButtonColor: other.expandButtonColor, - ); - } -} - -/// Effect store -class Effect { - /// Constructor for creating [Effect] - const Effect({ - this.sigmaX, - this.sigmaY, - this.color, - this.alpha, - this.blur, - }); - - /// - final double? sigmaX; - - /// - final double? sigmaY; - - /// - final Color? color; - - /// - final double? alpha; - - /// - final double? blur; - - /// Copy with new effect - Effect copyWith({ - double? sigmaX, - double? sigmaY, - Color? color, - double? alpha, - double? blur, - }) => - Effect( - sigmaX: sigmaX ?? this.sigmaX, - sigmaY: sigmaY ?? this.sigmaY, - color: color ?? this.color, - alpha: color as double? ?? this.alpha, - blur: blur ?? this.blur, - ); -} - -/// Overrides the default style of [GalleryHeader] descendants. -/// -/// See also: -/// -/// * [GalleryHeaderThemeData], which is used to configure this theme. -class GalleryHeaderTheme extends InheritedTheme { - /// Creates an [GalleryHeaderTheme]. - /// - /// The [data] parameter must not be null. - const GalleryHeaderTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final GalleryHeaderThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [GalleryHeaderTheme] widget, then - /// [StreamChatThemeData.galleryHeaderTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// ImageHeaderTheme theme = ImageHeaderTheme.of(context); - /// ``` - static GalleryHeaderThemeData of(BuildContext context) { - final galleryHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); - return galleryHeaderTheme?.data ?? - StreamChatTheme.of(context).galleryHeaderTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - GalleryHeaderTheme(data: data, child: child); - - @override - bool updateShouldNotify(GalleryHeaderTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [GalleryHeader]s when used -/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.galleryHeaderTheme]. -/// -/// See also: -/// -/// * [GalleryHeaderTheme], 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({ - this.closeButtonColor, - this.backgroundColor, - this.iconMenuPointColor, - this.titleTextStyle, - this.subtitleTextStyle, - this.bottomSheetBarrierColor, - }); - - /// The color of the "close" button. - /// - /// Defaults to [ColorTheme.textHighEmphasis]. - final Color? closeButtonColor; - - /// The background color of the [GalleryHeader] widget. - /// - /// Defaults to [ChannelHeaderTheme.color]. - final Color? backgroundColor; - - /// Defaults to [ColorTheme.textHighEmphasis]. - final Color? iconMenuPointColor; - - /// The [TextStyle] to use for the [GalleryHeader] title text. - /// - /// Defaults to [TextTheme.headlineBold]. - final TextStyle? titleTextStyle; - - /// The [TextStyle] to use for the [GalleryHeader] subtitle text. - /// - /// Defaults to [ChannelPreviewTheme.subtitle]. - final TextStyle? subtitleTextStyle; - - /// - final Color? bottomSheetBarrierColor; - - /// Copies this [GalleryHeaderThemeData] to another. - GalleryHeaderThemeData copyWith({ - Color? closeButtonColor, - Color? backgroundColor, - Color? iconMenuPointColor, - TextStyle? titleTextStyle, - TextStyle? subtitleTextStyle, - Color? bottomSheetBarrierColor, - }) => - GalleryHeaderThemeData( - closeButtonColor: closeButtonColor ?? this.closeButtonColor, - backgroundColor: backgroundColor ?? this.backgroundColor, - iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, - titleTextStyle: titleTextStyle ?? this.titleTextStyle, - subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, - bottomSheetBarrierColor: - bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, - ); - - /// Linearly interpolate between two [GalleryHeader] themes. - /// - /// All the properties must be non-null. - GalleryHeaderThemeData lerp( - GalleryHeaderThemeData a, - GalleryHeaderThemeData b, - double t, - ) => - GalleryHeaderThemeData( - closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - iconMenuPointColor: - Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), - titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), - subtitleTextStyle: - TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), - bottomSheetBarrierColor: - Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), - ); - - /// Merges one [GalleryHeaderThemeData] with the another - GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) { - if (other == null) return this; - return copyWith( - closeButtonColor: other.closeButtonColor, - backgroundColor: other.backgroundColor, - iconMenuPointColor: other.iconMenuPointColor, - titleTextStyle: other.titleTextStyle, - subtitleTextStyle: other.subtitleTextStyle, - bottomSheetBarrierColor: other.bottomSheetBarrierColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is GalleryHeaderThemeData && - runtimeType == other.runtimeType && - closeButtonColor == other.closeButtonColor && - backgroundColor == other.backgroundColor && - iconMenuPointColor == other.iconMenuPointColor && - titleTextStyle == other.titleTextStyle && - subtitleTextStyle == other.subtitleTextStyle && - bottomSheetBarrierColor == other.bottomSheetBarrierColor; - - @override - int get hashCode => - closeButtonColor.hashCode ^ - backgroundColor.hashCode ^ - iconMenuPointColor.hashCode ^ - titleTextStyle.hashCode ^ - subtitleTextStyle.hashCode ^ - bottomSheetBarrierColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ColorProperty('closeButtonColor', closeButtonColor)) - ..add(ColorProperty('backgroundColor', backgroundColor)) - ..add(ColorProperty('iconMenuPointColor', iconMenuPointColor)) - ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) - ..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle)) - ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)); - } -} - -/// Overrides the default style of [GalleryFooter] descendants. -/// -/// See also: -/// -/// * [GalleryFooterThemeData], which is used to configure this theme. -class GalleryFooterTheme extends InheritedTheme { - /// Creates an [GalleryFooterTheme]. - /// - /// The [data] parameter must not be null. - const GalleryFooterTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final GalleryFooterThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [GalleryFooterTheme] widget, then - /// [StreamChatThemeData.galleryFooterTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// ImageFooterTheme theme = ImageFooterTheme.of(context); - /// ``` - static GalleryFooterThemeData of(BuildContext context) { - final imageFooterTheme = - context.dependOnInheritedWidgetOfExactType(); - return imageFooterTheme?.data ?? - StreamChatTheme.of(context).galleryFooterTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - GalleryFooterTheme(data: data, child: child); - - @override - bool updateShouldNotify(GalleryFooterTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [GalleryFooter]s when used -/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.galleryFooterTheme]. -/// -/// See also: -/// -/// * [GalleryFooterTheme], 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({ - this.backgroundColor, - this.shareIconColor, - this.titleTextStyle, - this.gridIconButtonColor, - this.bottomSheetBarrierColor, - this.bottomSheetBackgroundColor, - this.bottomSheetPhotosTextStyle, - this.bottomSheetCloseIconColor, - }); - - /// The background color for the [GalleryFooter] widget. - /// - /// Defaults to [ColorTheme.barsBg]. - final Color? backgroundColor; - - /// The color for the "share" icon. - /// - /// Defaults to [ColorTheme.textHighEmphasis]. - final Color? shareIconColor; - - /// The [TextStyle] to use for the [GalleryFooter] title text. - /// - /// Defaults to [TextTheme.headlineBold]. - final TextStyle? titleTextStyle; - - /// The color to use for the "grid" icon. - /// - /// Defaults to [ColorTheme.textHighEmphasis]. - final Color? gridIconButtonColor; - - /// The color to use behind the bottom sheet. - /// - /// Defaults to [ColorTheme.overlay]. - final Color? bottomSheetBarrierColor; - - /// The background color to use for the bottom sheet. - /// - /// Defaults to [ColorTheme.barsBg]. - final Color? bottomSheetBackgroundColor; - - /// The [TextStyle] to use for the "photos" text in the bottom sheet. - /// - /// Defaults to [TextTheme.headlineBold]. - final TextStyle? bottomSheetPhotosTextStyle; - - /// The color to use for the "close" icon. - /// - /// Defaults to [ColorTheme.textHighEmphasis]. - final Color? bottomSheetCloseIconColor; - - /// Copies this [GalleryFooterThemeData] to another. - GalleryFooterThemeData copyWith({ - Color? backgroundColor, - Color? shareIconColor, - TextStyle? titleTextStyle, - Color? gridIconButtonColor, - Color? bottomSheetBarrierColor, - Color? bottomSheetBackgroundColor, - TextStyle? bottomSheetPhotosTextStyle, - Color? bottomSheetCloseIconColor, - }) => - GalleryFooterThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - shareIconColor: shareIconColor ?? this.shareIconColor, - titleTextStyle: titleTextStyle ?? this.titleTextStyle, - gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor, - bottomSheetBarrierColor: - bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, - bottomSheetBackgroundColor: - bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, - bottomSheetPhotosTextStyle: - bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, - bottomSheetCloseIconColor: - bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, - ); - - /// Linearly interpolate between two [GalleryFooter] themes. - /// - /// All the properties must be non-null. - GalleryFooterThemeData lerp( - GalleryFooterThemeData a, - GalleryFooterThemeData b, - double t, - ) => - GalleryFooterThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), - titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), - gridIconButtonColor: - Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), - bottomSheetBarrierColor: - Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), - bottomSheetBackgroundColor: Color.lerp( - a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t), - bottomSheetPhotosTextStyle: TextStyle.lerp( - a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t), - bottomSheetCloseIconColor: Color.lerp( - a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t), - ); - - /// Merges one [GalleryFooterThemeData] with another. - GalleryFooterThemeData merge(GalleryFooterThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - bottomSheetBarrierColor: other.bottomSheetBarrierColor, - bottomSheetBackgroundColor: other.bottomSheetBackgroundColor, - bottomSheetCloseIconColor: other.bottomSheetCloseIconColor, - bottomSheetPhotosTextStyle: other.bottomSheetPhotosTextStyle, - gridIconButtonColor: other.gridIconButtonColor, - titleTextStyle: other.titleTextStyle, - shareIconColor: other.shareIconColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is GalleryFooterThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor && - shareIconColor == other.shareIconColor && - titleTextStyle == other.titleTextStyle && - gridIconButtonColor == other.gridIconButtonColor && - bottomSheetBarrierColor == other.bottomSheetBarrierColor && - bottomSheetBackgroundColor == other.bottomSheetBackgroundColor && - bottomSheetPhotosTextStyle == other.bottomSheetPhotosTextStyle && - bottomSheetCloseIconColor == other.bottomSheetCloseIconColor; - - @override - int get hashCode => - backgroundColor.hashCode ^ - shareIconColor.hashCode ^ - titleTextStyle.hashCode ^ - gridIconButtonColor.hashCode ^ - bottomSheetBarrierColor.hashCode ^ - bottomSheetBackgroundColor.hashCode ^ - bottomSheetPhotosTextStyle.hashCode ^ - bottomSheetCloseIconColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ColorProperty('backgroundColor', backgroundColor)) - ..add(ColorProperty('shareIconColor', shareIconColor)) - ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) - ..add(ColorProperty('gridIconButtonColor', gridIconButtonColor)) - ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)) - ..add(ColorProperty( - 'bottomSheetBackgroundColor', bottomSheetBackgroundColor)) - ..add(DiagnosticsProperty( - 'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle)) - ..add(ColorProperty( - 'bottomSheetCloseIconColor', bottomSheetCloseIconColor)); - } -} - -/// Overrides the default style of [MessageListView] descendants. -/// -/// See also: -/// -/// * [MessageListViewThemeData], which is used to configure this theme. -class MessageListViewTheme extends InheritedTheme { - /// Creates a [MessageListViewTheme]. - /// - /// The [data] parameter must not be null. - const MessageListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final MessageListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [MessageListViewTheme] widget, then - /// [StreamChatThemeData.messageListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// MessageListViewTheme theme = MessageListViewTheme.of(context); - /// ``` - static MessageListViewThemeData 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); - - @override - bool updateShouldNotify(MessageListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [MessageListView]s when -/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.messageListViewTheme]. -/// -/// See also: -/// -/// * [MessageListViewTheme], 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({ - this.backgroundColor, - }); - - /// The color of the [MessageListView] background. - final Color? backgroundColor; - - /// Copies this [MessageListViewThemeData] to another. - MessageListViewThemeData copyWith({ - Color? backgroundColor, - }) => - MessageListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [MessageListView] themes. - /// - /// All the properties must be non-null. - MessageListViewThemeData lerp( - MessageListViewThemeData a, - MessageListViewThemeData b, - double t, - ) => - MessageListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [MessageListViewThemeData] with another. - MessageListViewThemeData merge(MessageListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is MessageListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} - -/// Overrides the default style of [ChannelListView] descendants. -/// -/// See also: -/// -/// * [ChannelListViewThemeData], which is used to configure this theme. -class ChannelListViewTheme extends InheritedTheme { - /// Creates a [ChannelListViewTheme]. - /// - /// The [data] parameter must not be null. - const ChannelListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final ChannelListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [ChannelListViewTheme] widget, then - /// [StreamChatThemeData.channelListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); - /// ``` - static ChannelListViewThemeData 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); - - @override - bool updateShouldNotify(ChannelListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [ChannelListView]s when -/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.channelListViewTheme]. -/// -/// See also: -/// -/// * [ChannelListViewTheme], 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({ - this.backgroundColor, - }); - - /// The color of the [ChannelListView] background. - final Color? backgroundColor; - - /// Copies this [ChannelListViewThemeData] to another. - ChannelListViewThemeData copyWith({ - Color? backgroundColor, - }) => - ChannelListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [ChannelListViewThemeData] themes. - /// - /// All the properties must be non-null. - ChannelListViewThemeData lerp( - ChannelListViewThemeData a, - ChannelListViewThemeData b, - double t, - ) => - ChannelListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [ChannelListViewThemeData] with another. - ChannelListViewThemeData merge(ChannelListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ChannelListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} - -/// Overrides the default style of [UserListView] descendants. -/// -/// See also: -/// -/// * [UserListViewThemeData], which is used to configure this theme. -class UserListViewTheme extends InheritedTheme { - /// Creates a [UserListViewTheme]. - /// - /// The [data] parameter must not be null. - const UserListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final UserListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [UserListViewTheme] widget, then - /// [StreamChatThemeData.userListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// UserListViewTheme theme = UserListViewTheme.of(context); - /// ``` - static UserListViewThemeData of(BuildContext context) { - final userListViewTheme = - context.dependOnInheritedWidgetOfExactType(); - return userListViewTheme?.data ?? - StreamChatTheme.of(context).userListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - UserListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(UserListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [UserListView]s when -/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.userListViewTheme]. -/// -/// See also: -/// -/// * [UserListViewTheme], 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({ - this.backgroundColor, - }); - - /// The color of the [ChannelListView] background. - final Color? backgroundColor; - - /// Copies this [ChannelListViewThemeData] to another. - UserListViewThemeData copyWith({ - Color? backgroundColor, - }) => - UserListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [UserListViewThemeData] themes. - /// - /// All the properties must be non-null. - UserListViewThemeData lerp( - UserListViewThemeData a, - UserListViewThemeData b, - double t, - ) => - UserListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [UserListViewThemeData] with another. - UserListViewThemeData merge(UserListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is UserListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} - -/// Overrides the default style of [MessageSearchListView] descendants. -/// -/// See also: -/// -/// * [UserListViewThemeData], which is used to configure this theme. -class MessageSearchListViewTheme extends InheritedTheme { - /// Creates a [UserListViewTheme]. - /// - /// The [data] parameter must not be null. - const MessageSearchListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final MessageSearchListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [MessageSearchListView] widget, then - /// [StreamChatThemeData.messageSearchListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); - /// ``` - static MessageSearchListViewThemeData of(BuildContext context) { - final messageSearchListViewTheme = context - .dependOnInheritedWidgetOfExactType(); - return messageSearchListViewTheme?.data ?? - StreamChatTheme.of(context).messageSearchListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - MessageSearchListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => - data != oldWidget.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. -/// * [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({ - this.backgroundColor, - }); - - /// The color of the [MessageSearchListView] background. - final Color? backgroundColor; - - /// Copies this [MessageSearchListViewThemeData] to another. - MessageSearchListViewThemeData copyWith({ - Color? backgroundColor, - }) => - MessageSearchListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [UserListViewThemeData] themes. - /// - /// All the properties must be non-null. - MessageSearchListViewThemeData lerp( - MessageSearchListViewThemeData a, - MessageSearchListViewThemeData b, - double t, - ) => - MessageSearchListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [MessageSearchListViewThemeData] with another. - MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is MessageSearchListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart new file mode 100644 index 00000000..6c287638 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -0,0 +1,77 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// A style that overrides the default appearance of various avatar widgets. +class AvatarThemeData with Diagnosticable { + /// Creates an [AvatarThemeData]. + const AvatarThemeData({ + BoxConstraints? constraints, + BorderRadius? borderRadius, + }) : _constraints = constraints, + _borderRadius = borderRadius; + + final BoxConstraints? _constraints; + final BorderRadius? _borderRadius; + + /// Get constraints for avatar + BoxConstraints get constraints => + _constraints ?? + const BoxConstraints.tightFor( + height: 32, + width: 32, + ); + + /// Get border radius + BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); + + /// Copy this [AvatarThemeData] to another. + AvatarThemeData copyWith({ + BoxConstraints? constraints, + BorderRadius? borderRadius, + }) => + AvatarThemeData( + constraints: constraints ?? _constraints, + borderRadius: borderRadius ?? _borderRadius, + ); + + /// Linearly interpolate between two [UserAvatar] themes. + /// + /// All the properties must be non-null. + AvatarThemeData lerp( + AvatarThemeData a, + AvatarThemeData b, + double t, + ) => + AvatarThemeData( + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), + constraints: BoxConstraints.lerp(a.constraints, b.constraints, t), + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AvatarThemeData && + runtimeType == other.runtimeType && + _constraints == other._constraints && + _borderRadius == other._borderRadius; + + @override + int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode; + + /// Merges one [AvatarThemeData] with the another + AvatarThemeData merge(AvatarThemeData? other) { + if (other == null) return this; + return copyWith( + constraints: other._constraints, + borderRadius: other._borderRadius, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('borderRadius', borderRadius)) + ..add(DiagnosticsProperty('constraints', 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 new file mode 100644 index 00000000..63e208e5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -0,0 +1,149 @@ +import 'package:flutter/foundation.dart'; +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'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; + +/// Overrides the default style of [ChannelHeader] descendants. +/// +/// See also: +/// +/// * [ChannelHeaderThemeData], which is used to configure this theme. +class ChannelHeaderTheme extends InheritedTheme { + /// Creates a [ChannelHeaderTheme]. + /// + /// The [data] parameter must not be null. + const ChannelHeaderTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelHeaderThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelHeaderTheme] widget, then + /// [StreamChatThemeData.channelTheme.channelHeaderTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = ChannelHeaderTheme.of(context); + /// ``` + static ChannelHeaderThemeData of(BuildContext context) { + final channelHeaderTheme = + context.dependOnInheritedWidgetOfExactType(); + return channelHeaderTheme?.data ?? + StreamChatTheme.of(context).channelHeaderTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + ChannelHeaderTheme(data: data, child: child); + + @override + bool updateShouldNotify(ChannelHeaderTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [ChannelHeader]s when used +/// with [ChannelHeaderTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.channelHeaderTheme]. +/// +/// See also: +/// +/// * [ChannelHeaderTheme], 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({ + this.titleStyle, + this.subtitleStyle, + this.avatarTheme, + this.color, + }); + + /// Theme for title + final TextStyle? titleStyle; + + /// Theme for subtitle + final TextStyle? subtitleStyle; + + /// Theme for avatar + final AvatarThemeData? avatarTheme; + + /// Color for [ChannelHeaderThemeData] + final Color? color; + + /// Copy with theme + ChannelHeaderThemeData copyWith({ + TextStyle? titleStyle, + TextStyle? subtitleStyle, + AvatarThemeData? avatarTheme, + Color? color, + }) => + ChannelHeaderThemeData( + titleStyle: titleStyle ?? this.titleStyle, + subtitleStyle: subtitleStyle ?? this.subtitleStyle, + avatarTheme: avatarTheme ?? this.avatarTheme, + color: color ?? this.color, + ); + + /// Linearly interpolate between two [ChannelHeaderThemeData]. + /// + /// All the properties must be non-null. + ChannelHeaderThemeData lerp( + ChannelHeaderThemeData a, + ChannelHeaderThemeData b, + double t, + ) => + ChannelHeaderThemeData( + 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), + color: Color.lerp(a.color, b.color, t), + ); + + /// Merge with other [ChannelHeaderThemeData] + ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) { + if (other == null) return this; + return copyWith( + titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + subtitleStyle: + subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, + avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, + color: other.color, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelHeaderThemeData && + runtimeType == other.runtimeType && + titleStyle == other.titleStyle && + subtitleStyle == other.subtitleStyle && + avatarTheme == other.avatarTheme && + color == other.color; + + @override + int get hashCode => + titleStyle.hashCode ^ + subtitleStyle.hashCode ^ + avatarTheme.hashCode ^ + color.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('title', titleStyle)) + ..add(DiagnosticsProperty('subtitle', subtitleStyle)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('color', color)); + } +} 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 new file mode 100644 index 00000000..3846232e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -0,0 +1,125 @@ +import 'package:flutter/foundation.dart'; +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'; + +/// Overrides the default style of [ChannelListHeader] descendants. +/// +/// See also: +/// +/// * [ChannelListHeaderThemeData], which is used to configure this theme. +class ChannelListHeaderTheme extends InheritedTheme { + /// Creates a [ChannelListHeaderTheme]. + /// + /// The [data] parameter must not be null. + const ChannelListHeaderTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelListHeaderThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelListHeaderTheme] widget, then + /// [StreamChatThemeData.channelListHeaderTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = ChannelListHeaderTheme.of(context); + /// ``` + static ChannelListHeaderThemeData 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); + + @override + bool updateShouldNotify(ChannelListHeaderTheme oldWidget) => + data != oldWidget.data; +} + +/// Theme dedicated to the [ChannelListHeader] +class ChannelListHeaderThemeData with Diagnosticable { + /// Returns a new [ChannelListHeaderThemeData] + const ChannelListHeaderThemeData({ + this.titleStyle, + this.avatarTheme, + this.color, + }); + + /// Style of the title text + final TextStyle? titleStyle; + + /// Theme dedicated to the userAvatar + final AvatarThemeData? avatarTheme; + + /// Background color of the appbar + final Color? color; + + /// Returns a new [ChannelListHeaderThemeData] replacing some of its + /// properties + ChannelListHeaderThemeData copyWith({ + TextStyle? titleStyle, + AvatarThemeData? avatarTheme, + Color? color, + }) => + ChannelListHeaderThemeData( + 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, + double t, + ) => + ChannelListHeaderThemeData( + avatarTheme: + const AvatarThemeData().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) { + if (other == null) return this; + return copyWith( + titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, + color: other.color, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelListHeaderThemeData && + runtimeType == other.runtimeType && + titleStyle == other.titleStyle && + avatarTheme == other.avatarTheme && + color == other.color; + + @override + int get hashCode => + titleStyle.hashCode ^ avatarTheme.hashCode ^ color.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('titleStyle', titleStyle)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('color', color)); + } +} 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 new file mode 100644 index 00000000..d310e2bc --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [ChannelListView] descendants. +/// +/// See also: +/// +/// * [ChannelListViewThemeData], which is used to configure this theme. +class ChannelListViewTheme extends InheritedTheme { + /// Creates a [ChannelListViewTheme]. + /// + /// The [data] parameter must not be null. + const ChannelListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelListViewTheme] widget, then + /// [StreamChatThemeData.channelListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); + /// ``` + static ChannelListViewThemeData 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); + + @override + bool updateShouldNotify(ChannelListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [ChannelListView]s when +/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.channelListViewTheme]. +/// +/// See also: +/// +/// * [ChannelListViewTheme], 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({ + this.backgroundColor, + }); + + /// The color of the [ChannelListView] background. + final Color? backgroundColor; + + /// Copies this [ChannelListViewThemeData] to another. + ChannelListViewThemeData copyWith({ + Color? backgroundColor, + }) => + ChannelListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [ChannelListViewThemeData] themes. + /// + /// All the properties must be non-null. + ChannelListViewThemeData lerp( + ChannelListViewThemeData a, + ChannelListViewThemeData b, + double t, + ) => + ChannelListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [ChannelListViewThemeData] with another. + ChannelListViewThemeData merge(ChannelListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', 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 new file mode 100644 index 00000000..ff9ef70c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -0,0 +1,169 @@ +import 'package:flutter/foundation.dart'; +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'; + +/// Overrides the default style of [ChannelPreview] descendants. +/// +/// See also: +/// +/// * [ChannelPreviewThemeData], which is used to configure this theme. +class ChannelPreviewTheme extends InheritedTheme { + /// Creates a [ChannelPreviewTheme]. + /// + /// The [data] parameter must not be null. + const ChannelPreviewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelPreviewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelPreviewTheme] widget, then + /// [StreamChatThemeData.channelPreviewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = ChannelPreviewTheme.of(context); + /// ``` + static ChannelPreviewThemeData of(BuildContext context) { + final channelPreviewTheme = + context.dependOnInheritedWidgetOfExactType(); + return channelPreviewTheme?.data ?? + StreamChatTheme.of(context).channelPreviewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + ChannelPreviewTheme(data: data, child: child); + + @override + bool updateShouldNotify(ChannelPreviewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [ChannelPreview]s when used +/// with [ChannelPreviewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.channelPreviewTheme]. +/// +/// See also: +/// +/// * [ChannelPreviewTheme], 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({ + this.titleStyle, + this.subtitleStyle, + this.lastMessageAtStyle, + this.avatarTheme, + this.unreadCounterColor, + this.indicatorIconSize, + }); + + /// Theme for title + final TextStyle? titleStyle; + + /// Theme for subtitle + final TextStyle? subtitleStyle; + + /// Theme of last message at + final TextStyle? lastMessageAtStyle; + + /// Avatar theme + final AvatarThemeData? avatarTheme; + + /// Unread counter color + final Color? unreadCounterColor; + + /// Indicator icon size + final double? indicatorIconSize; + + /// Copy with theme + ChannelPreviewThemeData copyWith({ + TextStyle? titleStyle, + TextStyle? subtitleStyle, + TextStyle? lastMessageAtStyle, + AvatarThemeData? avatarTheme, + Color? unreadCounterColor, + double? indicatorIconSize, + }) => + ChannelPreviewThemeData( + titleStyle: titleStyle ?? this.titleStyle, + subtitleStyle: subtitleStyle ?? this.subtitleStyle, + lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, + avatarTheme: avatarTheme ?? this.avatarTheme, + unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, + indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, + ); + + /// Linearly interpolate one [ChannelPreviewThemeData] to another. + ChannelPreviewThemeData lerp( + ChannelPreviewThemeData a, + ChannelPreviewThemeData b, + double t, + ) => + ChannelPreviewThemeData( + avatarTheme: + const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + indicatorIconSize: a.indicatorIconSize, + lastMessageAtStyle: + TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), + subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + unreadCounterColor: + Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t), + ); + + /// Merge with theme + ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) { + if (other == null) return this; + return copyWith( + titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + subtitleStyle: + subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, + lastMessageAtStyle: lastMessageAtStyle?.merge(other.lastMessageAtStyle) ?? + other.lastMessageAtStyle, + avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, + unreadCounterColor: other.unreadCounterColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelPreviewThemeData && + runtimeType == other.runtimeType && + titleStyle == other.titleStyle && + subtitleStyle == other.subtitleStyle && + lastMessageAtStyle == other.lastMessageAtStyle && + avatarTheme == other.avatarTheme && + unreadCounterColor == other.unreadCounterColor && + indicatorIconSize == other.indicatorIconSize; + + @override + int get hashCode => + titleStyle.hashCode ^ + subtitleStyle.hashCode ^ + lastMessageAtStyle.hashCode ^ + avatarTheme.hashCode ^ + unreadCounterColor.hashCode ^ + indicatorIconSize.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('titleStyle', titleStyle)) + ..add(DiagnosticsProperty('subtitleStyle', subtitleStyle)) + ..add(DiagnosticsProperty('lastMessageAtStyle', lastMessageAtStyle)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('unreadCounterColor', unreadCounterColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart new file mode 100644 index 00000000..d021c6cf --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart @@ -0,0 +1,286 @@ +import 'package:flutter/material.dart'; + +/// Theme that holds colors +class ColorTheme { + /// Initialise with light theme + ColorTheme.light({ + this.textHighEmphasis = const Color(0xff000000), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xffdbdbdb), + this.borders = const Color(0xffecebeb), + this.inputBg = const Color(0xfff2f2f2), + this.appBg = const Color(0xfffcfcfc), + this.barsBg = const Color(0xffffffff), + this.linkBg = const Color(0xffe9f2ff), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3842), + this.accentInfo = const Color(0xff20E070), + this.highlight = const Color(0xfffbf4dd), + this.overlay = const Color.fromRGBO(0, 0, 0, 0.2), + this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6), + this.bgGradient = const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xfff7f7f7), Color(0xfffcfcfc)], + stops: [0, 1], + ), + this.borderTop = const Effect( + sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08), + this.borderBottom = const Effect( + sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08), + this.shadowIconButton = const Effect( + sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4), + this.modalShadow = const Effect( + sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8), + }) : brightness = Brightness.light; + + /// Initialise with dark theme + ColorTheme.dark({ + this.textHighEmphasis = const Color(0xffffffff), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xff2d2f2f), + this.borders = const Color(0xff1c1e22), + this.inputBg = const Color(0xff13151b), + this.appBg = const Color(0xff070A0D), + this.barsBg = const Color(0xff101418), + this.linkBg = const Color(0xff00193D), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3742), + this.accentInfo = const Color(0xff20E070), + this.borderTop = const Effect( + sigmaX: 0, + sigmaY: -1, + color: Color(0xff141924), + blur: 0, + ), + this.borderBottom = const Effect( + sigmaX: 0, + sigmaY: 1, + color: Color(0xff141924), + blur: 0, + alpha: 1, + ), + this.shadowIconButton = const Effect( + sigmaX: 0, + sigmaY: 2, + color: Color(0xff000000), + alpha: 0.5, + blur: 4, + ), + this.modalShadow = const Effect( + sigmaX: 0, + sigmaY: 0, + color: Color(0xff000000), + alpha: 1, + blur: 8, + ), + this.highlight = const Color(0xff302d22), + this.overlay = const Color.fromRGBO(0, 0, 0, 0.4), + this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6), + this.bgGradient = const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xff101214), + Color(0xff070a0d), + ], + stops: [0, 1], + ), + }) : brightness = Brightness.dark; + + /// + final Color textHighEmphasis; + + /// + final Color textLowEmphasis; + + /// + final Color disabled; + + /// + final Color borders; + + /// + final Color inputBg; + + /// + final Color appBg; + + /// + final Color barsBg; + + /// + final Color linkBg; + + /// + final Color accentPrimary; + + /// + final Color accentError; + + /// + final Color accentInfo; + + /// + final Effect borderTop; + + /// + final Effect borderBottom; + + /// + final Effect shadowIconButton; + + /// + final Effect modalShadow; + + /// + final Color highlight; + + /// + final Color overlay; + + /// + final Color overlayDark; + + /// + final Gradient bgGradient; + + /// + final Brightness brightness; + + /// Copy with theme + ColorTheme copyWith({ + Brightness brightness = Brightness.light, + Color? textHighEmphasis, + Color? textLowEmphasis, + Color? disabled, + Color? borders, + Color? inputBg, + Color? appBg, + Color? barsBg, + Color? linkBg, + Color? accentPrimary, + Color? accentError, + Color? accentInfo, + Effect? borderTop, + Effect? borderBottom, + Effect? shadowIconButton, + Effect? modalShadow, + Color? highlight, + Color? overlay, + Color? overlayDark, + Gradient? bgGradient, + }) => + brightness == Brightness.light + ? ColorTheme.light( + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, + borderTop: borderTop ?? this.borderTop, + borderBottom: borderBottom ?? this.borderBottom, + shadowIconButton: shadowIconButton ?? this.shadowIconButton, + modalShadow: modalShadow ?? this.modalShadow, + highlight: highlight ?? this.highlight, + overlay: overlay ?? this.overlay, + overlayDark: overlayDark ?? this.overlayDark, + bgGradient: bgGradient ?? this.bgGradient, + ) + : ColorTheme.dark( + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, + borderTop: borderTop ?? this.borderTop, + borderBottom: borderBottom ?? this.borderBottom, + shadowIconButton: shadowIconButton ?? this.shadowIconButton, + modalShadow: modalShadow ?? this.modalShadow, + highlight: highlight ?? this.highlight, + overlay: overlay ?? this.overlay, + overlayDark: overlayDark ?? this.overlayDark, + bgGradient: bgGradient ?? this.bgGradient, + ); + + /// Merge color theme + ColorTheme merge(ColorTheme? other) { + if (other == null) return this; + return copyWith( + textHighEmphasis: other.textHighEmphasis, + textLowEmphasis: other.textLowEmphasis, + disabled: other.disabled, + borders: other.borders, + inputBg: other.inputBg, + appBg: other.appBg, + barsBg: other.barsBg, + linkBg: other.linkBg, + accentPrimary: other.accentPrimary, + accentError: other.accentError, + accentInfo: other.accentInfo, + highlight: other.highlight, + overlay: other.overlay, + overlayDark: other.overlayDark, + bgGradient: other.bgGradient, + borderTop: other.borderTop, + borderBottom: other.borderBottom, + shadowIconButton: other.shadowIconButton, + modalShadow: other.modalShadow, + ); + } +} + +/// Effect store +class Effect { + /// Constructor for creating [Effect] + const Effect({ + this.sigmaX, + this.sigmaY, + this.color, + this.alpha, + this.blur, + }); + + /// + final double? sigmaX; + + /// + final double? sigmaY; + + /// + final Color? color; + + /// + final double? alpha; + + /// + final double? blur; + + /// Copy with new effect + Effect copyWith({ + double? sigmaX, + double? sigmaY, + Color? color, + double? alpha, + double? blur, + }) => + Effect( + sigmaX: sigmaX ?? this.sigmaX, + sigmaY: sigmaY ?? this.sigmaY, + color: color ?? this.color, + alpha: color as double? ?? this.alpha, + blur: blur ?? this.blur, + ); +} 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 new file mode 100644 index 00000000..bdf273e7 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart @@ -0,0 +1,217 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [GalleryFooter] descendants. +/// +/// See also: +/// +/// * [GalleryFooterThemeData], which is used to configure this theme. +class GalleryFooterTheme extends InheritedTheme { + /// Creates an [GalleryFooterTheme]. + /// + /// The [data] parameter must not be null. + const GalleryFooterTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final GalleryFooterThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [GalleryFooterTheme] widget, then + /// [StreamChatThemeData.galleryFooterTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ImageFooterTheme theme = ImageFooterTheme.of(context); + /// ``` + static GalleryFooterThemeData of(BuildContext context) { + final imageFooterTheme = + context.dependOnInheritedWidgetOfExactType(); + return imageFooterTheme?.data ?? + StreamChatTheme.of(context).galleryFooterTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + GalleryFooterTheme(data: data, child: child); + + @override + bool updateShouldNotify(GalleryFooterTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [GalleryFooter]s when used +/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.galleryFooterTheme]. +/// +/// See also: +/// +/// * [GalleryFooterTheme], 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({ + this.backgroundColor, + this.shareIconColor, + this.titleTextStyle, + this.gridIconButtonColor, + this.bottomSheetBarrierColor, + this.bottomSheetBackgroundColor, + this.bottomSheetPhotosTextStyle, + this.bottomSheetCloseIconColor, + }); + + /// The background color for the [GalleryFooter] widget. + /// + /// Defaults to [ColorTheme.barsBg]. + final Color? backgroundColor; + + /// The color for the "share" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? shareIconColor; + + /// The [TextStyle] to use for the [GalleryFooter] title text. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? titleTextStyle; + + /// The color to use for the "grid" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? gridIconButtonColor; + + /// The color to use behind the bottom sheet. + /// + /// Defaults to [ColorTheme.overlay]. + final Color? bottomSheetBarrierColor; + + /// The background color to use for the bottom sheet. + /// + /// Defaults to [ColorTheme.barsBg]. + final Color? bottomSheetBackgroundColor; + + /// The [TextStyle] to use for the "photos" text in the bottom sheet. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? bottomSheetPhotosTextStyle; + + /// The color to use for the "close" icon. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? bottomSheetCloseIconColor; + + /// Copies this [GalleryFooterThemeData] to another. + GalleryFooterThemeData copyWith({ + Color? backgroundColor, + Color? shareIconColor, + TextStyle? titleTextStyle, + Color? gridIconButtonColor, + Color? bottomSheetBarrierColor, + Color? bottomSheetBackgroundColor, + TextStyle? bottomSheetPhotosTextStyle, + Color? bottomSheetCloseIconColor, + }) => + GalleryFooterThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + shareIconColor: shareIconColor ?? this.shareIconColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + bottomSheetBackgroundColor: + bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, + bottomSheetPhotosTextStyle: + bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, + bottomSheetCloseIconColor: + bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, + ); + + /// Linearly interpolate between two [GalleryFooter] themes. + /// + /// All the properties must be non-null. + GalleryFooterThemeData lerp( + GalleryFooterThemeData a, + GalleryFooterThemeData b, + double t, + ) => + GalleryFooterThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + gridIconButtonColor: + Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + bottomSheetBackgroundColor: Color.lerp( + a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t), + bottomSheetPhotosTextStyle: TextStyle.lerp( + a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t), + bottomSheetCloseIconColor: Color.lerp( + a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t), + ); + + /// Merges one [GalleryFooterThemeData] with another. + GalleryFooterThemeData merge(GalleryFooterThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + bottomSheetBarrierColor: other.bottomSheetBarrierColor, + bottomSheetBackgroundColor: other.bottomSheetBackgroundColor, + bottomSheetCloseIconColor: other.bottomSheetCloseIconColor, + bottomSheetPhotosTextStyle: other.bottomSheetPhotosTextStyle, + gridIconButtonColor: other.gridIconButtonColor, + titleTextStyle: other.titleTextStyle, + shareIconColor: other.shareIconColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GalleryFooterThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor && + shareIconColor == other.shareIconColor && + titleTextStyle == other.titleTextStyle && + gridIconButtonColor == other.gridIconButtonColor && + bottomSheetBarrierColor == other.bottomSheetBarrierColor && + bottomSheetBackgroundColor == other.bottomSheetBackgroundColor && + bottomSheetPhotosTextStyle == other.bottomSheetPhotosTextStyle && + bottomSheetCloseIconColor == other.bottomSheetCloseIconColor; + + @override + int get hashCode => + backgroundColor.hashCode ^ + shareIconColor.hashCode ^ + titleTextStyle.hashCode ^ + gridIconButtonColor.hashCode ^ + bottomSheetBarrierColor.hashCode ^ + bottomSheetBackgroundColor.hashCode ^ + bottomSheetPhotosTextStyle.hashCode ^ + bottomSheetCloseIconColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('shareIconColor', shareIconColor)) + ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) + ..add(ColorProperty('gridIconButtonColor', gridIconButtonColor)) + ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)) + ..add(ColorProperty( + 'bottomSheetBackgroundColor', bottomSheetBackgroundColor)) + ..add(DiagnosticsProperty( + 'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle)) + ..add(ColorProperty( + 'bottomSheetCloseIconColor', bottomSheetCloseIconColor)); + } +} 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 new file mode 100644 index 00000000..23402c57 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -0,0 +1,179 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [GalleryHeader] descendants. +/// +/// See also: +/// +/// * [GalleryHeaderThemeData], which is used to configure this theme. +class GalleryHeaderTheme extends InheritedTheme { + /// Creates a [GalleryHeaderTheme]. + /// + /// The [data] parameter must not be null. + const GalleryHeaderTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final GalleryHeaderThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [GalleryHeaderTheme] widget, then + /// [StreamChatThemeData.galleryHeaderTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ImageHeaderTheme theme = ImageHeaderTheme.of(context); + /// ``` + static GalleryHeaderThemeData of(BuildContext context) { + final galleryHeaderTheme = + context.dependOnInheritedWidgetOfExactType(); + return galleryHeaderTheme?.data ?? + StreamChatTheme.of(context).galleryHeaderTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + GalleryHeaderTheme(data: data, child: child); + + @override + bool updateShouldNotify(GalleryHeaderTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [GalleryHeader]s when used +/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.galleryHeaderTheme]. +/// +/// See also: +/// +/// * [GalleryHeaderTheme], 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({ + this.closeButtonColor, + this.backgroundColor, + this.iconMenuPointColor, + this.titleTextStyle, + this.subtitleTextStyle, + this.bottomSheetBarrierColor, + }); + + /// The color of the "close" button. + /// + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? closeButtonColor; + + /// The background color of the [GalleryHeader] widget. + /// + /// Defaults to [ChannelHeaderTheme.color]. + final Color? backgroundColor; + + /// Defaults to [ColorTheme.textHighEmphasis]. + final Color? iconMenuPointColor; + + /// The [TextStyle] to use for the [GalleryHeader] title text. + /// + /// Defaults to [TextTheme.headlineBold]. + final TextStyle? titleTextStyle; + + /// The [TextStyle] to use for the [GalleryHeader] subtitle text. + /// + /// Defaults to [ChannelPreviewTheme.subtitleStyle]. + final TextStyle? subtitleTextStyle; + + /// + final Color? bottomSheetBarrierColor; + + /// Copies this [GalleryHeaderThemeData] to another. + GalleryHeaderThemeData copyWith({ + Color? closeButtonColor, + Color? backgroundColor, + Color? iconMenuPointColor, + TextStyle? titleTextStyle, + TextStyle? subtitleTextStyle, + Color? bottomSheetBarrierColor, + }) => + GalleryHeaderThemeData( + closeButtonColor: closeButtonColor ?? this.closeButtonColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + ); + + /// Linearly interpolate between two [GalleryHeader] themes. + /// + /// All the properties must be non-null. + GalleryHeaderThemeData lerp( + GalleryHeaderThemeData a, + GalleryHeaderThemeData b, + double t, + ) => + GalleryHeaderThemeData( + closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + iconMenuPointColor: + Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + subtitleTextStyle: + TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + ); + + /// Merges one [GalleryHeaderThemeData] with the another + GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) { + if (other == null) return this; + return copyWith( + closeButtonColor: other.closeButtonColor, + backgroundColor: other.backgroundColor, + iconMenuPointColor: other.iconMenuPointColor, + titleTextStyle: other.titleTextStyle, + subtitleTextStyle: other.subtitleTextStyle, + bottomSheetBarrierColor: other.bottomSheetBarrierColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GalleryHeaderThemeData && + runtimeType == other.runtimeType && + closeButtonColor == other.closeButtonColor && + backgroundColor == other.backgroundColor && + iconMenuPointColor == other.iconMenuPointColor && + titleTextStyle == other.titleTextStyle && + subtitleTextStyle == other.subtitleTextStyle && + bottomSheetBarrierColor == other.bottomSheetBarrierColor; + + @override + int get hashCode => + closeButtonColor.hashCode ^ + backgroundColor.hashCode ^ + iconMenuPointColor.hashCode ^ + titleTextStyle.hashCode ^ + subtitleTextStyle.hashCode ^ + bottomSheetBarrierColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('closeButtonColor', closeButtonColor)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('iconMenuPointColor', iconMenuPointColor)) + ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) + ..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle)) + ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)); + } +} 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 new file mode 100644 index 00000000..26a995a9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -0,0 +1,237 @@ +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'; + +/// Overrides the default style of [MessageInput] descendants. +/// +/// See also: +/// +/// * [MessageInputThemeData], which is used to configure this theme. +class MessageInputTheme extends InheritedTheme { + /// Creates a [MessageInputTheme]. + /// + /// The [data] parameter must not be null. + const MessageInputTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageInputThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageInputTheme] widget, then + /// [StreamChatThemeData.messageInputTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = MessageInputTheme.of(context); + /// ``` + static MessageInputThemeData of(BuildContext context) { + final messageInputTheme = + context.dependOnInheritedWidgetOfExactType(); + return messageInputTheme?.data ?? + StreamChatTheme.of(context).messageInputTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageInputTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageInputTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageInput] widgets +/// when used with [MessageInputTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.messageInputTheme]. +class MessageInputThemeData with Diagnosticable { + /// Creates a [MessageInputThemeData]. + const MessageInputThemeData({ + this.sendAnimationDuration, + this.actionButtonColor, + this.sendButtonColor, + this.actionButtonIdleColor, + this.sendButtonIdleColor, + this.inputBackgroundColor, + this.inputTextStyle, + this.inputDecoration, + this.activeBorderGradient, + this.idleBorderGradient, + this.borderRadius, + this.expandButtonColor, + }); + + /// Duration of the [MessageInput] send button animation + final Duration? sendAnimationDuration; + + /// Background color of [MessageInput] send button + final Color? sendButtonColor; + + /// Background color of [MessageInput] action buttons + final Color? actionButtonColor; + + /// Background color of [MessageInput] send button + final Color? sendButtonIdleColor; + + /// Background color of [MessageInput] action buttons + final Color? actionButtonIdleColor; + + /// Background color of [MessageInput] expand button + final Color? expandButtonColor; + + /// Background color of [MessageInput] + final Color? inputBackgroundColor; + + /// TextStyle of [MessageInput] + final TextStyle? inputTextStyle; + + /// InputDecoration of [MessageInput] + final InputDecoration? inputDecoration; + + /// Border gradient when the [MessageInput] is not focused + final Gradient? idleBorderGradient; + + /// Border gradient when the [MessageInput] is focused + final Gradient? activeBorderGradient; + + /// Border radius of [MessageInput] + final BorderRadius? borderRadius; + + /// Returns a new [MessageInputThemeData] replacing some of its properties + MessageInputThemeData copyWith({ + Duration? sendAnimationDuration, + Color? inputBackgroundColor, + Color? actionButtonColor, + Color? sendButtonColor, + Color? actionButtonIdleColor, + Color? sendButtonIdleColor, + Color? expandButtonColor, + TextStyle? inputTextStyle, + InputDecoration? inputDecoration, + Gradient? activeBorderGradient, + Gradient? idleBorderGradient, + BorderRadius? borderRadius, + }) => + MessageInputThemeData( + sendAnimationDuration: + sendAnimationDuration ?? this.sendAnimationDuration, + inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, + actionButtonColor: actionButtonColor ?? this.actionButtonColor, + sendButtonColor: sendButtonColor ?? this.sendButtonColor, + actionButtonIdleColor: + actionButtonIdleColor ?? this.actionButtonIdleColor, + expandButtonColor: expandButtonColor ?? this.expandButtonColor, + inputTextStyle: inputTextStyle ?? this.inputTextStyle, + sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, + inputDecoration: inputDecoration ?? this.inputDecoration, + activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, + idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, + borderRadius: borderRadius ?? this.borderRadius, + ); + + /// Linearly interpolate from one [MessageInputThemeData] to another. + MessageInputThemeData lerp( + MessageInputThemeData a, + MessageInputThemeData b, + double t, + ) => + MessageInputThemeData( + actionButtonColor: + Color.lerp(a.actionButtonColor, b.actionButtonColor, t), + actionButtonIdleColor: + Color.lerp(a.actionButtonIdleColor, b.actionButtonIdleColor, t), + activeBorderGradient: + Gradient.lerp(a.activeBorderGradient, b.activeBorderGradient, t), + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), + expandButtonColor: + Color.lerp(a.expandButtonColor, b.expandButtonColor, t), + idleBorderGradient: + Gradient.lerp(a.idleBorderGradient, b.idleBorderGradient, t), + inputBackgroundColor: + Color.lerp(a.inputBackgroundColor, b.inputBackgroundColor, t), + inputTextStyle: TextStyle.lerp(a.inputTextStyle, b.inputTextStyle, t), + sendButtonColor: Color.lerp(a.sendButtonColor, b.sendButtonColor, t), + sendButtonIdleColor: + Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), + sendAnimationDuration: a.sendAnimationDuration, + inputDecoration: a.inputDecoration, + ); + + /// Merges [this] [MessageInputThemeData] with the [other] + MessageInputThemeData merge(MessageInputThemeData? other) { + if (other == null) return this; + return copyWith( + sendAnimationDuration: other.sendAnimationDuration, + inputBackgroundColor: other.inputBackgroundColor, + actionButtonColor: other.actionButtonColor, + actionButtonIdleColor: other.actionButtonIdleColor, + sendButtonColor: other.sendButtonColor, + sendButtonIdleColor: other.sendButtonIdleColor, + inputTextStyle: + inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, + inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? + other.inputDecoration, + activeBorderGradient: other.activeBorderGradient, + idleBorderGradient: other.idleBorderGradient, + borderRadius: other.borderRadius, + expandButtonColor: other.expandButtonColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageInputThemeData && + runtimeType == other.runtimeType && + sendAnimationDuration == other.sendAnimationDuration && + sendButtonColor == other.sendButtonColor && + actionButtonColor == other.actionButtonColor && + sendButtonIdleColor == other.sendButtonIdleColor && + actionButtonIdleColor == other.actionButtonIdleColor && + expandButtonColor == other.expandButtonColor && + inputBackgroundColor == other.inputBackgroundColor && + inputTextStyle == other.inputTextStyle && + inputDecoration == other.inputDecoration && + idleBorderGradient == other.idleBorderGradient && + activeBorderGradient == other.activeBorderGradient && + borderRadius == other.borderRadius; + + @override + int get hashCode => + sendAnimationDuration.hashCode ^ + sendButtonColor.hashCode ^ + actionButtonColor.hashCode ^ + sendButtonIdleColor.hashCode ^ + actionButtonIdleColor.hashCode ^ + expandButtonColor.hashCode ^ + inputBackgroundColor.hashCode ^ + inputTextStyle.hashCode ^ + inputDecoration.hashCode ^ + idleBorderGradient.hashCode ^ + activeBorderGradient.hashCode ^ + borderRadius.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('sendAnimationDuration', sendAnimationDuration)) + ..add(ColorProperty('inputBackgroundColor', inputBackgroundColor)) + ..add(ColorProperty('actionButtonColor', actionButtonColor)) + ..add(ColorProperty('actionButtonIdleColor', actionButtonIdleColor)) + ..add(ColorProperty('sendButtonColor', sendButtonColor)) + ..add(ColorProperty('sendButtonIdleColor', sendButtonIdleColor)) + ..add(DiagnosticsProperty('inputTextStyle', inputTextStyle)) + ..add(DiagnosticsProperty('inputDecoration', inputDecoration)) + ..add(DiagnosticsProperty('activeBorderGradient', activeBorderGradient)) + ..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient)) + ..add(DiagnosticsProperty('borderRadius', borderRadius)) + ..add(ColorProperty('expandButtonColor', expandButtonColor)); + } +} 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 new file mode 100644 index 00000000..8ad8ea9d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [MessageListView] descendants. +/// +/// See also: +/// +/// * [MessageListViewThemeData], which is used to configure this theme. +class MessageListViewTheme extends InheritedTheme { + /// Creates a [MessageListViewTheme]. + /// + /// The [data] parameter must not be null. + const MessageListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageListViewTheme] widget, then + /// [StreamChatThemeData.messageListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// MessageListViewTheme theme = MessageListViewTheme.of(context); + /// ``` + static MessageListViewThemeData 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); + + @override + bool updateShouldNotify(MessageListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageListView]s when +/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.messageListViewTheme]. +/// +/// See also: +/// +/// * [MessageListViewTheme], 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({ + this.backgroundColor, + }); + + /// The color of the [MessageListView] background. + final Color? backgroundColor; + + /// Copies this [MessageListViewThemeData] to another. + MessageListViewThemeData copyWith({ + Color? backgroundColor, + }) => + MessageListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [MessageListView] themes. + /// + /// All the properties must be non-null. + MessageListViewThemeData lerp( + MessageListViewThemeData a, + MessageListViewThemeData b, + double t, + ) => + MessageListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [MessageListViewThemeData] with another. + MessageListViewThemeData merge(MessageListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} 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 new file mode 100644 index 00000000..670e05e7 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart @@ -0,0 +1,112 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [MessageSearchListView] descendants. +/// +/// See also: +/// +/// * [UserListViewThemeData], which is used to configure this theme. +class MessageSearchListViewTheme extends InheritedTheme { + /// Creates a [UserListViewTheme]. + /// + /// The [data] parameter must not be null. + const MessageSearchListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageSearchListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageSearchListView] widget, then + /// [StreamChatThemeData.messageSearchListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); + /// ``` + static MessageSearchListViewThemeData of(BuildContext context) { + final messageSearchListViewTheme = context + .dependOnInheritedWidgetOfExactType(); + return messageSearchListViewTheme?.data ?? + StreamChatTheme.of(context).messageSearchListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageSearchListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => + data != oldWidget.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. +/// * [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({ + this.backgroundColor, + }); + + /// The color of the [MessageSearchListView] background. + final Color? backgroundColor; + + /// Copies this [MessageSearchListViewThemeData] to another. + MessageSearchListViewThemeData copyWith({ + Color? backgroundColor, + }) => + MessageSearchListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [UserListViewThemeData] themes. + /// + /// All the properties must be non-null. + MessageSearchListViewThemeData lerp( + MessageSearchListViewThemeData a, + MessageSearchListViewThemeData b, + double t, + ) => + MessageSearchListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [MessageSearchListViewThemeData] with another. + MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageSearchListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', 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 new file mode 100644 index 00000000..023a2917 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -0,0 +1,180 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; + +/// Class for getting message theme +class MessageThemeData with Diagnosticable { + /// Creates a [MessageThemeData]. + const MessageThemeData({ + this.repliesStyle, + this.messageTextStyle, + this.messageAuthorStyle, + this.messageLinksStyle, + this.messageBackgroundColor, + this.messageBorderColor, + this.reactionsBackgroundColor, + this.reactionsBorderColor, + this.reactionsMaskColor, + this.avatarTheme, + this.createdAtStyle, + }); + + /// Text style for message text + final TextStyle? messageTextStyle; + + /// Text style for message author + final TextStyle? messageAuthorStyle; + + /// Text style for message links + final TextStyle? messageLinksStyle; + + /// Text style for created at text + final TextStyle? createdAtStyle; + + /// Text style for replies + final TextStyle? repliesStyle; + + /// Color for messageBackgroundColor + final Color? messageBackgroundColor; + + /// Color for message border color + final Color? messageBorderColor; + + /// Color for reactions + final Color? reactionsBackgroundColor; + + /// Colors reaction border + final Color? reactionsBorderColor; + + /// Color for reaction mask + final Color? reactionsMaskColor; + + /// Theme of the avatar + final AvatarThemeData? avatarTheme; + + /// Copy with a theme + MessageThemeData copyWith({ + TextStyle? messageTextStyle, + TextStyle? messageAuthorStyle, + TextStyle? messageLinksStyle, + TextStyle? createdAtStyle, + TextStyle? repliesStyle, + Color? messageBackgroundColor, + Color? messageBorderColor, + AvatarThemeData? avatarTheme, + Color? reactionsBackgroundColor, + Color? reactionsBorderColor, + Color? reactionsMaskColor, + }) => + MessageThemeData( + messageTextStyle: messageTextStyle ?? this.messageTextStyle, + messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle, + messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle, + createdAtStyle: createdAtStyle ?? this.createdAtStyle, + messageBackgroundColor: + messageBackgroundColor ?? this.messageBackgroundColor, + messageBorderColor: messageBorderColor ?? this.messageBorderColor, + avatarTheme: avatarTheme ?? this.avatarTheme, + repliesStyle: repliesStyle ?? this.repliesStyle, + reactionsBackgroundColor: + reactionsBackgroundColor ?? this.reactionsBackgroundColor, + reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, + reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, + ); + + /// 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), + createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), + messageAuthorStyle: + TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), + messageBackgroundColor: + Color.lerp(a.messageBackgroundColor, b.messageBackgroundColor, t), + messageBorderColor: + Color.lerp(a.messageBorderColor, b.messageBorderColor, t), + messageLinksStyle: + TextStyle.lerp(a.messageLinksStyle, b.messageLinksStyle, t), + messageTextStyle: + TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t), + reactionsBackgroundColor: Color.lerp( + a.reactionsBackgroundColor, b.reactionsBackgroundColor, t), + reactionsBorderColor: + Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t), + reactionsMaskColor: + Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), + repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t), + ); + + /// Merge with a theme + MessageThemeData merge(MessageThemeData? other) { + if (other == null) return this; + return copyWith( + messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ?? + other.messageTextStyle, + messageAuthorStyle: messageAuthorStyle?.merge(other.messageAuthorStyle) ?? + other.messageAuthorStyle, + messageLinksStyle: messageLinksStyle?.merge(other.messageLinksStyle) ?? + other.messageLinksStyle, + createdAtStyle: + createdAtStyle?.merge(other.createdAtStyle) ?? other.createdAtStyle, + repliesStyle: + repliesStyle?.merge(other.repliesStyle) ?? other.repliesStyle, + messageBackgroundColor: other.messageBackgroundColor, + messageBorderColor: other.messageBorderColor, + avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, + reactionsBackgroundColor: other.reactionsBackgroundColor, + reactionsBorderColor: other.reactionsBorderColor, + reactionsMaskColor: other.reactionsMaskColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageThemeData && + runtimeType == other.runtimeType && + messageTextStyle == other.messageTextStyle && + messageAuthorStyle == other.messageAuthorStyle && + messageLinksStyle == other.messageLinksStyle && + createdAtStyle == other.createdAtStyle && + repliesStyle == other.repliesStyle && + messageBackgroundColor == other.messageBackgroundColor && + messageBorderColor == other.messageBorderColor && + reactionsBackgroundColor == other.reactionsBackgroundColor && + reactionsBorderColor == other.reactionsBorderColor && + reactionsMaskColor == other.reactionsMaskColor && + avatarTheme == other.avatarTheme; + + @override + int get hashCode => + messageTextStyle.hashCode ^ + messageAuthorStyle.hashCode ^ + messageLinksStyle.hashCode ^ + createdAtStyle.hashCode ^ + repliesStyle.hashCode ^ + messageBackgroundColor.hashCode ^ + messageBorderColor.hashCode ^ + reactionsBackgroundColor.hashCode ^ + reactionsBorderColor.hashCode ^ + reactionsMaskColor.hashCode ^ + avatarTheme.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('messageTextStyle', messageTextStyle)) + ..add(DiagnosticsProperty('messageAuthorStyle', messageAuthorStyle)) + ..add(DiagnosticsProperty('messageLinksStyle', messageLinksStyle)) + ..add(DiagnosticsProperty('createdAtStyle', createdAtStyle)) + ..add(DiagnosticsProperty('repliesStyle', repliesStyle)) + ..add(ColorProperty('messageBackgroundColor', messageBackgroundColor)) + ..add(ColorProperty('messageBorderColor', messageBorderColor)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor)) + ..add(ColorProperty('reactionsBorderColor', reactionsBorderColor)) + ..add(ColorProperty('reactionsMaskColor', reactionsMaskColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart new file mode 100644 index 00000000..ec70cf83 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; + +/// Class for holding text theme +class TextTheme { + /// Initialise light text theme + TextTheme.light({ + this.title = const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + this.headlineBold = const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + this.headline = const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + this.bodyBold = const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + this.body = const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + this.footnoteBold = const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + this.footnote = const TextStyle( + fontSize: 12, + color: Colors.black, + ), + this.captionBold = const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + }); + + /// Initialise with dark theme + TextTheme.dark({ + this.title = const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + this.headlineBold = const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + this.headline = const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + this.bodyBold = const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + this.body = const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + this.footnoteBold = const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + this.footnote = const TextStyle( + fontSize: 12, + color: Colors.white, + ), + this.captionBold = const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + }); + + /// Text theme for title + final TextStyle title; + + /// Body Text theme for headline + final TextStyle headlineBold; + + /// Text theme for headline + final TextStyle headline; + + /// Bold Text theme for body + final TextStyle bodyBold; + + /// Text theme body + final TextStyle body; + + /// Bold Text theme for footnote + final TextStyle footnoteBold; + + /// Text theme for footnote + final TextStyle footnote; + + /// Bold Text theme for caption + final TextStyle captionBold; + + /// Copy with theme + TextTheme copyWith({ + Brightness brightness = Brightness.light, + TextStyle? body, + TextStyle? title, + TextStyle? headlineBold, + TextStyle? headline, + TextStyle? bodyBold, + TextStyle? footnoteBold, + TextStyle? footnote, + TextStyle? captionBold, + }) => + brightness == Brightness.light + ? TextTheme.light( + body: body ?? this.body, + title: title ?? this.title, + headlineBold: headlineBold ?? this.headlineBold, + headline: headline ?? this.headline, + bodyBold: bodyBold ?? this.bodyBold, + footnoteBold: footnoteBold ?? this.footnoteBold, + footnote: footnote ?? this.footnote, + captionBold: captionBold ?? this.captionBold, + ) + : TextTheme.dark( + body: body ?? this.body, + title: title ?? this.title, + headlineBold: headlineBold ?? this.headlineBold, + headline: headline ?? this.headline, + bodyBold: bodyBold ?? this.bodyBold, + footnoteBold: footnoteBold ?? this.footnoteBold, + footnote: footnote ?? this.footnote, + captionBold: captionBold ?? this.captionBold, + ); + + /// Merge text theme + TextTheme merge(TextTheme? other) { + if (other == null) return this; + return copyWith( + body: body.merge(other.body), + title: title.merge(other.title), + headlineBold: headlineBold.merge(other.headlineBold), + headline: headline.merge(other.headline), + bodyBold: bodyBold.merge(other.bodyBold), + footnoteBold: footnoteBold.merge(other.footnoteBold), + footnote: footnote.merge(other.footnote), + captionBold: captionBold.merge(other.captionBold), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart new file mode 100644 index 00000000..7e2fb0aa --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -0,0 +1,14 @@ +export 'avatar_theme.dart'; +export 'channel_header_theme.dart'; +export 'channel_list_header_theme.dart'; +export 'channel_list_view_theme.dart'; +export 'channel_preview_theme.dart'; +export 'color_theme.dart'; +export 'gallery_footer_theme.dart'; +export 'gallery_header_theme.dart'; +export 'message_input_theme.dart'; +export 'message_list_view_theme.dart'; +export 'message_search_list_view_theme.dart'; +export 'message_theme.dart'; +export 'text_theme.dart'; +export 'user_list_view_theme.dart'; 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 new file mode 100644 index 00000000..0d99ccbd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [UserListView] descendants. +/// +/// See also: +/// +/// * [UserListViewThemeData], which is used to configure this theme. +class UserListViewTheme extends InheritedTheme { + /// Creates a [UserListViewTheme]. + /// + /// The [data] parameter must not be null. + const UserListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final UserListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [UserListViewTheme] widget, then + /// [StreamChatThemeData.userListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// UserListViewTheme theme = UserListViewTheme.of(context); + /// ``` + static UserListViewThemeData of(BuildContext context) { + final userListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return userListViewTheme?.data ?? + StreamChatTheme.of(context).userListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + UserListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(UserListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [UserListView]s when +/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.userListViewTheme]. +/// +/// See also: +/// +/// * [UserListViewTheme], 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({ + this.backgroundColor, + }); + + /// The color of the [ChannelListView] background. + final Color? backgroundColor; + + /// Copies this [ChannelListViewThemeData] to another. + UserListViewThemeData copyWith({ + Color? backgroundColor, + }) => + UserListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [UserListViewThemeData] themes. + /// + /// All the properties must be non-null. + UserListViewThemeData lerp( + UserListViewThemeData a, + UserListViewThemeData b, + double t, + ) => + UserListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [UserListViewThemeData] with another. + UserListViewThemeData merge(UserListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index a5eb62db..abbd9bf0 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -70,6 +70,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { this.actions, this.onTitleTap, this.showTypingIndicator = true, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -102,9 +103,12 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// if a user is typing in this thread final bool showTypingIndicator; + /// The background color of this [ThreadHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final channelHeaderTheme = ChannelHeaderTheme.of(context); final defaultSubtitle = subtitle ?? Row( @@ -113,12 +117,11 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { children: [ Text( '${context.translations.withText} ', - style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, + style: channelHeaderTheme.subtitleStyle, ), Flexible( child: ChannelName( - textStyle: - chatThemeData.channelTheme.channelHeaderTheme.subtitle, + textStyle: channelHeaderTheme.subtitleStyle, ), ), ], @@ -137,7 +140,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { showUnreads: true, ) : const SizedBox()), - backgroundColor: chatThemeData.channelTheme.channelHeaderTheme.color, + backgroundColor: backgroundColor ?? channelHeaderTheme.color, centerTitle: true, actions: actions, title: InkWell( @@ -151,14 +154,14 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { title ?? Text( context.translations.threadReplyLabel, - style: chatThemeData.channelTheme.channelHeaderTheme.title, + style: channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), if (showTypingIndicator) TypingIndicator( alignment: Alignment.center, channel: StreamChannel.of(context).channel, - style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, + style: channelHeaderTheme.subtitleStyle, parentId: parent.id, alternativeWidget: defaultSubtitle, ) diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 31e07f0a..6a5ef0de 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -19,6 +19,7 @@ class UserAvatar extends StatelessWidget { this.selected = false, this.selectionColor, this.selectionThickness = 4, + this.placeholder, }) : super(key: key); /// User whose avatar is to displayed @@ -54,13 +55,17 @@ class UserAvatar extends StatelessWidget { /// Selection thickness around the avatar final double selectionThickness; + /// The widget that will be built when the user image is loading + final Widget Function(BuildContext, User)? placeholder; + @override Widget build(BuildContext context) { - final hasImage = user.extraData.containsKey('image') && - user.extraData['image'] != null && - user.extraData['image'] != ''; + final hasImage = user.image != null && user.image!.isNotEmpty; final streamChatTheme = StreamChatTheme.of(context); + final placeholder = + this.placeholder ?? streamChatTheme.placeholderUserImage; + Widget avatar = FittedBox( fit: BoxFit.cover, child: ClipRRect( @@ -69,17 +74,16 @@ class UserAvatar extends StatelessWidget { child: Container( constraints: constraints ?? streamChatTheme.ownMessageTheme.avatarTheme?.constraints, - decoration: BoxDecoration( - color: streamChatTheme.colorTheme.accentPrimary, - ), child: hasImage ? CachedNetworkImage( - filterQuality: FilterQuality.high, - // ignore: cast_nullable_to_non_nullable - imageUrl: user.extraData['image'] as String, - errorWidget: (_, __, ___) => - streamChatTheme.defaultUserImage(context, user), fit: BoxFit.cover, + filterQuality: FilterQuality.high, + imageUrl: user.image!, + errorWidget: (context, __, ___) => + streamChatTheme.defaultUserImage(context, user), + placeholder: placeholder != null + ? (context, __) => placeholder(context, user) + : null, ) : streamChatTheme.defaultUserImage(context, user), ), 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 9481e831..eb45f918 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/extension.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 373a61d2..42188fa0 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -12,6 +12,7 @@ export 'src/channel_preview.dart'; export 'src/connection_status_builder.dart'; export 'src/date_divider.dart'; export 'src/deleted_message.dart'; +export 'src/extension.dart' show IconButtonX; export 'src/full_screen_media.dart'; export 'src/gallery_footer.dart'; export 'src/gallery_header.dart'; @@ -36,13 +37,12 @@ export 'src/stream_chat_theme.dart'; export 'src/stream_neumorphic_button.dart'; export 'src/stream_svg_icon.dart'; export 'src/system_message.dart'; +export 'src/theme/themes.dart'; export 'src/thread_header.dart'; export 'src/typing_indicator.dart'; export 'src/unread_indicator.dart'; export 'src/user_avatar.dart'; export 'src/user_item.dart'; -export 'src/user_item.dart'; -export 'src/user_list_view.dart'; export 'src/user_list_view.dart'; export 'src/utils.dart'; export 'src/visible_footnote.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index f47fa9f5..61146b72 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: 2.1.1 +version: 2.1.2 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues 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 c9293e79..f6d110dd 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -48,7 +48,7 @@ void main() { ); testWidgets( - 'it should show the the other member image', + 'it should show the other member image', (tester) async { final client = MockClient(); final clientState = MockClientState(); @@ -74,9 +74,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), ) ])); @@ -85,9 +83,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), ), Member( @@ -98,9 +94,7 @@ void main() { when(() => clientState.usersStream).thenAnswer((i) => Stream.value({ 'user-id2': User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), })); when(() => channel.extraData).thenReturn({ @@ -149,27 +143,21 @@ void main() { userId: 'user-id', user: User( id: 'user-id', - extraData: const { - 'image': 'testimage1', - }, + image: 'testimage1', ), ), Member( userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage2', - }, + image: 'testimage2', ), ), Member( userId: 'user-id3', user: User( id: 'user-id3', - extraData: const { - 'image': 'testimage3', - }, + image: 'testimage3', ), ), ]; 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 7d0c549e..d6f3a5ea 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -21,6 +21,8 @@ void main() { when(() => clientState.currentUser).thenReturn(user); when(() => clientState.currentUserStream) .thenAnswer((_) => Stream.value(user)); + when(() => channel.lastMessageAtStream) + .thenAnswer((_) => Stream.value(lastMessageAt)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); 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 ac953ee9..0d86e89c 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -21,11 +21,11 @@ void main() { client: client, child: const Scaffold( body: DeletedMessage( - messageTheme: MessageTheme( - createdAt: TextStyle( + messageTheme: MessageThemeData( + createdAtStyle: TextStyle( color: Colors.black, ), - messageText: TextStyle(), + messageTextStyle: TextStyle(), ), ), ), 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 5bfe0a34..57f0a282 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -84,7 +84,7 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); - const messageTheme = MessageTheme(); + const messageTheme = MessageThemeData(); final currentUser = OwnUser( id: 'sahil', 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 new file mode 100644 index 00000000..63e6da86 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +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); + }); + + group('AvatarThemeData lerps correctly', () { + test('Lerp completely', () { + expect( + const AvatarThemeData() + .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 1), + _avatarThemeDataControl2); + }); + + test('Lerp halfway', () { + expect( + const AvatarThemeData() + .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 0.5), + _avatarThemeDataControlMidLerp); + }); + }); + + test('Merging two AvatarThemeData results in the latter', () { + expect(_avatarThemeDataControl1.merge(_avatarThemeDataControl2), + _avatarThemeDataControl2); + }); +} + +const _avatarThemeDataControl1 = AvatarThemeData(); + +final _avatarThemeDataControlMidLerp = AvatarThemeData( + borderRadius: BorderRadius.circular(16), + constraints: const BoxConstraints.tightFor( + height: 33, + width: 33, + ), +); + +final _avatarThemeDataControl2 = AvatarThemeData( + borderRadius: BorderRadius.circular(12), + constraints: const BoxConstraints.tightFor( + height: 34, + width: 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 new file mode 100644 index 00000000..475b1084 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +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); + }); + + group('ChannelHeaderThemeData lerps', () { + test( + '''Light ChannelHeaderThemeData lerps completely to dark ChannelHeaderThemeData''', + () { + expect( + const ChannelHeaderThemeData() + .lerp(_channelThemeControl, _channelThemeControlDark, 1), + _channelThemeControlDark); + }); + + test( + '''Light ChannelHeaderThemeData lerps halfway to dark ChannelHeaderThemeData''', + () { + expect( + const ChannelHeaderThemeData() + .lerp(_channelThemeControl, _channelThemeControlDark, 0.5), + _channelThemeControlMidLerp); + }); + + test( + '''Dark ChannelHeaderThemeData lerps completely to light ChannelHeaderThemeData''', + () { + expect( + const ChannelHeaderThemeData() + .lerp(_channelThemeControlDark, _channelThemeControl, 1), + _channelThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect(_channelThemeControl.merge(_channelThemeControlDark), + _channelThemeControlDark); + }); +} + +final _channelThemeControl = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: const Color(0xff101418), + titleStyle: TextTheme.light().headlineBold.copyWith( + color: const Color(0xffffffff), + ), + subtitleStyle: TextTheme.light().footnote.copyWith( + color: const Color(0xff7a7a7a), + ), +); + +final _channelThemeControlMidLerp = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: const Color(0xff101418), + titleStyle: const TextStyle( + color: Color(0xffffffff), + fontWeight: FontWeight.bold, + fontSize: 16, + ), + subtitleStyle: TextTheme.light().footnote.copyWith( + color: const Color(0xff7a7a7a), + ), +); + +final _channelThemeControlDark = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( + 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: 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 new file mode 100644 index 00000000..aa378f3a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +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); + }); + + group('ChannelListHeaderThemeData lerps', () { + test( + '''Light ChannelListHeaderThemeData lerps completely to dark ChannelListHeaderThemeData''', + () { + expect( + const ChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControl, + _channelListHeaderThemeControlDark, + 1), + _channelListHeaderThemeControlDark); + }); + + test( + '''Light ChannelListHeaderThemeData lerps halfway to dark ChannelListHeaderThemeData''', + () { + expect( + const ChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControl, + _channelListHeaderThemeControlDark, + 0.5), + _channelListHeaderThemeControlMidLerp); + }); + + test( + '''Dark ChannelListHeaderThemeData lerps completely to light ChannelListHeaderThemeData''', + () { + expect( + const ChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControlDark, + _channelListHeaderThemeControl, + 1), + _channelListHeaderThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _channelListHeaderThemeControl + .merge(_channelListHeaderThemeControlDark), + _channelListHeaderThemeControlDark); + }); +} + +final _channelListHeaderThemeControl = ChannelListHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: ColorTheme.light().barsBg, + titleStyle: TextTheme.light().headlineBold, +); + +final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: const Color(0xff87898b), + titleStyle: const TextStyle( + color: Color(0xff7f7f7f), + fontSize: 16, + fontWeight: FontWeight.bold, + ), +); + +final _channelListHeaderThemeControlDark = ChannelListHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: ColorTheme.dark().barsBg, + titleStyle: TextTheme.dark().headlineBold, +); diff --git a/packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart index 80a21d25..38b1c524 100644 --- a/packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { test('ChannelListViewThemeData copyWith, ==, hashCode basics', () { 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 new file mode 100644 index 00000000..3be39952 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +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); + }); + + group('ChannelPreviewThemeData lerps', () { + test( + '''Light ChannelPreviewThemeData lerps completely to dark ChannelPreviewThemeData''', + () { + expect( + const ChannelPreviewThemeData().lerp( + _channelPreviewThemeControl, _channelPreviewThemeControlDark, 1), + _channelPreviewThemeControlDark); + }); + + test( + '''Light ChannelPreviewThemeData lerps halfway to dark ChannelPreviewThemeData''', + () { + expect( + const ChannelPreviewThemeData().lerp(_channelPreviewThemeControl, + _channelPreviewThemeControlDark, 0.5), + _channelPreviewThemeControlMidLerp); + }); + + test( + '''Dark ChannelPreviewThemeData lerps completely to light ChannelPreviewThemeData''', + () { + expect( + const ChannelPreviewThemeData().lerp( + _channelPreviewThemeControlDark, _channelPreviewThemeControl, 1), + _channelPreviewThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect(_channelPreviewThemeControl.merge(_channelPreviewThemeControlDark), + _channelPreviewThemeControlDark); + }); +} + +final _channelPreviewThemeControl = ChannelPreviewThemeData( + unreadCounterColor: ColorTheme.light().accentError, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + titleStyle: TextTheme.light().bodyBold, + subtitleStyle: TextTheme.light().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAtStyle: TextTheme.light().footnote.copyWith( + color: ColorTheme.light().textHighEmphasis.withOpacity(.5), + ), + indicatorIconSize: 16, +); + +final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( + unreadCounterColor: const Color(0xffff3742), + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + titleStyle: const TextStyle( + color: Color(0xff7f7f7f), + fontSize: 14, + fontWeight: FontWeight.bold, + ), + subtitleStyle: const TextStyle( + color: Color(0xff7a7a7a), + fontSize: 12, + ), + lastMessageAtStyle: TextTheme.light().footnote.copyWith( + color: const Color(0x807f7f7f).withOpacity(.5), + ), + indicatorIconSize: 16, +); + +final _channelPreviewThemeControlDark = ChannelPreviewThemeData( + unreadCounterColor: ColorTheme.dark().accentError, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + titleStyle: TextTheme.dark().bodyBold, + subtitleStyle: TextTheme.dark().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAtStyle: TextTheme.dark().footnote.copyWith( + color: ColorTheme.dark().textHighEmphasis.withOpacity(.5), + ), + indicatorIconSize: 16, +); diff --git a/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart similarity index 100% rename from packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart similarity index 100% rename from packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart 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 new file mode 100644 index 00000000..c618d65c --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +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); + }); + + group('MessageInputThemeData lerps correctly', () { + test('Lerp completely from light to dark', () { + expect( + const MessageInputThemeData().lerp( + _messageInputThemeControl, _messageInputThemeControlDark, 1), + _messageInputThemeControlDark); + }); + + test('Lerp halfway from light to dark', () { + expect( + const MessageInputThemeData().lerp( + _messageInputThemeControl, _messageInputThemeControlDark, 0.5), + _messageInputThemeControlMidLerp); + }); + + test('Lerp completely from dark to light', () { + expect( + const MessageInputThemeData().lerp( + _messageInputThemeControlDark, _messageInputThemeControl, 1), + _messageInputThemeControl); + }); + }); + + test('Merging two MessageInputThemeData results in the latter', () { + expect(_messageInputThemeControl.merge(_messageInputThemeControlDark), + _messageInputThemeControlDark); + }); +} + +final _messageInputThemeControl = MessageInputThemeData( + 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, + idleBorderGradient: LinearGradient( + stops: const [0.0, 1.0], + colors: [ + ColorTheme.light().disabled, + ColorTheme.light().disabled, + ], + ), + activeBorderGradient: LinearGradient( + stops: const [0.0, 1.0], + colors: [ + ColorTheme.light().disabled, + ColorTheme.light().disabled, + ], + ), +); + +final _messageInputThemeControlMidLerp = MessageInputThemeData( + borderRadius: BorderRadius.circular(20), + sendAnimationDuration: const Duration(milliseconds: 300), + inputBackgroundColor: const Color(0xff87898b), + actionButtonColor: const Color(0xff005fff), + actionButtonIdleColor: const Color(0xff7a7a7a), + sendButtonColor: const Color(0xff005fff), + sendButtonIdleColor: const Color(0xff848585), + expandButtonColor: const Color(0xff005fff), + inputTextStyle: const TextStyle( + color: Color(0xff7f7f7f), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + idleBorderGradient: const LinearGradient( + stops: [0.0, 1.0], + colors: [ + Color(0xff848585), + Color(0xff848585), + ], + ), + activeBorderGradient: const LinearGradient( + stops: [0.0, 1.0], + colors: [ + Color(0xff848585), + Color(0xff848585), + ], + ), +); + +final _messageInputThemeControlDark = MessageInputThemeData( + 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, + idleBorderGradient: LinearGradient( + stops: const [0.0, 1.0], + colors: [ + ColorTheme.dark().disabled, + ColorTheme.dark().disabled, + ], + ), + activeBorderGradient: LinearGradient( + stops: const [0.0, 1.0], + colors: [ + ColorTheme.dark().disabled, + ColorTheme.dark().disabled, + ], + ), +); diff --git a/packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart index ffe54dd0..476f5063 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; class MockStreamChatClient extends Mock implements StreamChatClient {} diff --git a/packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index e485e0d6..4da1c7dc 100644 --- a/packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { test('MessageSearchListViewThemeData copyWith, ==, hashCode basics', () { 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 new file mode 100644 index 00000000..5908d864 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +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); + }); + + group('MessageThemeData lerps', () { + test('''Light MessageThemeData lerps completely to dark MessageThemeData''', + () { + expect( + const MessageThemeData() + .lerp(_messageThemeControl, _messageThemeControlDark, 1), + _messageThemeControlDark); + }); + + test('''Dark MessageThemeData lerps completely to light MessageThemeData''', + () { + expect( + const MessageThemeData() + .lerp(_messageThemeControlDark, _messageThemeControl, 1), + _messageThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect(_messageThemeControl.merge(_messageThemeControlDark), + _messageThemeControlDark); + }); +} + +final _messageThemeControl = MessageThemeData( + messageAuthorStyle: TextTheme.light().footnote.copyWith( + color: ColorTheme.light().textLowEmphasis, + ), + messageTextStyle: TextTheme.light().body, + createdAtStyle: TextTheme.light().footnote.copyWith( + color: ColorTheme.light().textLowEmphasis, + ), + repliesStyle: TextTheme.light().footnoteBold.copyWith( + color: ColorTheme.light().accentPrimary, + ), + messageBackgroundColor: ColorTheme.light().disabled, + reactionsBackgroundColor: ColorTheme.light().barsBg, + reactionsBorderColor: ColorTheme.light().borders, + reactionsMaskColor: ColorTheme.light().appBg, + messageBorderColor: ColorTheme.light().disabled, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 32, + width: 32, + ), + ), + messageLinksStyle: TextStyle( + color: ColorTheme.light().accentPrimary, + ), +); + +final _messageThemeControlDark = MessageThemeData( + messageAuthorStyle: TextTheme.dark().footnote.copyWith( + color: ColorTheme.dark().textLowEmphasis, + ), + messageTextStyle: TextTheme.dark().body, + createdAtStyle: TextTheme.dark().footnote.copyWith( + color: ColorTheme.dark().textLowEmphasis, + ), + repliesStyle: TextTheme.dark().footnoteBold.copyWith( + color: ColorTheme.dark().accentPrimary, + ), + messageBackgroundColor: ColorTheme.dark().disabled, + reactionsBackgroundColor: ColorTheme.dark().barsBg, + reactionsBorderColor: ColorTheme.dark().borders, + reactionsMaskColor: ColorTheme.dark().appBg, + messageBorderColor: ColorTheme.dark().disabled, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 32, + width: 32, + ), + ), + messageLinksStyle: TextStyle( + color: ColorTheme.dark().accentPrimary, + ), +); diff --git a/packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart index e9b7985a..a22eebd0 100644 --- a/packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { test('UserListViewThemeData copyWith, ==, hashCode basics', () { diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index ef24e401..65280674 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +ЁЯРЮ Fixed +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh + ## 2.1.1 - Updated llc dependency diff --git a/packages/stream_chat_flutter_core/README.md b/packages/stream_chat_flutter_core/README.md index 462498f6..e625aeeb 100644 --- a/packages/stream_chat_flutter_core/README.md +++ b/packages/stream_chat_flutter_core/README.md @@ -26,7 +26,7 @@ It teaches you how to use this SDK and also shows how to make frequently require ## Example App This repo includes a fully functional example app with setup instructions. -The example is available under the [example](https://github.com/GetStream/stream-chat-flutter-core/tree/master/example) folder. +The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/main/packages/stream_chat_flutter_core/example) folder. ## Add dependency Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter_core.svg)](https://pub.dartlang.org/packages/stream_chat_flutter_core) diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 0a7b0bce..31410e63 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -13,13 +13,10 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: const { - 'image': - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - }, + image: + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9' - '.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', ); runApp( @@ -332,21 +329,9 @@ class _MessageScreenState extends State { } } -/// Extensions can be used to add functionality to the SDK. In the examples -/// below, we add two simple extensions to the [StreamChatClient] and [Channel]. +/// Extensions can be used to add functionality to the SDK. In the example +/// below, we add a simple extensions to the [StreamChatClient]. extension on StreamChatClient { /// Fetches the current user id. String get uid => state.currentUser!.id; } - -extension on Channel { - /// Fetches the name of the channel by accessing [extraData] or [cid]. - String? get name { - final _channelName = extraData['name']; - if (_channelName != null) { - return _channelName as String; - } else { - return cid; - } - } -} 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 46aaccee..6687eef5 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -106,6 +106,9 @@ class ChannelsBlocState extends State final client = _streamChatCoreState!.client; final clear = paginationParams.offset == 0; + if (clear && _paginationEnded) { + _paginationEnded = false; + } if ((!clear && _paginationEnded) || _queryChannelsLoadingController.value == true) { diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index a1ae75b0..d718ce12 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,9 +1,19 @@ ## Upcoming +тЬЕ Added + * Added support for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) locale. * Added support for [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) locale. * Added support for [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) locale. +ЁЯФД Changed + +* Some of the `Hindi` translations have been updated/changed for better understanding. + - 'рд░рд┐рдкреНрд▓рд╛рдИ' -> 'рдЬрд╡рд╛рдм рджреЗрдВ' + - 'рддрд╕реНрд╡реАрд░реЗрдВ' -> 'реЮреЛрдЯреЛрдЬ' + - 'рдмрд┐рддрд╛ рд╣реБрдЖ рдХрд▓' -> 'рдХрд▓' + - 'рдЪреИрдирд▓ рдореМрди рд╣реИ' -> 'рдЪреИрдирд▓ рдореНрдпреВрдЯ рд╣реИ' + ## 1.0.2 * Updated `stream_chat_flutter` dependency diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index a64717d3..1efe6c95 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -403,8 +403,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 76194992..c71ceb01 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -20,8 +20,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index 8eafd257..a52fcf9f 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -45,8 +45,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); 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 247b90f8..975e8097 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 @@ -197,7 +197,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { 'рдХрд╛рд░реНрд░рд╡рд╛рдИ рдкреВрд░реА рдирд╣реАрдВ рдХреА рдЬрд╛ рд╕рдХреА.'; @override - String get replyLabel => 'рд░рд┐рдкреНрд▓рд╛рдИ'; + String get replyLabel => 'рдЬрд╡рд╛рдм рджреЗрдВ'; @override String togglePinUnpinText({required bool pinned}) { @@ -224,7 +224,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { } @override - String get photosLabel => 'рддрд╕реНрд╡реАрд░реЗрдВ'; + String get photosLabel => 'реЮреЛрдЯреЛрдЬ'; String _getDay(DateTime dateTime) { final now = DateTime.now(); @@ -250,10 +250,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get todayLabel => 'рдЖрдЬ'; @override - String get yesterdayLabel => 'рдмрд┐рддрд╛ рд╣реБрдЖ рдХрд▓'; + String get yesterdayLabel => 'рдХрд▓'; @override - String get channelIsMutedText => 'рдЪреИрдирд▓ рдореМрди рд╣реИ'; + String get channelIsMutedText => 'рдЪреИрдирд▓ рдореНрдпреВрдЯ рд╣реИ'; @override String get noTitleText => 'рдХреЛрдИ рд╢реАрд░реНрд╖рдХ рдирд╣реАрдВ'; diff --git a/packages/stream_chat_persistence/example/lib/main.dart b/packages/stream_chat_persistence/example/lib/main.dart index 5c83ed78..e187da56 100644 --- a/packages/stream_chat_persistence/example/lib/main.dart +++ b/packages/stream_chat_persistence/example/lib/main.dart @@ -22,10 +22,8 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: const { - 'image': - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - }, + image: + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.' 'gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo',