Merge branch 'develop' into localization-ko-and-jp

This commit is contained in:
Salvatore Giordano
2021-08-19 12:19:11 +02:00
committed by GitHub
101 changed files with 4833 additions and 2494 deletions
+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''',
);
File diff suppressed because it is too large Load Diff
@@ -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);
});
});
}
+115 -47
View File
@@ -1,3 +1,59 @@
## Upcoming
✅ Added
- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516):
Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image
is loading
- Added a `backgroundColor` property to the following widgets:
- `ChannelHeader`
- `ChannelListHeader`
- `GalleryHeader`
- `GalleryFooter`
- `ThreadHeader`
- Added `MessageInput.attachmentButtonBuilder` and `MessageInput.commandButtonBuilder` for more
customizations.
```dart
typedef ActionButtonBuilder = Widget Function(
BuildContext context,
IconButton defaultActionButton,
);
```
> **_NOTE:_** The last parameter is the default `ActionButton`
You can call `.copyWith` to customize just a subset of properties.
🔄 Changed
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with
them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming
breakdown:
* `AvatarTheme` is now `AvatarThemeData`
* `ChannelHeaderTheme` is now `ChannelHeaderThemeData`
* `ChannelListHeaderTheme` is now `ChannelListHeaderThemeData`
* `ChannelListViewTheme` is now `ChannelListViewThemeData`
* `ChannelPreviewTheme` is now `ChannelPreviewThemeData`
* `MessageInputTheme` is now `MessageInputThemeData`
* `MessageListViewTheme` is now `MessageListViewTheme`
* `MessageSearchListViewTheme` is now `MessageSearchListViewThemeData`
* `MessageTheme` is now `MessageThemeData`
* `UserListViewTheme` is now `UserListViewThemeData`
🐞 Fixed
- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null.
- Fixed date dividers position/alignment in non reversed `MessageListView`.
## 2.1.2
🐞 Fixed
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no
members when sending message
## 2.1.1
- Updated core dependency
@@ -14,7 +70,8 @@
🔄 Changed
- `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`.
- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`.
- `StreamChat.of(context).userStream` is now deprecated in favor
of `StreamChat.of(context).currentUserStream`.
🐞 Fixed
@@ -28,17 +85,17 @@
- Renamed `ChannelImage` to `ChannelAvatar`
- Updated `StreamChatThemeData.reactionIcons` to accept custom builder
- Renamed `ColorTheme` properties to reflect the purpose of the colors
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
- `ColorTheme.white` -> `ColorTheme.barsBg`
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
- `ColorTheme.white` -> `ColorTheme.barsBg`
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
- `ChannelListCore` options property is removed in favor of individual properties
- `options.state` -> bool state
@@ -59,7 +116,7 @@ typedef MessageBuilder = Widget Function(
);
```
the last parameter is the default `MessageWidget`
> **_NOTE:_** the last parameter is the default `MessageWidget`
You can call `.copyWith` to customize just a subset of properties
@@ -67,7 +124,8 @@ You can call `.copyWith` to customize just a subset of properties
- Added video compress options (frame and quality) to `MessageInput`
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
header/footer
- `MessageWidget` accepts a `userAvatarBuilder`
- Added pinMessage ui support
- Added `MessageListView.threadSeparatorBuilder` property
@@ -76,10 +134,12 @@ You can call `.copyWith` to customize just a subset of properties
🐞 Fixed
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
box when editing message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
a reload
- `MessageListView` not rendering if the user is not a member of the channel
- Fix `MessageInput` overflow when there are no actions
- Minor fixes and improvements
@@ -89,17 +149,17 @@ You can call `.copyWith` to customize just a subset of properties
🛑️ Breaking Changes from `2.0.0-nullsafety.8`
- Renamed `ColorTheme` properties to reflect the purpose of the colors
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
- `ColorTheme.white` -> `ColorTheme.barsBg`
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
- `ColorTheme.black` -> `ColorTheme.textHighEmphasis`
- `ColorTheme.grey` -> `ColorTheme.textLowEmphasis`
- `ColorTheme.greyGainsboro` -> `ColorTheme.disabled`
- `ColorTheme.greyWhisper` -> `ColorTheme.borders`
- `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg`
- `ColorTheme.whiteSnow` -> `ColorTheme.appBg`
- `ColorTheme.white` -> `ColorTheme.barsBg`
- `ColorTheme.blueAlice` -> `ColorTheme.linkBg`
- `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary`
- `ColorTheme.accentRed` -> `ColorTheme.accentError`
- `ColorTheme.accentGreen` -> `ColorTheme.accentInfo`
✅ Added
@@ -126,21 +186,24 @@ typedef MessageBuilder = Widget Function(
);
```
the last parameter is the default `MessageWidget`
You can call `.copyWith` to customize just a subset of properties
> **_NOTE:_** The last parameter is the default `MessageWidget`
You can call `.copyWith` to customize just a subset of properties.
✅ Added
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
header/footer
- `MessageWidget` accepts a `userAvatarBuilder`
🐞 Fixed
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
box when editing message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
use case
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
a reload
- `MessageListView` not rendering if the user is not a member of the channel
## 2.0.0-nullsafety.7
@@ -210,7 +273,8 @@ You can call `.copyWith` to customize just a subset of properties
- Show error messages as system and keep them in the message input
- Remove notification badge logic
- Use shimmer while loading images
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput`
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated
to `MessageInput`
- Add possibility to specify custom message actions using `MessageWidget.customActions`
- Added `MessageListView.onAttachmentTap` callback
- Fixed message newline issue
@@ -267,7 +331,8 @@ You can call `.copyWith` to customize just a subset of properties
- Improved api documentation
- Updated `stream_chat` dependency to `^1.0.0-beta`
- Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples)
- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
- Reimplemented existing widgets
using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
## 0.2.21
@@ -284,8 +349,8 @@ You can call `.copyWith` to customize just a subset of properties
## 0.2.20+2
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message
arrives
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the
list when a new message arrives
## 0.2.20+1
@@ -319,7 +384,8 @@ You can call `.copyWith` to customize just a subset of properties
## 0.2.16
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress
implementation
- Make public autofocus field of the TextField of message_input
## 0.2.15
@@ -504,10 +570,11 @@ You can call `.copyWith` to customize just a subset of properties
## 0.2.1-alpha+1
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget
as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of
your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to
every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have
the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to
your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in
the usual way, but you need to add a `StreamChat` widget to every route of your app.
Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
information.
```dart
@@ -525,6 +592,7 @@ Widget build(BuildContext context) {
},
home: ChannelListPage(),
);
}
```
- Fix reaction bubble going below previous message on iOS
@@ -608,8 +676,8 @@ Widget build(BuildContext context) {
- Add gesture (vertical drag down) to close the keyboard
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the
keyboard)
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will
even close the keyboard)
The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261
@@ -22,8 +22,7 @@ void main() async {
/// https://getstream.io/chat/docs/ios_user_setup_and_tokens/
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.'
'0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
final channel = client.channel('messaging', id: 'godevs');
@@ -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';
@@ -583,11 +584,16 @@ class _ChannelListViewState extends State<ChannelListView> {
),
],
child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: () => widget.onImageTap?.call(channel),
onTap: (channel) => onTap(channel, widget.channelWidget),
DecoratedBox(
decoration: BoxDecoration(
color: chatThemeData.channelListViewTheme.backgroundColor,
),
child: ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: () => widget.onImageTap?.call(channel),
onTap: (channel) => onTap(channel, widget.channelWidget),
),
),
),
);
@@ -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>?>(
@@ -132,7 +132,7 @@ class ChannelPreview extends StatelessWidget {
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: channel.state!.read
?.where((element) =>
.where((element) =>
element.user.id !=
channel
.client.state.currentUser!.id)
@@ -183,14 +183,13 @@ class ChannelPreview extends StatelessWidget {
return Text(
stringDate,
style:
StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
style: ChannelPreviewTheme.of(context).lastMessageAtStyle,
);
},
);
Widget _buildSubtitle(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final channelPreviewTheme = ChannelPreviewTheme.of(context);
if (channel.isMuted) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
@@ -200,7 +199,7 @@ class ChannelPreview extends StatelessWidget {
),
Text(
' ${context.translations.channelIsMutedText}',
style: chatThemeData.channelPreviewTheme.subtitle,
style: channelPreviewTheme.subtitleStyle,
),
],
);
@@ -208,7 +207,7 @@ class ChannelPreview extends StatelessWidget {
return TypingIndicator(
channel: channel,
alternativeWidget: _buildLastMessage(context),
style: chatThemeData.channelPreviewTheme.subtitle,
style: channelPreviewTheme.subtitleStyle,
);
}
@@ -243,19 +242,19 @@ class ChannelPreview extends StatelessWidget {
text = parts.join(' ');
final chatThemeData = StreamChatTheme.of(context);
final channelPreviewTheme = ChannelPreviewTheme.of(context);
return Text.rich(
_getDisplayText(
text,
lastMessage.mentionedUsers,
lastMessage.attachments,
chatThemeData.channelPreviewTheme.subtitle?.copyWith(
color: chatThemeData.channelPreviewTheme.subtitle?.color,
channelPreviewTheme.subtitleStyle?.copyWith(
color: channelPreviewTheme.subtitleStyle?.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal),
chatThemeData.channelPreviewTheme.subtitle?.copyWith(
color: chatThemeData.channelPreviewTheme.subtitle?.color,
channelPreviewTheme.subtitleStyle?.copyWith(
color: channelPreviewTheme.subtitleStyle?.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal,
@@ -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;
@@ -328,7 +352,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(
@@ -402,11 +426,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),
],
@@ -484,12 +508,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),
@@ -509,7 +533,7 @@ class MessageInputState extends State<MessageInput> {
? pi
: 0,
child: StreamSvgIcon.emptyCircleLeft(
color: _streamChatTheme.messageInputTheme.expandButtonColor,
color: _messageInputTheme.expandButtonColor,
),
),
padding: const EdgeInsets.all(0),
@@ -527,12 +551,13 @@ class MessageInputState extends State<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)),
),
@@ -555,17 +580,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,
@@ -583,7 +608,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),
@@ -599,11 +624,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(
@@ -668,9 +693,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),
@@ -696,7 +719,7 @@ class MessageInputState extends State<MessageInput> {
),
if (!_commandEnabled &&
widget.actionsLocation == ActionsLocation.rightInside)
_buildExpandActionsButton(),
_buildExpandActionsButton(context),
if (widget.sendButtonLocation == SendButtonLocation.inside)
_animateSendButton(context),
],
@@ -1680,16 +1703,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(
@@ -1721,38 +1743,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
@@ -1868,15 +1898,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) {
@@ -1963,7 +1992,7 @@ class MessageInputState extends State<MessageInput> {
padding: const EdgeInsets.all(8),
child: StreamSvgIcon(
assetName: _getIdleSendIcon(),
color: _streamChatTheme.messageInputTheme.sendButtonIdleColor,
color: _messageInputTheme.sendButtonIdleColor,
),
);
@@ -1979,7 +2008,7 @@ class MessageInputState extends State<MessageInput> {
),
icon: StreamSvgIcon(
assetName: _getSendIcon(),
color: _streamChatTheme.messageInputTheme.sendButtonColor,
color: _messageInputTheme.sendButtonColor,
),
),
);
@@ -2186,6 +2215,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';
@@ -504,8 +505,14 @@ class _MessageListViewState extends State<MessageListView> {
if (i == 1 || i == itemCount - 4) return const Offstage();
final message = messages[i - 1];
final nextMessage = messages[i - 2];
late final Message message, nextMessage;
if (widget.reverse) {
message = messages[i - 1];
nextMessage = messages[i - 2];
} else {
message = messages[i - 2];
nextMessage = messages[i - 1];
}
if (!Jiffy(message.createdAt.toLocal()).isSame(
nextMessage.createdAt.toLocal(),
Units.DAY,
@@ -628,15 +635,14 @@ class _MessageListViewState extends State<MessageListView> {
child: Text(
context.translations.threadSeparatorText(replyCount),
textAlign: TextAlign.center,
style: _streamTheme.channelTheme.channelHeaderTheme.subtitle,
style: ChannelHeaderTheme.of(context).subtitleStyle,
),
),
);
}
Positioned _buildFloatingDateDivider(int itemCount) => Positioned(
top: widget.reverse ? 20 : null,
bottom: widget.reverse ? null : 20,
top: 20,
left: 0,
right: 0,
child: BetterStreamBuilder<Iterable<ItemPosition>>(
@@ -646,19 +652,36 @@ class _MessageListViewState extends State<MessageListView> {
if (a == null || b == null) {
return false;
}
final aTop = _getTopElementIndex(a);
final bTop = _getTopElementIndex(b);
return aTop == bTop;
if (widget.reverse) {
final aTop = _getTopElementIndex(a);
final bTop = _getTopElementIndex(b);
return aTop == bTop;
} else {
final aBottom = _getBottomElementIndex(a);
final bBottom = _getBottomElementIndex(b);
return aBottom == bBottom;
}
},
builder: (context, values) {
if (values.isEmpty || messages.isEmpty) {
return const Offstage();
}
final index = _getTopElementIndex(values);
int? index;
if (widget.reverse) {
index = _getTopElementIndex(values);
} else {
index = _getBottomElementIndex(values);
}
if (index == null || index <= 2 || index >= itemCount - 3) {
return const Offstage();
if (index == null) return const Offstage();
if (index <= 2 || index >= itemCount - 3) {
if (widget.reverse) {
index = itemCount - 4;
} else {
index = 2;
}
}
final message = messages[index - 2];
@@ -684,6 +707,15 @@ class _MessageListViewState extends State<MessageListView> {
.index;
}
int? _getBottomElementIndex(Iterable<ItemPosition> values) {
final inView = values.where((position) => position.itemLeadingEdge < 1);
if (inView.isEmpty) return null;
return inView
.reduce((min, position) =>
position.itemLeadingEdge < min.itemLeadingEdge ? position : min)
.index;
}
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2(
streamChannel!.channel.state!.isUpToDateStream.distinct(),
@@ -914,7 +946,7 @@ class _MessageListViewState extends State<MessageListView> {
}
final channel = streamChannel!.channel;
final readList = channel.state?.read?.where((read) {
final readList = channel.state?.read.where((read) {
if (read.user.id == userId) return false;
return read.lastRead.isAfter(message.createdAt) ||
read.lastRead.isAtSameMomentAs(message.createdAt);
@@ -965,7 +997,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUser = StreamChat.of(context).currentUser;
final members = StreamChannel.of(context).channel.state?.members ?? [];
final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id);
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
Widget messageWidget = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
@@ -1072,7 +1104,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) {
@@ -1201,8 +1234,7 @@ class _MessageListViewState extends State<MessageListView> {
MaterialPageRoute(
builder: (_) => BetterStreamBuilder<Message>(
stream: streamChannel!.channel.state!.messagesStream.map(
(messages) =>
messages!.firstWhere((m) => m.id == message.id)),
(messages) => messages.firstWhere((m) => m.id == message.id)),
initialData: message,
builder: (_, data) => StreamChannel(
channel: streamChannel!.channel,
@@ -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,6 +21,8 @@ void main() {
when(() => clientState.currentUser).thenReturn(user);
when(() => clientState.currentUserStream)
.thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAtStream)
.thenAnswer((_) => Stream.value(lastMessageAt));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -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,3 +1,8 @@
## Upcoming
🐞 Fixed
- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh
## 2.1.1
- Updated llc dependency
+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;
}
}
}
@@ -106,6 +106,9 @@ class ChannelsBlocState extends State<ChannelsBloc>
final client = _streamChatCoreState!.client;
final clear = paginationParams.offset == 0;
if (clear && _paginationEnded) {
_paginationEnded = false;
}
if ((!clear && _paginationEnded) ||
_queryChannelsLoadingController.value == true) {
@@ -1,9 +1,19 @@
## Upcoming
✅ Added
* Added support for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) locale.
* Added support for [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) locale.
* Added support for [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) locale.
🔄 Changed
* Some of the `Hindi` translations have been updated/changed for better understanding.
- 'रिप्लाई' -> 'जवाब दें'
- 'तस्वीरें' -> 'फ़ोटोज'
- 'बिता हुआ कल' -> 'कल'
- 'चैनल मौन है' -> 'चैनल म्यूट है'
## 1.0.2
* Updated `stream_chat_flutter` dependency
@@ -403,8 +403,7 @@ void main() async {
/// https://getstream.io/chat/docs/ios_user_setup_and_tokens/
await client.connectUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.'
'0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
);
final channel = client.channel('messaging', id: 'godevs');
@@ -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');
@@ -197,7 +197,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
'कार्रवाई पूरी नहीं की जा सकी.';
@override
String get replyLabel => 'रिप्लाई';
String get replyLabel => 'जवाब दें';
@override
String togglePinUnpinText({required bool pinned}) {
@@ -224,7 +224,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
}
@override
String get photosLabel => 'तस्वीरें';
String get photosLabel => 'फ़ोटोज';
String _getDay(DateTime dateTime) {
final now = DateTime.now();
@@ -250,10 +250,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
String get todayLabel => 'आज';
@override
String get yesterdayLabel => 'बिता हुआ कल';
String get yesterdayLabel => 'कल';
@override
String get channelIsMutedText => 'चैनल मौन है';
String get channelIsMutedText => 'चैनल म्यूट है';
@override
String get noTitleText => 'कोई शीर्षक नहीं';
@@ -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',