Conflicts:
	packages/stream_chat/lib/src/client/channel.dart
	packages/stream_chat_flutter/example/lib/main.dart
	packages/stream_chat_flutter/lib/src/message_list_view.dart
This commit is contained in:
Deven Joshi
2021-08-16 14:05:18 +05:30
98 changed files with 5094 additions and 2424 deletions
+4 -7
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

@@ -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: <Widget>[
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: <Widget>[
// 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(),
],
),
),
],
),
)
```
@@ -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.
@@ -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.
@@ -25,6 +25,7 @@ At the moment we support the following languages:
- [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart)
- [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart)
- [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart)
- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart)
More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages.
@@ -62,6 +63,7 @@ class MyApp extends StatelessWidget {
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
// Add GlobalStreamChatLocalizations.delegates
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
@@ -130,6 +132,7 @@ Here is an example of how that would look like:
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
// locales are the locales of the device
// supportedLocales are the app supported locales
@@ -172,5 +175,6 @@ Example:
<string>nb</string>
<string>fr</string>
<string>it</string>
<string>es</string>
</array>
```
@@ -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
},
),
],
);
},
)
```
@@ -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])
])
```
+12
View File
@@ -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
+3 -4
View File
@@ -13,10 +13,9 @@ Future<void> main() async {
await client.connectUser(
User(
id: 'cool-shadow-7',
extraData: const {
'image':
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
},
name: 'Cool Shadow',
image:
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
),
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''',
);
+299 -119
View File
@@ -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<String, Object?>? 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,66 +110,92 @@ class Channel {
String? _cid;
final Map<String, Object?> _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<String, Object?> 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
/// Returns true if the channel is muted, as a stream.
Stream<bool>? get isMutedStream => _client.state.currentUserStream
.map((event) =>
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;
}
/// Channel configuration as a stream
/// Channel configuration as a stream.
Stream<ChannelConfig?>? get configStream {
_checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.config);
}
/// Channel user creator
/// Channel user creator.
User? get createdBy {
_checkInitialized();
return state?._channelState.channel?.createdBy;
}
/// Channel user creator as a stream
/// Channel user creator as a stream.
Stream<User?>? get createdByStream {
_checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.createdBy);
}
/// Channel frozen status
/// Channel frozen status.
bool? get frozen {
_checkInitialized();
return state?._channelState.channel?.frozen;
}
/// Channel frozen status as a stream
/// Channel frozen status as a stream.
Stream<bool?>? get frozenStream {
_checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.frozen);
@@ -133,90 +216,90 @@ class Channel {
///
DateTime? cooldownStartedAt;
/// Channel creation date
/// Channel creation date.
DateTime? get createdAt {
_checkInitialized();
return state?._channelState.channel?.createdAt;
}
/// Channel creation date as a stream
/// Channel creation date as a stream.
Stream<DateTime?>? get createdAtStream {
_checkInitialized();
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;
}
/// Channel last message date as a stream
/// Channel last message date as a stream.
Stream<DateTime?>? get lastMessageAtStream {
_checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.lastMessageAt);
}
/// Channel updated date
/// Channel updated date.
DateTime? get updatedAt {
_checkInitialized();
return state?._channelState.channel?.updatedAt;
}
/// Channel updated date as a stream
/// Channel updated date as a stream.
Stream<DateTime?>? get updatedAtStream {
_checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.updatedAt);
}
/// Channel deletion date
/// Channel deletion date.
DateTime? get deletedAt {
_checkInitialized();
return state?._channelState.channel?.deletedAt;
}
/// Channel deletion date as a stream
/// Channel deletion date as a stream.
Stream<DateTime?>? get deletedAtStream {
_checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.deletedAt);
}
/// Channel member count
/// Channel member count.
int? get memberCount {
_checkInitialized();
return state?._channelState.channel?.memberCount;
}
/// Channel member count as a stream
/// Channel member count as a stream.
Stream<int?>? get memberCountStream {
_checkInitialized();
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;
}
/// Channel extra data
/// Channel extra data.
Map<String, Object?> get extraData {
var data = state?._channelState.channel?.extraData;
if (data == null || data.isEmpty) {
@@ -225,7 +308,7 @@ class Channel {
return data;
}
/// Channel extra data as a stream
/// Channel extra data as a stream.
Stream<Map<String, dynamic>> get extraDataStream {
_checkInitialized();
return state!.channelStateStream.map(
@@ -233,15 +316,46 @@ class Channel {
);
}
/// 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<String?> 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<String?> 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<bool> _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<bool> get initialized => _initializedCompleter.future;
final _cancelableAttachmentUploadRequest = <String, CancelToken>{};
@@ -377,7 +491,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<SendMessageResponse> sendMessage(
@@ -445,6 +561,7 @@ class Channel {
}
/// Updates the [message] in this channel.
///
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
/// before actually updating the message.
Future<UpdateMessageResponse> updateMessage(Message message) async {
@@ -507,8 +624,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<UpdateMessageResponse> partialUpdateMessage(
Message message, {
Map<String, Object?>? set,
@@ -608,7 +727,7 @@ class Channel {
);
}
/// Unpins provided message
/// Unpins provided message.
Future<UpdateMessageResponse> unpinMessage(Message message) =>
partialUpdateMessage(
message,
@@ -617,7 +736,7 @@ class Channel {
},
);
/// Send a file to this channel
/// Send a file to this channel.
Future<SendFileResponse> sendFile(
AttachmentFile file, {
ProgressCallback? onSendProgress,
@@ -633,7 +752,7 @@ class Channel {
);
}
/// Send an image to this channel
/// Send an image to this channel.
Future<SendImageResponse> sendImage(
AttachmentFile file, {
ProgressCallback? onSendProgress,
@@ -649,7 +768,7 @@ class Channel {
);
}
/// A message search.
/// Search for a message with the given options.
Future<SearchMessagesResponse> search({
String? query,
Filter? messageFilters,
@@ -666,7 +785,7 @@ class Channel {
);
}
/// Delete a file from this channel
/// Delete a file from this channel.
Future<EmptyResponse> deleteFile(
String url, {
CancelToken? cancelToken,
@@ -680,7 +799,7 @@ class Channel {
);
}
/// Delete an image from this channel
/// Delete an image from this channel.
Future<EmptyResponse> deleteImage(
String url, {
CancelToken? cancelToken,
@@ -694,14 +813,15 @@ class Channel {
);
}
/// Send an event on this channel
/// Send an event on this channel.
Future<EmptyResponse> 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<SendReactionResponse> sendReaction(
Message message,
String type, {
@@ -764,7 +884,7 @@ class Channel {
}
}
/// Delete a reaction from this channel
/// Delete a reaction from this channel.
Future<EmptyResponse> deleteReaction(
Message message, Reaction reaction) async {
final type = reaction.type;
@@ -810,7 +930,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<PartialUpdateChannelResponse> 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<PartialUpdateChannelResponse> 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<UpdateChannelResponse> update(
Map<String, Object?> channelData, [
Message? updateMessage,
@@ -824,7 +986,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<PartialUpdateChannelResponse> updatePartial({
Map<String, Object?>? set,
List<String>? unset,
@@ -853,25 +1026,25 @@ class Channel {
return _client.deleteChannel(id!, type);
}
/// Removes all messages from the channel
/// Removes all messages from the channel.
Future<EmptyResponse> truncate() async {
_checkInitialized();
return _client.truncateChannel(id!, type);
}
/// Accept invitation to the channel
/// Accept invitation to the channel.
Future<AcceptInviteResponse> acceptInvite([Message? message]) async {
_checkInitialized();
return _client.acceptChannelInvite(id!, type, message: message);
}
/// Reject invitation to the channel
/// Reject invitation to the channel.
Future<RejectInviteResponse> rejectInvite([Message? message]) async {
_checkInitialized();
return _client.rejectChannelInvite(id!, type, message: message);
}
/// Add members to the channel
/// Add members to the channel.
Future<AddMembersResponse> addMembers(
List<String> memberIds, [
Message? message,
@@ -880,7 +1053,7 @@ class Channel {
return _client.addChannelMembers(id!, type, memberIds, message: message);
}
/// Invite members to the channel
/// Invite members to the channel.
Future<InviteMembersResponse> inviteMembers(
List<String> memberIds, [
Message? message,
@@ -889,7 +1062,7 @@ class Channel {
return _client.inviteChannelMembers(id!, type, memberIds, message: message);
}
/// Remove members from the channel
/// Remove members from the channel.
Future<RemoveMembersResponse> removeMembers(
List<String> memberIds, [
Message? message,
@@ -898,7 +1071,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<SendActionResponse> sendAction(
Message message,
Map<String, dynamic> formData,
@@ -945,9 +1118,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<EmptyResponse> markRead({String? messageId}) async {
_checkInitialized();
client.state.totalUnreadCount =
@@ -956,7 +1130,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<ChannelState> watch() async {
ChannelState response;
@@ -987,15 +1161,16 @@ class Channel {
}
}
/// Stop watching the channel
/// Stop watching the channel.
Future<EmptyResponse> 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<QueryRepliesResponse> getReplies(
String parentId, {
PaginationParams? options,
@@ -1019,7 +1194,7 @@ class Channel {
return repliesResponse;
}
/// List the reactions for a message in the channel
/// List the reactions for a message in the channel.
Future<QueryReactionsResponse> getReactions(
String messageId, {
PaginationParams? pagination,
@@ -1029,7 +1204,7 @@ class Channel {
pagination: pagination,
);
/// Retrieves a list of messages by ID
/// Retrieves a list of messages by given [messageIDs].
Future<GetMessagesByIdResponse> getMessagesById(
List<String> messageIDs,
) async {
@@ -1040,7 +1215,7 @@ class Channel {
return res;
}
/// Retrieves a list of messages by ID
/// Translate a message by given [messageId] and [language].
Future<TranslateMessageResponse> translateMessage(
String messageId,
String language,
@@ -1050,12 +1225,13 @@ class Channel {
language,
);
/// Creates a new channel
/// Creates a new channel.
Future<ChannelState> 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<ChannelState> query({
bool state = true,
bool watch = false,
@@ -1109,7 +1285,7 @@ class Channel {
}
}
/// Query channel members
/// Query channel members.
Future<QueryMembersResponse> queryMembers({
Filter? filter,
List<SortOption>? sort,
@@ -1124,19 +1300,19 @@ class Channel {
pagination: pagination,
);
/// Mutes the channel
/// Mutes the channel.
Future<EmptyResponse> mute({Duration? expiration}) {
_checkInitialized();
return _client.muteChannel(cid!, expiration: expiration);
}
/// Unmutes the channel
/// Unmute the channel.
Future<EmptyResponse> unmute() {
_checkInitialized();
return _client.unmuteChannel(cid!);
}
/// Bans a user from the channel
/// Bans the user with given [userID] from the channel.
Future<EmptyResponse> banUser(
String userID,
Map<String, dynamic> options,
@@ -1150,7 +1326,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<EmptyResponse> unbanUser(String userID) async {
_checkInitialized();
return _client.unbanUser(userID, {
@@ -1159,7 +1335,7 @@ class Channel {
});
}
/// Shadow bans a user from the channel
/// Shadow bans the user with the given [userID] from the channel.
Future<EmptyResponse> shadowBan(
String userID,
Map<String, dynamic> options,
@@ -1173,7 +1349,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<EmptyResponse> removeShadowBan(String userID) async {
_checkInitialized();
return _client.removeShadowBan(userID, {
@@ -1183,8 +1359,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<EmptyResponse> hide({bool clearHistory = false}) async {
_checkInitialized();
final response = await _client.hideChannel(
@@ -1202,7 +1380,7 @@ class Channel {
return response;
}
/// Removes the hidden status for the channel
/// Removes the hidden status for the channel.
Future<EmptyResponse> show() async {
_checkInitialized();
return _client.showChannel(id!, type);
@@ -1210,7 +1388,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<Event> on([
String? eventType,
String? eventType2,
@@ -1248,7 +1426,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<void> stopTyping([String? parentId]) async {
if (config?.typingEvents == false) {
return;
@@ -1262,7 +1440,7 @@ class Channel {
));
}
/// Call this method to dispose the channel client
/// Call this method to dispose the channel client.
void dispose() {
state?.dispose();
}
@@ -1276,9 +1454,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,
@@ -1425,23 +1603,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<bool> get isUpToDateStream => _isUpToDateController.stream;
final BehaviorSubject<bool> _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<void> retryFailedMessages() async {
final failedMessages =
<Message>[...messages, ...threads.values.expand((v) => v)]
@@ -1534,7 +1714,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<Message>.from(_channelState.messages);
@@ -1599,36 +1779,36 @@ class ChannelClientState {
);
}
/// Channel message list
/// Channel message list.
List<Message> get messages => _channelState.messages;
/// Channel message list as a stream
/// Channel message list as a stream.
Stream<List<Message>?> get messagesStream => channelStateStream
.map((cs) => cs.messages)
.distinct(const ListEquality().equals);
/// Channel pinned message list
/// Channel pinned message list.
List<Message>? get pinnedMessages => _channelState.pinnedMessages.toList();
/// Channel pinned message list as a stream
/// Channel pinned message list as a stream.
Stream<List<Message>?> 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
/// Get channel last message.
Stream<Message?> get lastMessageStream => messagesStream
.map((event) => event?.isNotEmpty == true ? event!.last : null);
/// Channel members list
/// Channel members list.
List<Member> 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<List<Member>> get membersStream => CombineLatestStream.combine2<
List<Member?>?, Map<String?, User?>, List<Member>>(
channelStateStream.map((cs) => cs.members),
@@ -1637,19 +1817,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<int?> get watcherCountStream =>
channelStateStream.map((cs) => cs.watcherCount);
/// Channel watchers list
/// Channel watchers list.
List<User> 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<List<User>> get watchersStream => CombineLatestStream.combine2<
List<User>?, Map<String?, User?>, List<User>>(
channelStateStream.map((cs) => cs.watchers),
@@ -1657,20 +1837,20 @@ class ChannelClientState {
(watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(),
);
/// Channel read list
/// Channel read list.
List<Read>? get read => _channelState.read;
/// Channel read list as a stream
/// Channel read list as a stream.
Stream<List<Read>?> get readStream => channelStateStream.map((cs) => cs.read);
final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0);
set unreadCount(int value) => _unreadCountController.add(value);
/// Unread count getter as a stream
/// Unread count getter as a stream.
Stream<int> get unreadCountStream => _unreadCountController.stream.distinct();
/// Unread count getter
/// Unread count getter.
int get unreadCount => _unreadCountController.value;
bool _countMessageAsUnread(Message message) {
@@ -1686,7 +1866,7 @@ class ChannelClientState {
!userIsMuted;
}
/// Update threads with updated information about messages
/// Update threads with updated information about messages.
void updateThreadInfo(String parentId, List<Message> messages) {
final newThreads = Map<String, List<Message>>.from(threads);
@@ -1708,7 +1888,7 @@ class ChannelClientState {
_threads = newThreads;
}
/// Delete all channel messages
/// Delete all channel messages.
void truncate() {
_channelState = _channelState.copyWith(
messages: [],
@@ -1717,7 +1897,7 @@ class ChannelClientState {
final List<String> _updatedMessagesIds = [];
/// Update channelState with updated information
/// Update channelState with updated information.
void updateChannelState(ChannelState updatedState) {
final newMessages = <Message>[
...updatedState.messages,
@@ -1769,13 +1949,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<ChannelState> 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<ChannelState> _channelStateController;
@@ -1786,11 +1966,11 @@ class ChannelClientState {
_debouncedUpdatePersistenceChannelState.call([v]);
}
/// The channel threads related to this channel
/// The channel threads related to this channel.
Map<String, List<Message>> 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<Map<String, List<Message>>> get threadsStream =>
_threadsController.stream;
final BehaviorSubject<Map<String, List<Message>>> _threadsController =
@@ -1804,10 +1984,10 @@ class ChannelClientState {
_threadsController.add(v);
}
/// Channel related typing users last value
/// Channel related typing users last value.
Map<User, Event> get typingEvents => _typingEventsController.value;
/// Channel related typing users stream
/// Channel related typing users stream.
Stream<Map<User, Event>> get typingEventsStream =>
_typingEventsController.stream;
@@ -1935,7 +2115,7 @@ class ChannelClientState {
});
}
/// Call this method to dispose this object
/// Call this method to dispose this object.
void dispose() {
_debouncedUpdatePersistenceChannelState.cancel();
_unreadCountController.close();
@@ -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<UpdateChannelResponse> 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<PartialUpdateChannelResponse> updateChannelPartial(
String channelId,
String channelType, {
@@ -84,7 +84,7 @@ class ChannelApi {
/// Mark all channels for this user as read
Future<EmptyResponse> markAllRead() async {
final response = await _client.post('channels/read');
final response = await _client.post('/channels/read');
return EmptyResponse.fromJson(response.data);
}
@@ -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<String, dynamic> 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: <Device>[])
final List<Device> devices;
/// List of users muted by the user
/// List of users muted by the user.
@JsonKey(includeIfNull: false, defaultValue: <Mute>[])
final List<Mute> mutes;
/// List of users muted by the user
/// List of users muted by the user.
@JsonKey(includeIfNull: false, defaultValue: <Mute>[])
final List<Mute> 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',
@@ -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<String, Object?> 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<String, dynamic> 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: <String>[],
)
final List<String> 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<String, Object?> 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: <String>[],
)
final List<String> 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<String, Object?> extraData;
/// The language this user prefers.
@JsonKey(includeIfNull: false)
final String? language;
/// List of users to list of userIds.
static List<String>? toIds(List<User>? users) =>
users?.map((u) => u.id).toList();
/// Serialize to json
/// Serialize to json.
Map<String, dynamic> 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<Object?> get props => [id];
List<Object?> get props => [id, role];
}
@@ -19,8 +19,8 @@ User _$UserFromJson(Map<String, dynamic> 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<String, dynamic>? ?? {},
online: json['online'] as bool? ?? false,
banned: json['banned'] as bool? ?? false,
teams:
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
+11
View File
@@ -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&amp;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"
}
@@ -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<StateError>());
}
});
test('should throw if trying to set `name`', () {
try {
channel.name = 'New name';
} catch (e) {
expect(e, isA<StateError>());
}
});
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',
@@ -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: <String, dynamic>{}));
@@ -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');
});
});
}
@@ -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');
@@ -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&amp;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);
});
});
}
+114 -47
View File
@@ -1,3 +1,58 @@
## 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.
## 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 +69,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 +84,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 +115,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 +123,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 +133,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 +148,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 +185,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 +272,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 +330,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 +348,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 +383,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 +569,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 +591,7 @@ Widget build(BuildContext context) {
},
home: ChannelListPage(),
);
}
```
- Fix reaction bubble going below previous message on iOS
@@ -608,8 +675,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
@@ -10,7 +10,7 @@ void main() async {
/// Create a new instance of [StreamChatClient] passing the apikey obtained
/// from your project dashboard.
final client = StreamChatClient(
'kv7mcsxr24p8',
's2dxdhpxd94g',
logLevel: Level.INFO,
);
@@ -21,19 +21,14 @@ void main() async {
/// Please see the following for more information:
/// https://getstream.io/chat/docs/ios_user_setup_and_tokens/
await client.connectUser(
User(id: 'salvatore'),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.pgiJz7sIc7iP29BHKFwe3nLm5-OaR_1l2P-SlgiC9a8',
User(id: 'super-band-9'),
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
final channel = client.channel('messaging', id: 'godevs2', extraData: {
'members': ['salvatore'],
});
final channel = client.channel('messaging', id: 'godevs');
await channel.watch();
await channel.enableSlowMode(cooldownInterval: 20);
print('Channel cooldown set to ${channel.cooldown}');
runApp(
MyApp(
client: client,
@@ -78,6 +73,7 @@ class MyApp extends StatelessWidget {
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
builder: (context, widget) => StreamChat(
@@ -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
@@ -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),
),
),
@@ -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,
),
],
),
@@ -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;
@@ -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;
@@ -19,6 +19,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
bool _showActions = true;
late StreamChannelState _streamChannelState;
late ChannelPreviewThemeData _channelPreviewThemeData;
late StreamChatThemeData _streamChatThemeData;
late StreamChatState _streamChatState;
@@ -64,8 +65,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
child: ChannelInfo(
showTypingIndicator: false,
channel: _streamChannelState.channel,
textStyle:
_streamChatThemeData.channelPreviewTheme.subtitle,
textStyle: _channelPreviewThemeData.subtitleStyle,
),
),
const SizedBox(
@@ -213,6 +213,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
void didChangeDependencies() {
_streamChannelState = StreamChannel.of(context);
_streamChatThemeData = StreamChatTheme.of(context);
_channelPreviewThemeData = ChannelPreviewTheme.of(context);
_streamChatState = StreamChat.of(context);
super.didChangeDependencies();
}
@@ -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<Widget>? 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 ??
<Widget>[
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: <Widget>[
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,
),
],
),
@@ -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;
@@ -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<Widget>? 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,
@@ -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';
@@ -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<bool>(
stream: channel.isMutedStream,
@@ -91,7 +91,7 @@ class ChannelPreview extends StatelessWidget {
Flexible(
child: title ??
ChannelName(
textStyle: channelPreviewTheme.title,
textStyle: channelPreviewTheme.titleStyle,
),
),
BetterStreamBuilder<List<Member>?>(
@@ -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,
@@ -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,
),
),
),
@@ -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,
);
}
@@ -124,10 +124,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.color,
begin: ChannelHeaderTheme.of(context).color,
end: Colors.black,
).lerp(_controller.value),
),
@@ -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<int>? mediaSelectedCallBack;
/// The background color of this [GalleryFooter].
final Color? backgroundColor;
@override
_GalleryFooterState createState() => _GalleryFooterState();
@@ -89,7 +94,8 @@ class _GalleryFooterState extends State<GalleryFooter> {
context: context,
removeTop: true,
child: BottomAppBar(
color: galleryFooterThemeData.backgroundColor,
color:
widget.backgroundColor ?? galleryFooterThemeData.backgroundColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -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: <Widget>[
if (!message.isEphemeral)
IconButton(
@@ -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;
@@ -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<MessageActionsModal> {
}
}
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<MessageActionsModal> {
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),
@@ -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<MessageInput> {
late final TextEditingController textEditingController;
late StreamChatThemeData _streamChatTheme;
late MessageInputThemeData _messageInputTheme;
bool get _hasQuotedMessage => widget.quotedMessage != null;
@@ -351,7 +375,7 @@ class MessageInputState extends State<MessageInput> {
Widget build(BuildContext context) {
Widget child = DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.messageInputTheme.inputBackground,
color: _messageInputTheme.inputBackgroundColor,
),
child: SafeArea(
child: GestureDetector(
@@ -425,11 +449,11 @@ class MessageInputState extends State<MessageInput> {
children: <Widget>[
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),
],
@@ -538,12 +562,12 @@ class MessageInputState extends State<MessageInput> {
: 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),
@@ -563,7 +587,7 @@ class MessageInputState extends State<MessageInput> {
? pi
: 0,
child: StreamSvgIcon.emptyCircleLeft(
color: _streamChatTheme.messageInputTheme.expandButtonColor,
color: _messageInputTheme.expandButtonColor,
),
),
padding: const EdgeInsets.all(0),
@@ -581,12 +605,13 @@ class MessageInputState extends State<MessageInput> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
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)),
),
@@ -609,17 +634,17 @@ class MessageInputState extends State<MessageInput> {
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,
@@ -637,7 +662,7 @@ class MessageInputState extends State<MessageInput> {
keyboardType: widget.keyboardType,
controller: textEditingController,
focusNode: _focusNode,
style: _streamChatTheme.messageInputTheme.inputTextStyle,
style: _messageInputTheme.inputTextStyle,
autofocus: widget.autofocus,
textAlignVertical: TextAlignVertical.center,
decoration: _getInputDecoration(context),
@@ -653,11 +678,11 @@ class MessageInputState extends State<MessageInput> {
}
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(
@@ -722,9 +747,7 @@ class MessageInputState extends State<MessageInput> {
: (widget.actionsLocation == ActionsLocation.leftInside
? Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildExpandActionsButton(),
],
children: [_buildExpandActionsButton(context)],
)
: null),
suffixIconConstraints: const BoxConstraints.tightFor(height: 40),
@@ -750,7 +773,7 @@ class MessageInputState extends State<MessageInput> {
),
if (!_commandEnabled &&
widget.actionsLocation == ActionsLocation.rightInside)
_buildExpandActionsButton(),
_buildExpandActionsButton(context),
if (widget.sendButtonLocation == SendButtonLocation.inside)
_animateSendButton(context),
],
@@ -1734,16 +1757,15 @@ class MessageInputState extends State<MessageInput> {
}
}
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(
@@ -1775,38 +1797,46 @@ class MessageInputState extends State<MessageInput> {
}
},
);
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
@@ -1922,15 +1952,14 @@ class MessageInputState extends State<MessageInput> {
} 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) {
@@ -2017,7 +2046,7 @@ class MessageInputState extends State<MessageInput> {
padding: const EdgeInsets.all(8),
child: StreamSvgIcon(
assetName: _getIdleSendIcon(),
color: _streamChatTheme.messageInputTheme.sendButtonIdleColor,
color: _messageInputTheme.sendButtonIdleColor,
),
);
@@ -2033,7 +2062,7 @@ class MessageInputState extends State<MessageInput> {
),
icon: StreamSvgIcon(
assetName: _getSendIcon(),
color: _streamChatTheme.messageInputTheme.sendButtonColor,
color: _messageInputTheme.sendButtonColor,
),
),
);
@@ -2241,6 +2270,7 @@ class MessageInputState extends State<MessageInput> {
@override
void didChangeDependencies() {
_streamChatTheme = StreamChatTheme.of(context);
_messageInputTheme = MessageInputTheme.of(context);
if (widget.editMessage != null && !_initialized) {
FocusScope.of(context).requestFocus(_focusNode);
_initialized = true;
@@ -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';
@@ -628,7 +629,7 @@ class _MessageListViewState extends State<MessageListView> {
child: Text(
context.translations.threadSeparatorText(replyCount),
textAlign: TextAlign.center,
style: _streamTheme.channelTheme.channelHeaderTheme.subtitle,
style: ChannelHeaderTheme.of(context).subtitleStyle,
),
),
);
@@ -1072,7 +1073,8 @@ class _MessageListViewState extends State<MessageListView> {
}
FocusScope.of(context).unfocus();
},
showPinButton: widget.pinPermissions.contains(currentUserMember?.role),
showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role),
);
if (widget.messageBuilder != null) {
@@ -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));
@@ -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,
@@ -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';
@@ -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,
),
);
},
@@ -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<MessageWidget>
),
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<MessageWidget>
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<MessageWidget>
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<MessageWidget>
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<MessageWidget>
}
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<MessageWidget>
onMentionTap: widget.onMentionTap,
messageTheme: isOnlyEmoji
? widget.messageTheme.copyWith(
messageText:
widget.messageTheme.messageText!.copyWith(
messageTextStyle:
widget.messageTheme.messageTextStyle!.copyWith(
fontSize: 42,
))
: widget.messageTheme,
@@ -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,
)),
),
File diff suppressed because it is too large Load Diff
@@ -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));
}
}
@@ -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<ChannelHeaderTheme>();
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));
}
}
@@ -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<ChannelListHeaderTheme>();
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));
}
}
@@ -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<ChannelListViewTheme>();
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));
}
}
@@ -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<ChannelPreviewTheme>();
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));
}
}
@@ -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,
);
}
@@ -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<GalleryFooterTheme>();
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));
}
}
@@ -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<GalleryHeaderTheme>();
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));
}
}
@@ -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<MessageInputTheme>();
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));
}
}
@@ -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<MessageListViewTheme>();
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));
}
}
@@ -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<MessageSearchListViewTheme>();
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));
}
}
@@ -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));
}
}
@@ -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),
);
}
}
@@ -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';
@@ -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<UserListViewTheme>();
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));
}
}
@@ -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,
)
@@ -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),
),
@@ -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';
@@ -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';
+1 -1
View File
@@ -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
@@ -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',
),
),
];
@@ -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(),
),
),
),
@@ -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',
@@ -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,
),
);
@@ -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),
),
);
@@ -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,
);
@@ -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', () {
@@ -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,
);
@@ -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,
],
),
);
@@ -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 {}
@@ -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', () {
@@ -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,
),
);
@@ -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', () {
+1 -1
View File
@@ -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)
@@ -13,13 +13,10 @@ Future<void> main() async {
await client.connectUser(
User(
id: 'cool-shadow-7',
extraData: const {
'image':
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
},
image:
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9'
'.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''',
);
runApp(
@@ -332,21 +329,9 @@ class _MessageScreenState extends State<MessageScreen> {
}
}
/// 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;
}
}
}
@@ -1,6 +1,10 @@
## Upcoming
* 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.
## 1.0.2
* Updated stream_chat_flutter dependency
* Updated `stream_chat_flutter` dependency
## 1.0.1
@@ -8,4 +12,8 @@
## 1.0.0
* First release
* Initial Release with support for 4 locales
- [English](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart)
- [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart)
- [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart)
- [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart)
+5 -2
View File
@@ -34,6 +34,7 @@ At the moment we support the following languages:
- [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart)
- [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart)
- [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart)
- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart)
More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages.
@@ -68,6 +69,7 @@ class MyApp extends StatelessWidget {
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
// Add GlobalStreamChatLocalizations.delegates
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
@@ -86,13 +88,13 @@ class MyApp extends StatelessWidget {
### Adding a new language
To add a new language, you need to create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it adding it to the `delegates` array.
To add a new language, create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it, adding it to the `delegates` array.
Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/add_new_lang.dart) to see how to add a new language.
### Override existing languages
To override an existing language, you need to create a new class extending that particular language class and create a delegate for it adding it to the `delegates` array.
To override an existing language, create a new class extending that particular language class and create a delegate for it, adding it to the `delegates` array.
Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/override_lang.dart) to see how to override an existing language.
@@ -110,6 +112,7 @@ Example:
<string>nb</string>
<string>fr</string>
<string>it</string>
<string>es</string>
</array>
```
@@ -401,8 +401,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');
@@ -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');
@@ -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');
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
part 'stream_chat_localizations_es.dart';
part 'stream_chat_localizations_en.dart';
part 'stream_chat_localizations_fr.dart';
@@ -24,6 +26,7 @@ const kStreamChatSupportedLanguages = {
'hi',
'fr',
'it',
'es',
};
/// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`.
@@ -54,6 +57,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) {
return const StreamChatLocalizationsFr();
case 'it':
return const StreamChatLocalizationsIt();
case 'es':
return const StreamChatLocalizationsEs();
}
}
@@ -0,0 +1,365 @@
part of 'stream_chat_localizations.dart';
/// The translations for Spanish (`es`).
class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
/// Create an instance of the translation bundle for Spanish.
const StreamChatLocalizationsEs({String localeName = 'es'})
: super(localeName: localeName);
@override
String get launchUrlError => 'No se pudo abrir la url';
@override
String get loadingUsersError => 'Error de carga del usuario';
@override
String get noUsersLabel => 'No hay usuarios actualmente';
@override
String get retryLabel => 'Inténtelo de nuevo';
@override
String get userLastOnlineText => 'Última vez en línea';
@override
String get userOnlineText => 'En línea';
@override
String userTypingText(Iterable<User> users) {
if (users.isEmpty) return '';
final first = users.first;
if (users.length == 1) {
return '${first.name} está escribiendo';
}
return '${first.name} y ${users.length - 1} están escribiendo';
}
@override
String get threadReplyLabel => 'Responder al hilo de discusión';
@override
String get onlyVisibleToYouText => 'Sólo visible para usted';
@override
String threadReplyCountText(int count) =>
'$count respuestas al hilo de discusión';
@override
String attachmentsUploadProgressText({
required int remaining,
required int total,
}) =>
'Transferencia en curso $remaining/$total ...';
@override
String pinnedByUserText({
required User pinnedBy,
required User currentUser,
}) {
final pinnedByCurrentUser = currentUser.id == pinnedBy.id;
if (pinnedByCurrentUser) return 'Fijado por ti';
return 'Fijado por ${pinnedBy.name}';
}
@override
String get emptyMessagesText => 'Actualmente no hay mensajes';
@override
String get genericErrorText => 'Hubo un problema';
@override
String get loadingMessagesError =>
'Hubo un error mientras se cargaba el mensaje';
@override
String resultCountText(int count) => '$count resultados';
@override
String get messageDeletedText => 'Este mensaje ha sido borrado.';
@override
String get messageDeletedLabel => 'Mensaje borrado';
@override
String get messageReactionsLabel => 'Reacciones a los mensajes';
@override
String get emptyChatMessagesText => 'Todavía no hay charlas aquí...';
@override
String threadSeparatorText(int replyCount) {
if (replyCount == 1) return '1 respuesta';
return '$replyCount respuestas';
}
@override
String get connectedLabel => 'Conectado';
@override
String get disconnectedLabel => 'Desconectado';
@override
String get reconnectingLabel => 'Reconectando...';
@override
String get alsoSendAsDirectMessageLabel =>
'Enviar también como mensaje directo';
@override
String get addACommentOrSendLabel => 'Añadir un comentario o enviar';
@override
String get searchGifLabel => 'Búsqueda de GIFs';
@override
String get writeAMessageLabel => 'Escribir un mensaje';
@override
String get instantCommandsLabel => 'Comandos instantáneos';
@override
String fileTooLargeAfterCompressionError(double limitInMB) =>
'El archivo es demasiado grande para descargarlo. '
'El tamaño máximo del archivo es de $limitInMB MB. '
'Intentamos comprimirlo, pero no fue suficiente.';
@override
String fileTooLargeError(double limitInMB) =>
'El archivo es demasiado grande para descargarlo. '
'El límite de tamaño de los archivos es de $limitInMB MB.';
@override
String emojiMatchingQueryText(String query) =>
'Emoji que corresponde a "$query"';
@override
String get addAFileLabel => 'Añadir un archivo';
@override
String get photoFromCameraLabel => 'Foto de la cámara';
@override
String get uploadAFileLabel => 'Transferir un archivo';
@override
String get uploadAPhotoLabel => 'Subir una foto';
@override
String get uploadAVideoLabel => 'Subir una vídeo';
@override
String get videoFromCameraLabel => 'Vídeo de la cámara';
@override
String get okLabel => 'Vale';
@override
String get somethingWentWrongError => 'Algo ha salido mal';
@override
String get addMoreFilesLabel => 'Añadir más archivos';
@override
String get enablePhotoAndVideoAccessMessage =>
'Por favor, permita el acceso a sus fotos'
'\ny vídeos para que puedas compartirlos con sus amigos.';
@override
String get allowGalleryAccessMessage => 'Permitir el acceso a su galería';
@override
String get flagMessageLabel => 'Reportar un mensaje';
@override
String get flagMessageQuestion =>
'¿Quiere enviar una copia de este mensaje a un'
'\nmoderador para una mayor investigación?';
@override
String get flagLabel => 'REPORTAR';
@override
String get cancelLabel => 'CANCELAR';
@override
String get flagMessageSuccessfulLabel => 'Mensaje reportado';
@override
String get flagMessageSuccessfulText =>
'Este mensaje ha sido reportado a un moderador.';
@override
String get deleteLabel => 'BORRAR';
@override
String get deleteMessageLabel => 'Borrar el mensaje';
@override
String get deleteMessageQuestion =>
'¿Estás seguro de que quieres borrar este\nmensaje de forma permanente?';
@override
String get operationCouldNotBeCompletedText =>
'La operación no pudo completarse.';
@override
String get replyLabel => 'Respuesta';
@override
String togglePinUnpinText({required bool pinned}) {
if (pinned) return 'Desfijar a la conversación';
return 'Fijar a la conversación';
}
@override
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) {
if (isDeleteFailed) return 'Reintentar borrar el mensaje';
return 'Borrar el mensaje';
}
@override
String get copyMessageLabel => 'Copiar el mensaje';
@override
String get editMessageLabel => 'Editar el mensaje';
@override
String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) {
if (isUpdateFailed) return 'Reenviar el mensaje modificado';
return 'Reenviar';
}
@override
String get photosLabel => 'Fotos';
String _getDay(DateTime dateTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = DateTime(now.year, now.month, now.day - 1);
final date = DateTime(dateTime.year, dateTime.month, dateTime.day);
if (date == today) {
return 'hoy';
} else if (date == yesterday) {
return 'ayer';
} else {
return 'el ${Jiffy(date).MMMd}';
}
}
@override
String sentAtText({required DateTime date, required DateTime time}) =>
'''Enviado el ${_getDay(date)} a las ${Jiffy(time.toLocal()).format('HH:mm')}''';
@override
String get todayLabel => 'Hoy';
@override
String get yesterdayLabel => 'Ayer';
@override
String get channelIsMutedText => 'El canal está silenciado';
@override
String get noTitleText => 'Sin título';
@override
String get letsStartChattingLabel => '¡Empecemos a charlar!';
@override
String get sendingFirstMessageLabel =>
'¿Qué le parece enviar su primer mensaje a un amigo?';
@override
String get startAChatLabel => 'Iniciar una conversación';
@override
String get loadingChannelsError => 'Error al cargar los canales';
@override
String get deleteConversationLabel => 'Borrar la conversación';
@override
String get deleteConversationQuestion =>
'¿Estás seguro de que quieres borrar esta conversación?';
@override
String get streamChatLabel => 'Stream Chat';
@override
String get searchingForNetworkText => 'Buscando red';
@override
String get offlineLabel => 'Sin conexión...';
@override
String get tryAgainLabel => 'Inténtelo de nuevo';
@override
String membersCountText(int count) {
if (count == 1) return '1 miembro';
return '$count miembros';
}
@override
String watchersCountText(int count) {
if (count == 1) return '1 En línea';
return '$count En línea';
}
@override
String get viewInfoLabel => 'Ver información';
@override
String get leaveGroupLabel => 'Salir del Grupo';
@override
String get leaveLabel => 'SALIR';
@override
String get leaveConversationLabel => 'Salir de la conversación';
@override
String get leaveConversationQuestion =>
'¿Estás seguro de que quiere salir de esta conversación?';
@override
String get showInChatLabel => 'Mostrar en el chat';
@override
String get saveImageLabel => 'Guardar la imagen';
@override
String get saveVideoLabel => 'Guardar el vídeo';
@override
String get uploadErrorLabel => 'ERROR DE TRANSFERENCIA';
@override
String get giphyLabel => 'Giphy';
@override
String get shuffleLabel => 'Mezclar';
@override
String get sendLabel => 'Enviar';
@override
String get withText => 'con';
@override
String get inText => 'en';
@override
String get youText => 'Usted';
@override
String get ofText => 'de';
@override
String get fileText => 'Archivo';
@override
String get replyToMessageLabel => 'Responder al Mensaje';
}
@@ -31,7 +31,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
if (users.length == 1) {
return "${first.name} est en train d'écrire";
}
return "${first.name} and ${users.length - 1} sont entrain d'écrire";
return "${first.name} et ${users.length - 1} sont entrain d'écrire";
}
@override
@@ -49,7 +49,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
required int remaining,
required int total,
}) =>
'Uploading $remaining/$total ...';
'Transfert en cours $remaining/$total ...';
@override
String pinnedByUserText({
@@ -206,8 +206,8 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
@override
String togglePinUnpinText({required bool pinned}) {
if (pinned) return 'tacher de la conversation';
return 'Attacher à la conversation';
if (pinned) return 'crocher de la conversation';
return 'Épingler à la discussion';
}
@override
@@ -311,7 +311,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
String get viewInfoLabel => 'Voir les informations';
@override
String get leaveGroupLabel => 'Quitter le Group';
String get leaveGroupLabel => 'Quitter le Groupe';
@override
String get leaveLabel => 'QUITTER';
@@ -324,7 +324,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
'Etes-vous sûr de vouloir quitter cette conversation ?';
@override
String get showInChatLabel => 'Montrer dans le Chat';
String get showInChatLabel => 'Montrer dans la Discussion';
@override
String get saveImageLabel => "Sauvegarder l'image";
@@ -22,10 +22,8 @@ Future<void> main() async {
await client.connectUser(
User(
id: 'cool-shadow-7',
extraData: const {
'image':
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
},
image:
'https://getstream.io/random_png/?id=cool-shadow-7&amp;name=Cool+shadow',
),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.'
'gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo',