Merge branch 'develop' of github.com:GetStream/stream-chat-flutter into fix/message-search-pagination
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
## Upcoming
|
||||
## 2.2.1
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fixed unread indicator not updating correctly
|
||||
- Fix `channel.show` not working because of null body
|
||||
|
||||
## 2.2.0
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
@@ -8,7 +15,9 @@
|
||||
|
||||
- `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.
|
||||
- Added slow mode which allows a cooldown period after a user sends a message.
|
||||
## 2.1.1
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
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, {
|
||||
@@ -1247,6 +1252,28 @@ class StreamChatClient {
|
||||
language,
|
||||
);
|
||||
|
||||
/// Enables slow mode
|
||||
Future<PartialUpdateChannelResponse> enableSlowdown(
|
||||
String channelId,
|
||||
String channelType,
|
||||
int cooldown,
|
||||
) async =>
|
||||
_chatApi.channel.enableSlowdown(
|
||||
channelId,
|
||||
channelType,
|
||||
cooldown,
|
||||
);
|
||||
|
||||
/// Disables slow mode
|
||||
Future<PartialUpdateChannelResponse> disableSlowdown(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) async =>
|
||||
_chatApi.channel.disableSlowdown(
|
||||
channelId,
|
||||
channelType,
|
||||
);
|
||||
|
||||
/// Pins provided message
|
||||
/// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds
|
||||
/// to be added to [DateTime.now]
|
||||
|
||||
@@ -123,6 +123,35 @@ class ChannelApi {
|
||||
return PartialUpdateChannelResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Enable slowdown
|
||||
Future<PartialUpdateChannelResponse> enableSlowdown(
|
||||
String channelId,
|
||||
String channelType,
|
||||
int cooldown,
|
||||
) async {
|
||||
final response = await updateChannelPartial(
|
||||
channelId,
|
||||
channelType,
|
||||
set: {
|
||||
'cooldown': cooldown,
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// Disable slowdown
|
||||
Future<PartialUpdateChannelResponse> disableSlowdown(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) async {
|
||||
final response = await updateChannelPartial(
|
||||
channelId,
|
||||
channelType,
|
||||
unset: ['cooldown'],
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// Accept invitation to the channel
|
||||
Future<AcceptInviteResponse> acceptChannelInvite(
|
||||
String channelId,
|
||||
@@ -263,6 +292,7 @@ class ChannelApi {
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'${_getChannelUrl(channelId, channelType)}/show',
|
||||
data: {},
|
||||
);
|
||||
return EmptyResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class ChannelModel {
|
||||
this.memberCount = 0,
|
||||
this.extraData = const {},
|
||||
this.team,
|
||||
this.cooldown = 0,
|
||||
}) : assert(
|
||||
(cid != null && cid.contains(':')) || (id != null && type != null),
|
||||
'provide either a cid or an id and type',
|
||||
@@ -81,6 +82,10 @@ class ChannelModel {
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0)
|
||||
final int memberCount;
|
||||
|
||||
/// The number of seconds in a cooldown
|
||||
@JsonKey(includeIfNull: false, defaultValue: 0)
|
||||
final int cooldown;
|
||||
|
||||
/// Map of custom channel extraData
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
@@ -107,6 +112,7 @@ class ChannelModel {
|
||||
'deleted_at',
|
||||
'member_count',
|
||||
'team',
|
||||
'cooldown',
|
||||
];
|
||||
|
||||
/// Shortcut for channel name
|
||||
@@ -133,6 +139,7 @@ class ChannelModel {
|
||||
int? memberCount,
|
||||
Map<String, Object?>? extraData,
|
||||
String? team,
|
||||
int? cooldown,
|
||||
}) =>
|
||||
ChannelModel(
|
||||
id: id ?? this.id,
|
||||
@@ -148,6 +155,7 @@ class ChannelModel {
|
||||
memberCount: memberCount ?? this.memberCount,
|
||||
extraData: extraData ?? this.extraData,
|
||||
team: team ?? this.team,
|
||||
cooldown: cooldown ?? this.cooldown,
|
||||
);
|
||||
|
||||
/// Returns a new [ChannelModel] that is a combination of this channelModel
|
||||
@@ -168,6 +176,7 @@ class ChannelModel {
|
||||
memberCount: other.memberCount,
|
||||
extraData: other.extraData,
|
||||
team: other.team,
|
||||
cooldown: other.cooldown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) {
|
||||
memberCount: json['member_count'] as int? ?? 0,
|
||||
extraData: json['extra_data'] as Map<String, dynamic>? ?? {},
|
||||
team: json['team'] as String?,
|
||||
cooldown: json['cooldown'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +58,7 @@ Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) {
|
||||
writeNotNull('updated_at', readonly(instance.updatedAt));
|
||||
writeNotNull('deleted_at', readonly(instance.deletedAt));
|
||||
writeNotNull('member_count', readonly(instance.memberCount));
|
||||
val['cooldown'] = instance.cooldown;
|
||||
val['extra_data'] = instance.extraData;
|
||||
writeNotNull('team', readonly(instance.team));
|
||||
return val;
|
||||
|
||||
@@ -184,6 +184,8 @@ class EventChannel extends ChannelModel {
|
||||
DateTime? deletedAt,
|
||||
required int memberCount,
|
||||
Map<String, Object?>? extraData,
|
||||
required int cooldown,
|
||||
String? team,
|
||||
}) : super(
|
||||
id: id,
|
||||
type: type,
|
||||
@@ -197,6 +199,8 @@ class EventChannel extends ChannelModel {
|
||||
deletedAt: deletedAt,
|
||||
memberCount: memberCount,
|
||||
extraData: extraData ?? {},
|
||||
cooldown: cooldown,
|
||||
team: team,
|
||||
);
|
||||
|
||||
/// Create a new instance from a json
|
||||
|
||||
@@ -87,5 +87,7 @@ EventChannel _$EventChannelFromJson(Map<String, dynamic> json) {
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
memberCount: json['member_count'] as int? ?? 0,
|
||||
extraData: json['extra_data'] as Map<String, dynamic>? ?? {},
|
||||
cooldown: json['cooldown'] as int? ?? 0,
|
||||
team: json['team'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
||||
/// Current package version
|
||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||
// ignore: constant_identifier_names
|
||||
const PACKAGE_VERSION = '2.1.1';
|
||||
const PACKAGE_VERSION = '2.2.1';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat
|
||||
homepage: https://getstream.io/
|
||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||
version: 2.1.1
|
||||
version: 2.2.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"id": "dev",
|
||||
"type": "team",
|
||||
"frozen": true,
|
||||
"cooldown": 0,
|
||||
"name": "#dev",
|
||||
"image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png",
|
||||
"example": 1
|
||||
|
||||
+150
@@ -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',
|
||||
@@ -1753,6 +1859,50 @@ void main() {
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('`.enableSlowMode`', () async {
|
||||
const cooldown = 10;
|
||||
|
||||
final channelModel = ChannelModel(
|
||||
cid: channelCid,
|
||||
cooldown: cooldown,
|
||||
);
|
||||
|
||||
when(() => client.enableSlowdown(
|
||||
channelId,
|
||||
channelType,
|
||||
cooldown,
|
||||
)).thenAnswer((_) async => PartialUpdateChannelResponse()
|
||||
..channel = channelModel);
|
||||
|
||||
final res = await channel.enableSlowMode(cooldownInterval: 10);
|
||||
|
||||
expect(res, isNotNull);
|
||||
|
||||
verify(() => client.enableSlowdown(
|
||||
channelId,
|
||||
channelType,
|
||||
cooldown,
|
||||
)).called(1);
|
||||
});
|
||||
|
||||
test('`.disableSlowMode`', () async {
|
||||
final channelModel = ChannelModel(
|
||||
cid: channelCid,
|
||||
);
|
||||
|
||||
when(() => client.disableSlowdown(
|
||||
channelId,
|
||||
channelType,
|
||||
)).thenAnswer((_) async => PartialUpdateChannelResponse()
|
||||
..channel = channelModel);
|
||||
|
||||
final res = await channel.disableSlowMode();
|
||||
|
||||
expect(res, isNotNull);
|
||||
|
||||
verify(() => client.disableSlowdown(channelId, channelType)).called(1);
|
||||
});
|
||||
|
||||
test('`.banUser`', () async {
|
||||
const userId = 'test-user-id';
|
||||
const options = {'key': 'value'};
|
||||
@@ -550,14 +550,21 @@ void main() {
|
||||
|
||||
final path = '${_getChannelUrl(channelId, channelType)}/show';
|
||||
|
||||
when(() => client.post(path)).thenAnswer(
|
||||
(_) async => successResponse(path, data: <String, dynamic>{}));
|
||||
when(() => client.post(
|
||||
path,
|
||||
data: {},
|
||||
))
|
||||
.thenAnswer(
|
||||
(_) async => successResponse(path, data: <String, dynamic>{}));
|
||||
|
||||
final res = await channelApi.showChannel(channelId, channelType);
|
||||
|
||||
expect(res, isNotNull);
|
||||
|
||||
verify(() => client.post(path)).called(1);
|
||||
verify(() => client.post(
|
||||
path,
|
||||
data: {},
|
||||
)).called(1);
|
||||
verifyNoMoreInteractions(client);
|
||||
});
|
||||
|
||||
@@ -605,4 +612,65 @@ void main() {
|
||||
verify(() => client.post(path, data: {})).called(1);
|
||||
verifyNoMoreInteractions(client);
|
||||
});
|
||||
|
||||
test('enableSlowdown', () async {
|
||||
const channelId = 'test-channel-id';
|
||||
const channelType = 'test-channel-type';
|
||||
const cooldown = 10;
|
||||
const set = {
|
||||
'cooldown': 10,
|
||||
};
|
||||
|
||||
final path = _getChannelUrl(channelId, channelType);
|
||||
|
||||
final channelModel = ChannelModel(
|
||||
id: channelId,
|
||||
type: channelType,
|
||||
extraData: set,
|
||||
);
|
||||
|
||||
when(() => client.patch(path, data: {
|
||||
'set': set,
|
||||
})).thenAnswer((_) async => successResponse(path, data: {
|
||||
'channel': channelModel.toJson(),
|
||||
}));
|
||||
|
||||
final res =
|
||||
await channelApi.enableSlowdown(channelId, channelType, cooldown);
|
||||
|
||||
expect(res, isNotNull);
|
||||
|
||||
verify(() => client.patch(path, data: {
|
||||
'set': set,
|
||||
})).called(1);
|
||||
verifyNoMoreInteractions(client);
|
||||
});
|
||||
|
||||
test('disableSlowdown', () async {
|
||||
const channelId = 'test-channel-id';
|
||||
const channelType = 'test-channel-type';
|
||||
const unset = ['cooldown'];
|
||||
|
||||
final path = _getChannelUrl(channelId, channelType);
|
||||
|
||||
final channelModel = ChannelModel(
|
||||
id: channelId,
|
||||
type: channelType,
|
||||
);
|
||||
|
||||
when(() => client.patch(path, data: {
|
||||
'unset': unset,
|
||||
})).thenAnswer((_) async => successResponse(path, data: {
|
||||
'channel': channelModel.toJson(),
|
||||
}));
|
||||
|
||||
final res = await channelApi.disableSlowdown(channelId, channelType);
|
||||
|
||||
expect(res, isNotNull);
|
||||
|
||||
verify(() => client.patch(path, data: {
|
||||
'unset': unset,
|
||||
})).called(1);
|
||||
verifyNoMoreInteractions(client);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ void main() {
|
||||
expect(channel.cid, equals('livestream:test'));
|
||||
expect(channel.extraData['cats'], equals(true));
|
||||
expect(channel.extraData['fruit'], equals(['bananas', 'apples']));
|
||||
expect(channel.cooldown, equals(0));
|
||||
});
|
||||
|
||||
test('should serialize to json correctly', () {
|
||||
@@ -24,7 +25,13 @@ void main() {
|
||||
|
||||
expect(
|
||||
channel.toJson(),
|
||||
{'id': 'id', 'type': 'type', 'frozen': false, 'name': 'cool'},
|
||||
{
|
||||
'id': 'id',
|
||||
'type': 'type',
|
||||
'frozen': false,
|
||||
'cooldown': 0,
|
||||
'name': 'cool',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,7 +45,13 @@ void main() {
|
||||
|
||||
expect(
|
||||
channel.toJson(),
|
||||
{'id': 'id', 'type': 'type', 'name': 'cool', 'frozen': false},
|
||||
{
|
||||
'id': 'id',
|
||||
'type': 'type',
|
||||
'frozen': false,
|
||||
'cooldown': 0,
|
||||
'name': 'cool',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
## Upcoming
|
||||
## 2.2.1
|
||||
|
||||
- Updated `stream_chat_flutter_core` dependency to 2.2.1
|
||||
|
||||
## 2.2.0
|
||||
|
||||
✅ Added
|
||||
|
||||
- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): Added `StreamChatThemeData.placeholderUserImage` for
|
||||
building a widget when the `UserAvatar` image is loading
|
||||
- [#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.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message.
|
||||
- Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded.
|
||||
This will override the default error alert behaviour.
|
||||
- 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.
|
||||
|
||||
- Added slow mode which allows a cooldown period after a user sends a message.
|
||||
|
||||
🔄 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:
|
||||
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`
|
||||
@@ -22,12 +49,23 @@ Here's the full naming breakdown:
|
||||
* `MessageTheme` is now `MessageThemeData`
|
||||
* `UserListViewTheme` is now `UserListViewThemeData`
|
||||
|
||||
- Updated core dependency.
|
||||
|
||||
🐞 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`.
|
||||
- Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set.
|
||||
- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text.
|
||||
|
||||
## 2.1.2
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending message
|
||||
|
||||
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no
|
||||
members when sending message
|
||||
|
||||
## 2.1.1
|
||||
|
||||
- Updated core dependency
|
||||
@@ -44,7 +82,8 @@ Here's the full naming breakdown:
|
||||
🔄 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
|
||||
|
||||
@@ -58,17 +97,17 @@ Here's the full naming breakdown:
|
||||
- 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
|
||||
@@ -89,7 +128,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
|
||||
|
||||
|
||||
@@ -97,7 +136,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
|
||||
@@ -106,10 +146,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
|
||||
@@ -119,17 +161,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
|
||||
|
||||
@@ -156,21 +198,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
|
||||
@@ -240,7 +285,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
|
||||
@@ -297,7 +343,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
|
||||
|
||||
@@ -314,8 +361,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
|
||||
|
||||
@@ -349,7 +396,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
|
||||
@@ -534,10 +582,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
|
||||
@@ -639,8 +688,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
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ class MyApp extends StatelessWidget {
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
Locale('es'),
|
||||
],
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
builder: (context, widget) => StreamChat(
|
||||
@@ -105,7 +106,7 @@ class ChannelPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: MessageListView(),
|
||||
),
|
||||
MessageInput(),
|
||||
MessageInput(attachmentLimit: 3),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -90,57 +90,54 @@ class ChannelAvatar extends StatelessWidget {
|
||||
final colorTheme = chatThemeData.colorTheme;
|
||||
final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme;
|
||||
|
||||
return BetterStreamBuilder<Map<String, dynamic>>(
|
||||
stream: channel.extraDataStream,
|
||||
initialData: channel.extraData,
|
||||
builder: (context, extraData) {
|
||||
final channelImage = extraData['image'];
|
||||
|
||||
if (channelImage != null) {
|
||||
Widget child = ClipRRect(
|
||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ?? previewTheme?.constraints,
|
||||
decoration: BoxDecoration(color: colorTheme.accentPrimary),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: channelImage,
|
||||
errorWidget: (_, __, ___) => Center(
|
||||
child: Text(
|
||||
extraData['name']?[0] ?? '',
|
||||
style: TextStyle(
|
||||
color: colorTheme.barsBg,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
return BetterStreamBuilder<String>(
|
||||
stream: channel.imageStream,
|
||||
initialData: channel.image,
|
||||
builder: (context, channelImage) {
|
||||
Widget child = ClipRRect(
|
||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ?? previewTheme?.constraints,
|
||||
decoration: BoxDecoration(color: colorTheme.accentPrimary),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: channelImage,
|
||||
errorWidget: (_, __, ___) => Center(
|
||||
child: Text(
|
||||
channel.name?[0] ?? '',
|
||||
style: TextStyle(
|
||||
color: colorTheme.barsBg,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
child = ClipRRect(
|
||||
key: const Key('selectedImage'),
|
||||
borderRadius: BorderRadius.circular(selectionThickness) +
|
||||
(borderRadius ??
|
||||
previewTheme?.borderRadius ??
|
||||
BorderRadius.zero),
|
||||
child: Container(
|
||||
constraints: constraints ?? previewTheme?.constraints,
|
||||
color: selectionColor ?? colorTheme.accentPrimary,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
child = ClipRRect(
|
||||
key: const Key('selectedImage'),
|
||||
borderRadius: BorderRadius.circular(selectionThickness) +
|
||||
(borderRadius ??
|
||||
previewTheme?.borderRadius ??
|
||||
BorderRadius.zero),
|
||||
child: Container(
|
||||
constraints: constraints ?? previewTheme?.constraints,
|
||||
color: selectionColor ?? colorTheme.accentPrimary,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
noDataBuilder: (context) {
|
||||
final currentUser = streamChat.currentUser!;
|
||||
final otherMembers = channel.state!.members
|
||||
.where((it) => it.userId != currentUser.id)
|
||||
|
||||
@@ -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,6 +103,9 @@ 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;
|
||||
@@ -141,7 +145,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
leading: leadingWidget,
|
||||
backgroundColor: channelHeaderTheme.color,
|
||||
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
|
||||
actions: actions ??
|
||||
<Widget>[
|
||||
Padding(
|
||||
|
||||
@@ -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;
|
||||
@@ -124,7 +128,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
textTheme: Theme.of(context).textTheme,
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
backgroundColor: channelListHeaderThemeData.color,
|
||||
backgroundColor:
|
||||
backgroundColor ?? channelListHeaderThemeData.color,
|
||||
centerTitle: true,
|
||||
leading: leading ??
|
||||
Center(
|
||||
|
||||
@@ -584,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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -27,40 +27,46 @@ class ChannelName extends StatelessWidget {
|
||||
final client = StreamChat.of(context);
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
return BetterStreamBuilder<Map<String, Object?>>(
|
||||
stream: channel.extraDataStream,
|
||||
initialData: channel.extraData,
|
||||
builder: (context, data) => _buildName(
|
||||
data,
|
||||
channel.state?.members,
|
||||
client,
|
||||
assert(channel.state != null, 'Channel ${channel.id} is not initialized');
|
||||
|
||||
return BetterStreamBuilder<String>(
|
||||
stream: channel.nameStream,
|
||||
initialData: channel.name,
|
||||
builder: (context, channelName) => Text(
|
||||
channelName,
|
||||
style: textStyle,
|
||||
overflow: textOverflow,
|
||||
),
|
||||
noDataBuilder: (context) => _generateName(
|
||||
client.currentUser!,
|
||||
channel.state!.members,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(
|
||||
Map<String, dynamic> extraData,
|
||||
List<Member>? members,
|
||||
StreamChatState client,
|
||||
Widget _generateName(
|
||||
User currentUser,
|
||||
List<Member> members,
|
||||
) =>
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
var title = context.translations.noTitleText;
|
||||
if (extraData['name'] != null) {
|
||||
title = extraData['name'];
|
||||
} else {
|
||||
final otherMembers = members
|
||||
?.where((member) => member.userId != client.currentUser!.id);
|
||||
if (otherMembers?.length == 1) {
|
||||
if (otherMembers!.first.user != null) {
|
||||
title = otherMembers.first.user!.name;
|
||||
var channelName = context.translations.noTitleText;
|
||||
final otherMembers = members.where(
|
||||
(member) => member.userId != currentUser.id,
|
||||
);
|
||||
|
||||
if (otherMembers.isNotEmpty) {
|
||||
if (otherMembers.length == 1) {
|
||||
final user = otherMembers.first.user;
|
||||
if (user != null) {
|
||||
channelName = user.name;
|
||||
}
|
||||
} else if (otherMembers?.isNotEmpty == true) {
|
||||
} else {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxChars = maxWidth / (textStyle?.fontSize ?? 1);
|
||||
var currentChars = 0;
|
||||
final currentMembers = <Member>[];
|
||||
otherMembers!.forEach((element) {
|
||||
otherMembers.forEach((element) {
|
||||
final newLength =
|
||||
currentChars + (element.user?.name.length ?? 0);
|
||||
if (newLength < maxChars) {
|
||||
@@ -71,13 +77,14 @@ class ChannelName extends StatelessWidget {
|
||||
|
||||
final exceedingMembers =
|
||||
otherMembers.length - currentMembers.length;
|
||||
title = '${currentMembers.map((e) => e.user?.name).join(', ')} '
|
||||
channelName =
|
||||
'${currentMembers.map((e) => e.user?.name).join(', ')} '
|
||||
'${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
|
||||
}
|
||||
}
|
||||
|
||||
return Text(
|
||||
title,
|
||||
channelName,
|
||||
style: textStyle,
|
||||
overflow: textOverflow,
|
||||
);
|
||||
|
||||
@@ -94,13 +94,13 @@ class ChannelPreview extends StatelessWidget {
|
||||
textStyle: channelPreviewTheme.titleStyle,
|
||||
),
|
||||
),
|
||||
BetterStreamBuilder<List<Member>?>(
|
||||
BetterStreamBuilder<List<Member>>(
|
||||
stream: channel.state?.membersStream,
|
||||
initialData: channel.state?.members,
|
||||
comparator: const ListEquality().equals,
|
||||
builder: (context, members) {
|
||||
if (members?.isEmpty == true ||
|
||||
members?.any((Member e) =>
|
||||
if (members.isEmpty ||
|
||||
members.any((Member e) =>
|
||||
e.user!.id ==
|
||||
channel.client.state.currentUser?.id) !=
|
||||
true) {
|
||||
@@ -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)
|
||||
@@ -153,13 +153,10 @@ class ChannelPreview extends StatelessWidget {
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime?>(
|
||||
Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime>(
|
||||
stream: channel.lastMessageAtStream,
|
||||
initialData: channel.lastMessageAt,
|
||||
builder: (context, data) {
|
||||
if (data == null) {
|
||||
return const Offstage();
|
||||
}
|
||||
final lastMessageAt = data.toLocal();
|
||||
|
||||
String stringDate;
|
||||
@@ -213,12 +210,12 @@ class ChannelPreview extends StatelessWidget {
|
||||
|
||||
Widget _buildLastMessage(BuildContext context) => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: BetterStreamBuilder<List<Message>?>(
|
||||
child: BetterStreamBuilder<List<Message>>(
|
||||
stream: channel.state!.messagesStream,
|
||||
initialData: channel.state!.messages,
|
||||
builder: (context, data) {
|
||||
final lastMessage = data
|
||||
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
|
||||
final lastMessage =
|
||||
data.lastWhereOrNull((m) => !m.shadowed && !m.isDeleted);
|
||||
if (lastMessage == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class ConnectionStatusBuilder extends StatelessWidget {
|
||||
return BetterStreamBuilder<ConnectionStatus>(
|
||||
initialData: client.wsConnectionStatus,
|
||||
stream: stream,
|
||||
loadingBuilder: loadingBuilder,
|
||||
noDataBuilder: loadingBuilder,
|
||||
errorBuilder: (context, error) {
|
||||
if (errorBuilder != null) {
|
||||
return errorBuilder!(context, error);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,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);
|
||||
|
||||
@@ -55,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();
|
||||
|
||||
@@ -90,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: [
|
||||
@@ -137,9 +142,9 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${widget.currentPage + 1} '
|
||||
'${context.translations.ofText} '
|
||||
'${widget.totalPages}',
|
||||
context.translations.galleryPaginationText(
|
||||
currentPage: widget.currentPage,
|
||||
totalPages: widget.totalPages),
|
||||
style: galleryFooterThemeData.titleTextStyle,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -19,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);
|
||||
|
||||
@@ -50,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);
|
||||
@@ -66,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(
|
||||
|
||||
@@ -100,6 +100,9 @@ abstract class Translations {
|
||||
/// The label for write a message in [MessageInput]
|
||||
String get writeAMessageLabel;
|
||||
|
||||
/// The label for slow mode enabled in [MessageInput]
|
||||
String get slowModeOnLabel;
|
||||
|
||||
/// The label for instant commands in [MessageInput]
|
||||
String get instantCommandsLabel;
|
||||
|
||||
@@ -296,14 +299,19 @@ abstract class Translations {
|
||||
/// The text shown for "You"
|
||||
String get youText;
|
||||
|
||||
/// The text shown for "Of"
|
||||
String get ofText;
|
||||
/// Gallery footer pagination text
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages});
|
||||
|
||||
/// The text shown for "File"
|
||||
String get fileText;
|
||||
|
||||
/// The label for "Reply to message"
|
||||
String get replyToMessageLabel;
|
||||
|
||||
/// Label for "Attachment limit exceeded:
|
||||
/// it's not possible to add more than $limit attachments"
|
||||
String attachmentLimitExceedError(int limit);
|
||||
}
|
||||
|
||||
/// Default implementation of Translation strings for the stream chat widgets
|
||||
@@ -657,11 +665,20 @@ class DefaultTranslations implements Translations {
|
||||
String get youText => 'You';
|
||||
|
||||
@override
|
||||
String get ofText => 'of';
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} of $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'File';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'Reply to Message';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Slow mode ON';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) => """
|
||||
Attachment limit exceeded: it's not possible to add more than $limit attachments""";
|
||||
}
|
||||
|
||||
@@ -37,6 +37,16 @@ typedef ErrorListener = void Function(
|
||||
StackTrace? stackTrace,
|
||||
);
|
||||
|
||||
/// A callback that can be passed to [MessageInput.onAttachmentLimitExceed].
|
||||
///
|
||||
/// This callback should not throw.
|
||||
///
|
||||
/// It exists merely for showing custom error, and should not be used otherwise.
|
||||
typedef AttachmentLimitExceedListener = void Function(
|
||||
int limit,
|
||||
String error,
|
||||
);
|
||||
|
||||
/// Builder for attachment thumbnails
|
||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
@@ -50,6 +60,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,7 +183,15 @@ class MessageInput extends StatefulWidget {
|
||||
this.compressedVideoQuality = VideoQuality.DefaultQuality,
|
||||
this.compressedVideoFrameRate = 30,
|
||||
this.onError,
|
||||
}) : super(key: key);
|
||||
this.attachmentLimit = 10,
|
||||
this.onAttachmentLimitExceed,
|
||||
this.attachmentButtonBuilder,
|
||||
this.commandButtonBuilder,
|
||||
}) : assert(
|
||||
initialMessage == null || editMessage == null,
|
||||
"Can't provide both `initialMessage` and `editMessage`",
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// Message to edit
|
||||
final Message? editMessage;
|
||||
@@ -247,6 +274,26 @@ class MessageInput extends StatefulWidget {
|
||||
/// A callback for error reporting
|
||||
final ErrorListener? onError;
|
||||
|
||||
/// A limit for the no. of attachments that can be sent with a single message.
|
||||
final int attachmentLimit;
|
||||
|
||||
/// A callback for when the [attachmentLimit] is exceeded.
|
||||
///
|
||||
/// This will override the default error alert behaviour.
|
||||
final AttachmentLimitExceedListener? onAttachmentLimitExceed;
|
||||
|
||||
/// Builder for customizing the attachment button.
|
||||
///
|
||||
/// The builder contains the default [IconButton] that can be customized by
|
||||
/// calling `.copyWith`.
|
||||
final ActionButtonBuilder? attachmentButtonBuilder;
|
||||
|
||||
/// Builder for customizing the command button.
|
||||
///
|
||||
/// The builder contains the default [IconButton] that can be customized by
|
||||
/// calling `.copyWith`.
|
||||
final ActionButtonBuilder? commandButtonBuilder;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -270,7 +317,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
final _imagePicker = ImagePicker();
|
||||
late final FocusNode _focusNode;
|
||||
bool _inputEnabled = true;
|
||||
bool _messageIsPresent = false;
|
||||
bool _commandEnabled = false;
|
||||
OverlayEntry? _commandsOverlay, _mentionsOverlay, _emojiOverlay;
|
||||
late Iterable<String> _emojiNames;
|
||||
@@ -280,9 +326,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
bool _sendAsDm = false;
|
||||
bool _openFilePickerSection = false;
|
||||
int _filePickerIndex = 0;
|
||||
double _filePickerSize = _kMinMediaPickerSize;
|
||||
final KeyboardVisibilityController _keyboardVisibilityController =
|
||||
KeyboardVisibilityController();
|
||||
|
||||
final _keyboardVisibilityController = KeyboardVisibilityController();
|
||||
|
||||
/// The editing controller passed to the input TextField
|
||||
late final TextEditingController textEditingController;
|
||||
@@ -292,6 +337,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
bool get _messageIsPresent => textEditingController.text.trim().isNotEmpty;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -325,6 +372,33 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
int _timeOut = 0;
|
||||
Timer? _slowModeTimer;
|
||||
|
||||
void _startSlowMode() {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final cooldownStartedAt = channel.cooldownStartedAt;
|
||||
if (cooldownStartedAt != null) {
|
||||
final diff = DateTime.now().difference(cooldownStartedAt).inSeconds;
|
||||
if (diff < channel.cooldown) {
|
||||
_timeOut = channel.cooldown - diff;
|
||||
if (_timeOut > 0) {
|
||||
_slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_timeOut == 0) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => _timeOut -= 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _stopSlowMode() => _slowModeTimer?.cancel();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = DecoratedBox(
|
||||
@@ -403,11 +477,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),
|
||||
],
|
||||
@@ -473,24 +547,27 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
|
||||
Widget _animateSendButton(BuildContext context) {
|
||||
final sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
return AnimatedCrossFade(
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: sendButton,
|
||||
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
|
||||
duration: _messageInputTheme.sendAnimationDuration!,
|
||||
alignment: Alignment.center,
|
||||
late Widget sendButton;
|
||||
if (_timeOut > 0) {
|
||||
sendButton = _CountdownButton(count: _timeOut);
|
||||
} else if (!_messageIsPresent && _attachments.isEmpty) {
|
||||
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
|
||||
} else {
|
||||
sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||
child: sendButton,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpandActionsButton() {
|
||||
Widget _buildExpandActionsButton(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
@@ -528,12 +605,13 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
if (!widget.disableAttachments) _buildAttachmentButton(),
|
||||
if (!widget.disableAttachments)
|
||||
_buildAttachmentButton(context),
|
||||
if (widget.showCommandsButton &&
|
||||
widget.editMessage == null &&
|
||||
channel.state != null &&
|
||||
channel.config?.commands.isNotEmpty == true)
|
||||
_buildCommandButton(),
|
||||
_buildCommandButton(context),
|
||||
...widget.actions ?? [],
|
||||
].insertBetween(const SizedBox(width: 8)),
|
||||
),
|
||||
@@ -548,7 +626,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
final margin = (widget.sendButtonLocation == SendButtonLocation.inside
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: EdgeInsets.zero) +
|
||||
(widget.actionsLocation != ActionsLocation.left
|
||||
(widget.actionsLocation != ActionsLocation.left || _commandEnabled
|
||||
? const EdgeInsets.only(left: 8)
|
||||
: EdgeInsets.zero);
|
||||
return Expanded(
|
||||
@@ -669,9 +747,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
: (widget.actionsLocation == ActionsLocation.leftInside
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildExpandActionsButton(),
|
||||
],
|
||||
children: [_buildExpandActionsButton(context)],
|
||||
)
|
||||
: null),
|
||||
suffixIconConstraints: const BoxConstraints.tightFor(height: 40),
|
||||
@@ -697,7 +773,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
if (!_commandEnabled &&
|
||||
widget.actionsLocation == ActionsLocation.rightInside)
|
||||
_buildExpandActionsButton(),
|
||||
_buildExpandActionsButton(context),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
@@ -728,7 +804,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
.catchError((e) {});
|
||||
|
||||
setState(() {
|
||||
_messageIsPresent = s.trim().isNotEmpty;
|
||||
_actionsShrunk = s.trim().isNotEmpty &&
|
||||
((widget.actions?.length ?? 0) +
|
||||
(widget.showCommandsButton ? 1 : 0) +
|
||||
@@ -759,6 +834,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (_attachments.isNotEmpty) {
|
||||
return context.translations.addACommentOrSendLabel;
|
||||
}
|
||||
if (_timeOut != 0) {
|
||||
return context.translations.slowModeOnLabel;
|
||||
}
|
||||
|
||||
return context.translations.writeAMessageLabel;
|
||||
}
|
||||
|
||||
@@ -817,7 +896,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (matchedCommandsList.length == 1) {
|
||||
_chosenCommand = matchedCommandsList[0];
|
||||
textEditingController.clear();
|
||||
_messageIsPresent = false;
|
||||
setState(() {
|
||||
_commandEnabled = true;
|
||||
});
|
||||
@@ -964,6 +1042,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
final _attachmentContainsFile =
|
||||
_attachments.values.any((it) => it.type == 'file');
|
||||
|
||||
final attachmentLimitCrossed =
|
||||
_attachments.length >= widget.attachmentLimit;
|
||||
|
||||
Color _getIconColor(int index) {
|
||||
final streamChatThemeData = _streamChatTheme;
|
||||
switch (index) {
|
||||
@@ -983,15 +1064,21 @@ class MessageInputState extends State<MessageInput> {
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2));
|
||||
case 2:
|
||||
return _attachmentContainsFile && _attachments.isNotEmpty
|
||||
return attachmentLimitCrossed
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
: _attachmentContainsFile && _attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
case 3:
|
||||
return _attachmentContainsFile && _attachments.isNotEmpty
|
||||
return attachmentLimitCrossed
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
: _attachmentContainsFile && _attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
@@ -999,7 +1086,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: _openFilePickerSection ? _filePickerSize : 0,
|
||||
height: _openFilePickerSection ? _kMinMediaPickerSize : 0,
|
||||
child: Material(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
@@ -1034,10 +1121,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
icon: StreamSvgIcon.camera(
|
||||
color: _getIconColor(2),
|
||||
),
|
||||
onPressed: _attachmentContainsFile && _attachments.isNotEmpty
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile && _attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.image, true);
|
||||
pickFile(DefaultAttachmentTypes.image, camera: true);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@@ -1045,46 +1133,32 @@ class MessageInputState extends State<MessageInput> {
|
||||
icon: StreamSvgIcon.record(
|
||||
color: _getIconColor(3),
|
||||
),
|
||||
onPressed: _attachmentContainsFile && _attachments.isNotEmpty
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile && _attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.video, true);
|
||||
pickFile(DefaultAttachmentTypes.video, camera: true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GestureDetector(
|
||||
onVerticalDragUpdate: (update) {
|
||||
setState(() {
|
||||
_filePickerSize = (_filePickerSize - update.delta.dy).clamp(
|
||||
_kMinMediaPickerSize,
|
||||
MediaQuery.of(context).size.height / 1.7,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 4,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1107,7 +1181,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (_attachments.containsKey(media.id)) {
|
||||
setState(() => _attachments.remove(media.id));
|
||||
} else {
|
||||
_addAttachment(media);
|
||||
_addAssetAttachment(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1119,15 +1193,13 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
void _addAttachment(AssetEntity medium) async {
|
||||
void _addAssetAttachment(AssetEntity medium) async {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
|
||||
if (mediaFile == null) {
|
||||
return;
|
||||
}
|
||||
if (mediaFile == null) return;
|
||||
|
||||
var file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
@@ -1166,11 +1238,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_attachments[medium.id] = Attachment(
|
||||
final attachment = Attachment(
|
||||
id: medium.id,
|
||||
file: file,
|
||||
type: medium.type == AssetType.image ? 'image' : 'video',
|
||||
);
|
||||
_addAttachments([attachment]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1491,7 +1564,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
setState(() {
|
||||
_chosenCommand = c;
|
||||
_commandEnabled = true;
|
||||
_messageIsPresent = false;
|
||||
});
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
@@ -1681,10 +1753,9 @@ 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
|
||||
@@ -1700,10 +1771,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
setState(() => _openFilePickerSection = false);
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
@@ -1722,38 +1790,43 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return widget.commandButtonBuilder?.call(context, defaultButton) ??
|
||||
defaultButton;
|
||||
}
|
||||
|
||||
Widget _buildAttachmentButton() => 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;
|
||||
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);
|
||||
} else {
|
||||
showAttachmentModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return widget.attachmentButtonBuilder?.call(context, defaultButton) ??
|
||||
defaultButton;
|
||||
}
|
||||
|
||||
/// Show the attachment modal, making the user choose where to
|
||||
/// pick a media from
|
||||
@@ -1768,87 +1841,91 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
} else {
|
||||
showModalBottomSheet(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(32),
|
||||
topRight: Radius.circular(32),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(32),
|
||||
topRight: Radius.circular(32),
|
||||
),
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.translations.addAFileLabel,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: Text(context.translations.uploadAPhotoLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.image);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.video_library),
|
||||
title: Text(context.translations.uploadAVideoLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.video);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (!kIsWeb)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: Text(context.translations.photoFromCameraLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.image, true);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (!kIsWeb)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.videocam),
|
||||
title: Text(context.translations.videoFromCameraLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.video, true);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
title: Text(context.translations.uploadAFileLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
),
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.translations.addAFileLabel,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: Text(context.translations.uploadAPhotoLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.image);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.video_library),
|
||||
title: Text(context.translations.uploadAVideoLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.video);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
title: Text(context.translations.uploadAFileLabel),
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an attachment to the sending message
|
||||
/// Use this to add custom type attachments
|
||||
///
|
||||
/// Note: Only meant to be used from outside the state.
|
||||
void addAttachment(Attachment attachment) {
|
||||
setState(() {
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState,
|
||||
setState(() => _addAttachments([attachment]));
|
||||
}
|
||||
|
||||
/// Adds an attachment to the [_attachments] map
|
||||
void _addAttachments(Iterable<Attachment> attachments) {
|
||||
final limit = widget.attachmentLimit;
|
||||
final length = _attachments.length + attachments.length;
|
||||
if (length > limit) {
|
||||
final onAttachmentLimitExceed = widget.onAttachmentLimitExceed;
|
||||
if (onAttachmentLimitExceed != null) {
|
||||
return onAttachmentLimitExceed(
|
||||
widget.attachmentLimit,
|
||||
context.translations.attachmentLimitExceedError(limit),
|
||||
);
|
||||
}
|
||||
return _showErrorAlert(
|
||||
context.translations.attachmentLimitExceedError(limit),
|
||||
);
|
||||
});
|
||||
}
|
||||
for (final attachment in attachments) {
|
||||
_attachments[attachment.id] = attachment;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick a file from the device
|
||||
/// If [camera] is true then the camera will open
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
||||
void pickFile(
|
||||
DefaultAttachmentTypes fileType, {
|
||||
bool camera = false,
|
||||
}) async {
|
||||
setState(() => _inputEnabled = false);
|
||||
|
||||
AttachmentFile? file;
|
||||
@@ -1869,15 +1946,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) {
|
||||
@@ -1947,16 +2023,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
}
|
||||
|
||||
_attachments[attachment.id] = attachment;
|
||||
|
||||
setState(() {
|
||||
_attachments.update(
|
||||
attachment.id,
|
||||
(it) => it.copyWith(
|
||||
file: file,
|
||||
extraData: {...it.extraData}
|
||||
..update('file_size', ((_) => file!.size!)),
|
||||
));
|
||||
_addAttachments([
|
||||
attachment.copyWith(
|
||||
file: file,
|
||||
extraData: {...attachment.extraData}
|
||||
..update('file_size', ((_) => file!.size!)),
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2023,7 +2097,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
widget.onQuotedMessageCleared?.call();
|
||||
|
||||
setState(() {
|
||||
_messageIsPresent = false;
|
||||
_commandEnabled = false;
|
||||
});
|
||||
|
||||
@@ -2087,6 +2160,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (resp.message?.type == 'error') {
|
||||
_parseExistingMessage(message);
|
||||
}
|
||||
_startSlowMode();
|
||||
widget.onMessageSent?.call(resp.message);
|
||||
} catch (e, stk) {
|
||||
if (widget.onError != null) {
|
||||
@@ -2153,7 +2227,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Text(
|
||||
context.translations.okLabel,
|
||||
style: _streamChatTheme.textTheme.bodyBold.copyWith(
|
||||
color: _streamChatTheme.colorTheme.accentPrimary),
|
||||
color: _streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2164,13 +2239,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
void _parseExistingMessage(Message message) {
|
||||
textEditingController.text = message.text!;
|
||||
_messageIsPresent = true;
|
||||
for (final attachment in message.attachments) {
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState,
|
||||
);
|
||||
}
|
||||
final messageText = message.text;
|
||||
if (messageText != null) textEditingController.text = messageText;
|
||||
_addAttachments(message.attachments);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2179,6 +2250,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
_emojiOverlay?.remove();
|
||||
_mentionsOverlay?.remove();
|
||||
_keyboardListener?.cancel();
|
||||
textEditingController.dispose();
|
||||
_stopSlowMode();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -2188,7 +2261,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
void didChangeDependencies() {
|
||||
_streamChatTheme = StreamChatTheme.of(context);
|
||||
_messageInputTheme = MessageInputTheme.of(context);
|
||||
if (widget.editMessage != null && !_initialized) {
|
||||
if (widget.editMessage == null) _startSlowMode();
|
||||
|
||||
if ((widget.editMessage != null || widget.initialMessage != null) &&
|
||||
!_initialized) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
_initialized = true;
|
||||
}
|
||||
@@ -2196,54 +2272,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 2-tuple, or pair.
|
||||
class Tuple2<T1, T2> {
|
||||
/// Creates a new tuple value with the specified items.
|
||||
const Tuple2(this.item1, this.item2);
|
||||
|
||||
/// Create a new tuple value with the specified list [items].
|
||||
factory Tuple2.fromList(List items) {
|
||||
if (items.length != 2) {
|
||||
throw ArgumentError('items must have length 2');
|
||||
}
|
||||
|
||||
return Tuple2<T1, T2>(items[0] as T1, items[1] as T2);
|
||||
}
|
||||
|
||||
/// Returns the first item of the tuple
|
||||
final T1 item1;
|
||||
|
||||
/// Returns the second item of the tuple
|
||||
final T2 item2;
|
||||
|
||||
/// Returns a tuple with the first item set to the specified value.
|
||||
Tuple2<T1, T2> withItem1(T1 v) => Tuple2<T1, T2>(v, item2);
|
||||
|
||||
/// Returns a tuple with the second item set to the specified value.
|
||||
Tuple2<T1, T2> withItem2(T2 v) => Tuple2<T1, T2>(item1, v);
|
||||
|
||||
/// Creates a [List] containing the items of this [Tuple2].
|
||||
///
|
||||
/// The elements are in item order. The list is variable-length
|
||||
/// if [growable] is true.
|
||||
List toList({bool growable = false}) =>
|
||||
List.from([item1, item2], growable: growable);
|
||||
|
||||
@override
|
||||
String toString() => '[$item1, $item2]';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Tuple2 &&
|
||||
runtimeType == other.runtimeType &&
|
||||
item1 == other.item1 &&
|
||||
item2 == other.item2;
|
||||
|
||||
@override
|
||||
int get hashCode => item1.hashCode ^ item2.hashCode;
|
||||
}
|
||||
|
||||
class _PickerWidget extends StatefulWidget {
|
||||
const _PickerWidget({
|
||||
Key? key,
|
||||
@@ -2281,74 +2309,101 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
||||
return const Offstage();
|
||||
}
|
||||
return FutureBuilder<bool>(
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.data!) {
|
||||
if (widget.containsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
alignment: Alignment.center,
|
||||
if (snapshot.data!) {
|
||||
if (widget.containsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.translations.addMoreFilesLabel,
|
||||
style: TextStyle(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: widget.streamChatTheme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.body.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.textLowEmphasis),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translations.addMoreFilesLabel,
|
||||
style: TextStyle(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: widget.streamChatTheme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.body.copyWith(
|
||||
color:
|
||||
widget.streamChatTheme.colorTheme.textLowEmphasis),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountdownButton extends StatelessWidget {
|
||||
const _CountdownButton({
|
||||
Key? key,
|
||||
required this.count,
|
||||
}) : super(key: key);
|
||||
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Text('$count'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -298,8 +298,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
StreamChannelState? streamChannel;
|
||||
late StreamChatThemeData _streamTheme;
|
||||
|
||||
int? get _initialIndex {
|
||||
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
|
||||
int get _initialIndex {
|
||||
final initialScrollIndex = widget.initialScrollIndex;
|
||||
if (initialScrollIndex != null) return initialScrollIndex;
|
||||
if (streamChannel!.initialMessageId != null) {
|
||||
final messages = streamChannel!.channel.state!.messages;
|
||||
final totalMessages = messages.length;
|
||||
@@ -312,8 +313,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double? get _initialAlignment {
|
||||
if (widget.initialAlignment != null) return widget.initialAlignment;
|
||||
double get _initialAlignment {
|
||||
final initialAlignment = widget.initialAlignment;
|
||||
if (initialAlignment != null) return initialAlignment;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -326,8 +328,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
|
||||
int? initialIndex;
|
||||
double? initialAlignment;
|
||||
int initialIndex = 0;
|
||||
double initialAlignment = 0;
|
||||
|
||||
List<Message> messages = <Message>[];
|
||||
|
||||
@@ -454,10 +456,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.separated(
|
||||
key: ValueKey(initialIndex! + initialAlignment!),
|
||||
key: _upToDate
|
||||
? null
|
||||
: ValueKey(initialIndex + initialAlignment),
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
initialScrollIndex: initialIndex ?? 0,
|
||||
initialAlignment: initialAlignment ?? 0,
|
||||
initialScrollIndex: initialIndex,
|
||||
initialAlignment: initialAlignment,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: widget.reverse,
|
||||
@@ -505,8 +509,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,
|
||||
@@ -636,8 +646,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
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>>(
|
||||
@@ -647,19 +656,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];
|
||||
@@ -685,6 +711,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(),
|
||||
@@ -814,7 +849,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
) {
|
||||
final isMyMessage =
|
||||
message.user!.id == StreamChat.of(context).currentUser!.id;
|
||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
final currentUser = StreamChat.of(context).currentUser;
|
||||
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||
final currentUserMember =
|
||||
@@ -915,7 +950,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);
|
||||
@@ -953,7 +988,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final showInChannelIndicator = !_isThreadConversation && isThreadMessage;
|
||||
final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
|
||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
|
||||
final hasUrlAttachment =
|
||||
message.attachments.any((it) => it.ogScrapeUrl != null) == true;
|
||||
@@ -1163,6 +1198,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
initialIndex = _initialIndex;
|
||||
initialAlignment = _initialAlignment;
|
||||
|
||||
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
||||
_scrollController?.jumpTo(
|
||||
index: initialIndex,
|
||||
alignment: initialAlignment,
|
||||
);
|
||||
});
|
||||
|
||||
_messageNewListener =
|
||||
streamChannel!.channel.on(EventType.messageNew).listen((event) {
|
||||
if (_upToDate) {
|
||||
@@ -1203,8 +1245,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,
|
||||
|
||||
@@ -1048,7 +1048,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
text: widget.message.text!.length > 200
|
||||
text: (widget.message.text?.length ?? 0) > 200
|
||||
? '${widget.message.text!.substring(0, 200)}...'
|
||||
: widget.message.text,
|
||||
),
|
||||
@@ -1111,7 +1111,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
text: widget.message.text!.length > 200
|
||||
text: (widget.message.text?.length ?? 0) > 200
|
||||
? '${widget.message.text!.substring(0, 200)}...'
|
||||
: widget.message.text,
|
||||
),
|
||||
@@ -1258,7 +1258,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
);
|
||||
|
||||
Widget _buildTextBubble() {
|
||||
if (widget.message.text!.trim().isEmpty) return const Offstage();
|
||||
if (widget.message.text?.trim().isEmpty ?? false) return const Offstage();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -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,6 +103,9 @@ 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 channelHeaderTheme = ChannelHeaderTheme.of(context);
|
||||
@@ -136,7 +140,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
showUnreads: true,
|
||||
)
|
||||
: const SizedBox()),
|
||||
backgroundColor: channelHeaderTheme.color,
|
||||
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
|
||||
centerTitle: true,
|
||||
actions: actions,
|
||||
title: InkWell(
|
||||
|
||||
@@ -17,7 +17,7 @@ class UnreadIndicator extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final client = StreamChat.of(context).client;
|
||||
return IgnorePointer(
|
||||
child: BetterStreamBuilder<int?>(
|
||||
child: BetterStreamBuilder<int>(
|
||||
stream: cid != null
|
||||
? client.state.channels[cid]?.state?.unreadCountStream
|
||||
: client.state.totalUnreadCountStream,
|
||||
@@ -25,7 +25,7 @@ class UnreadIndicator extends StatelessWidget {
|
||||
? client.state.channels[cid]?.state?.unreadCount
|
||||
: client.state.totalUnreadCount,
|
||||
builder: (context, data) {
|
||||
if (data == null || data == 0) {
|
||||
if (data == 0) {
|
||||
return const Offstage();
|
||||
}
|
||||
return Material(
|
||||
|
||||
@@ -68,25 +68,34 @@ class UserAvatar extends StatelessWidget {
|
||||
|
||||
Widget avatar = FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme?.constraints,
|
||||
child: hasImage
|
||||
? CachedNetworkImage(
|
||||
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),
|
||||
),
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme?.constraints,
|
||||
child: hasImage
|
||||
? CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.high,
|
||||
imageUrl: user.image!,
|
||||
errorWidget: (context, __, ___) =>
|
||||
streamChatTheme.defaultUserImage(context, user),
|
||||
placeholder: placeholder != null
|
||||
? (context, __) => placeholder(context, user)
|
||||
: null,
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme
|
||||
.ownMessageTheme.avatarTheme?.borderRadius,
|
||||
image: DecorationImage(
|
||||
image: imageProvider, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
|
||||
child: streamChatTheme.defaultUserImage(context, user),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -340,3 +340,51 @@ Widget wrapAttachmentWidget(
|
||||
type: MaterialType.transparency,
|
||||
child: attachmentWidget,
|
||||
);
|
||||
|
||||
/// Represents a 2-tuple, or pair.
|
||||
class Tuple2<T1, T2> {
|
||||
/// Creates a new tuple value with the specified items.
|
||||
const Tuple2(this.item1, this.item2);
|
||||
|
||||
/// Create a new tuple value with the specified list [items].
|
||||
factory Tuple2.fromList(List items) {
|
||||
if (items.length != 2) {
|
||||
throw ArgumentError('items must have length 2');
|
||||
}
|
||||
|
||||
return Tuple2<T1, T2>(items[0] as T1, items[1] as T2);
|
||||
}
|
||||
|
||||
/// Returns the first item of the tuple
|
||||
final T1 item1;
|
||||
|
||||
/// Returns the second item of the tuple
|
||||
final T2 item2;
|
||||
|
||||
/// Returns a tuple with the first item set to the specified value.
|
||||
Tuple2<T1, T2> withItem1(T1 v) => Tuple2<T1, T2>(v, item2);
|
||||
|
||||
/// Returns a tuple with the second item set to the specified value.
|
||||
Tuple2<T1, T2> withItem2(T2 v) => Tuple2<T1, T2>(item1, v);
|
||||
|
||||
/// Creates a [List] containing the items of this [Tuple2].
|
||||
///
|
||||
/// The elements are in item order. The list is variable-length
|
||||
/// if [growable] is true.
|
||||
List toList({bool growable = false}) =>
|
||||
List.from([item1, item2], growable: growable);
|
||||
|
||||
@override
|
||||
String toString() => '[$item1, $item2]';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Tuple2 &&
|
||||
runtimeType == other.runtimeType &&
|
||||
item1 == other.item1 &&
|
||||
item2 == other.item2;
|
||||
|
||||
@override
|
||||
int get hashCode => item1.hashCode ^ item2.hashCode;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.2
|
||||
version: 2.2.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -37,7 +37,7 @@ dependencies:
|
||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
||||
share_plus: ^2.0.3
|
||||
shimmer: ^2.0.0
|
||||
stream_chat_flutter_core: ^2.1.1
|
||||
stream_chat_flutter_core: ^2.2.1
|
||||
substring_highlight: ^1.0.26
|
||||
synchronized: ^3.0.0
|
||||
url_launcher: ^6.0.3
|
||||
|
||||
@@ -26,12 +26,11 @@ void main() {
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
when(() => channelState.unreadCount).thenReturn(1);
|
||||
when(() => client.wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.connected));
|
||||
@@ -91,12 +90,11 @@ void main() {
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
when(() => channelState.unreadCount).thenReturn(1);
|
||||
when(() => channelState.unreadCountStream)
|
||||
.thenAnswer((i) => Stream.value(1));
|
||||
@@ -159,12 +157,11 @@ void main() {
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
when(() => channelState.unreadCount).thenReturn(1);
|
||||
when(() => channelState.unreadCountStream)
|
||||
.thenAnswer((i) => Stream.value(1));
|
||||
@@ -305,12 +302,11 @@ void main() {
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
when(() => channelState.unreadCount).thenReturn(1);
|
||||
when(() => channelState.unreadCountStream)
|
||||
.thenAnswer((i) => Stream.value(1));
|
||||
@@ -373,12 +369,11 @@ void main() {
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
when(() => channelState.unreadCount).thenReturn(1);
|
||||
when(() => channelState.unreadCountStream)
|
||||
.thenAnswer((i) => Stream.value(1));
|
||||
|
||||
@@ -20,14 +20,11 @@ void main() {
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
'image': 'imagetest',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
'image': 'imagetest',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: StreamChat(
|
||||
@@ -43,12 +40,12 @@ void main() {
|
||||
|
||||
final image =
|
||||
tester.widget<CachedNetworkImage>(find.byType(CachedNetworkImage));
|
||||
expect(image.imageUrl, 'imagetest');
|
||||
expect(image.imageUrl, 'https://bit.ly/321RmWb');
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'it should show the the other member image',
|
||||
'it should show the other member image',
|
||||
(tester) async {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
@@ -59,12 +56,10 @@ void main() {
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream).thenAnswer((i) => Stream.value(null));
|
||||
when(() => channel.image).thenReturn(null);
|
||||
when(() => channelState.membersStream).thenAnswer((i) => Stream.value([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
@@ -74,9 +69,7 @@ void main() {
|
||||
userId: 'user-id2',
|
||||
user: User(
|
||||
id: 'user-id2',
|
||||
extraData: const {
|
||||
'image': 'testimage',
|
||||
},
|
||||
image: 'testimage',
|
||||
),
|
||||
)
|
||||
]));
|
||||
@@ -85,9 +78,7 @@ void main() {
|
||||
userId: 'user-id2',
|
||||
user: User(
|
||||
id: 'user-id2',
|
||||
extraData: const {
|
||||
'image': 'testimage',
|
||||
},
|
||||
image: 'testimage',
|
||||
),
|
||||
),
|
||||
Member(
|
||||
@@ -98,9 +89,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({
|
||||
@@ -138,38 +127,29 @@ void main() {
|
||||
when(() => clientState.currentUser).thenReturn(currentUser);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream).thenAnswer((i) => Stream.value(null));
|
||||
final members = [
|
||||
Member(
|
||||
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',
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -210,14 +190,11 @@ void main() {
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
'image': 'imagetest',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
'image': 'imagetest',
|
||||
});
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: StreamChat(
|
||||
|
||||
@@ -21,17 +21,13 @@ void main() {
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false));
|
||||
when(() => channel.nameStream).thenAnswer((_) => Stream.value('test'));
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channelState.unreadCount).thenReturn(1);
|
||||
when(() => channelState.unreadCountStream)
|
||||
.thenAnswer((i) => Stream.value(1));
|
||||
when(() => channelState.membersStream).thenAnswer((i) => Stream.value([
|
||||
when(() => channelState.membersStream).thenAnswer((_) => Stream.value([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
user: User(id: 'user-id'),
|
||||
|
||||
@@ -21,17 +21,19 @@ 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);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test name',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test name',
|
||||
});
|
||||
when(() => channel.nameStream)
|
||||
.thenAnswer((i) => Stream.value('test name'));
|
||||
when(() => channel.name).thenReturn('test name');
|
||||
when(() => channel.imageStream)
|
||||
.thenAnswer((i) => Stream.value('https://bit.ly/321RmWb'));
|
||||
when(() => channel.image).thenReturn('https://bit.ly/321RmWb');
|
||||
when(() => clientState.channels).thenReturn({
|
||||
channel.cid!: channel,
|
||||
});
|
||||
|
||||
@@ -167,7 +167,7 @@ void main() {
|
||||
expect(translations.withText, isNotNull);
|
||||
expect(translations.inText, isNotNull);
|
||||
expect(translations.youText, isNotNull);
|
||||
expect(translations.ofText, isNotNull);
|
||||
expect(translations.galleryPaginationText, isNotNull);
|
||||
expect(translations.fileText, isNotNull);
|
||||
expect(translations.replyToMessageLabel, isNotNull);
|
||||
});
|
||||
|
||||
@@ -69,4 +69,69 @@ void main() {
|
||||
expect(find.byKey(const Key('messageInputText')), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'checks message input slow mode',
|
||||
(WidgetTester tester) async {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
final channel = MockChannel();
|
||||
final channelState = MockChannelState();
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.cooldown).thenReturn(10);
|
||||
when(() => channel.cooldownStartedAt).thenReturn(DateTime.now());
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channelState.membersStream).thenAnswer((i) => Stream.value([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
user: User(id: 'user-id'),
|
||||
)
|
||||
]));
|
||||
when(() => channelState.members).thenReturn([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
user: User(id: 'user-id'),
|
||||
),
|
||||
]);
|
||||
when(() => channelState.messages).thenReturn([
|
||||
Message(
|
||||
text: 'hello',
|
||||
user: User(id: 'other-user'),
|
||||
)
|
||||
]);
|
||||
when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([
|
||||
Message(
|
||||
text: 'hello',
|
||||
user: User(id: 'other-user'),
|
||||
)
|
||||
]));
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: StreamChat(
|
||||
client: client,
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: const Scaffold(
|
||||
body: MessageInput(),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('Slow mode ON'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,12 +22,8 @@ void main() {
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channel.name).thenReturn('test');
|
||||
when(() => channel.nameStream).thenAnswer((i) => Stream.value('test'));
|
||||
when(() => channelState.unreadCount).thenReturn(1);
|
||||
when(() => channelState.unreadCountStream)
|
||||
.thenAnswer((i) => Stream.value(1));
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
## 2.2.1
|
||||
|
||||
- Updated `stream_chat` dependency to 2.2.1
|
||||
|
||||
## 2.2.0
|
||||
|
||||
🛑️ Breaking Changes from `2.1.1`
|
||||
- Renamed `BetterStreamBuilder.loadingBuilder` to `.noDataBuilder`
|
||||
|
||||
🔄 Changed
|
||||
- `BetterStreamBuilder.initialData` is now nullable/not-required.
|
||||
|
||||
🐞 Fixed
|
||||
- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh
|
||||
|
||||
## 2.1.1
|
||||
|
||||
- Updated llc dependency
|
||||
|
||||
@@ -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 [](https://pub.dartlang.org/packages/stream_chat_flutter_core)
|
||||
|
||||
@@ -329,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,23 +6,23 @@ import 'package:flutter/widgets.dart';
|
||||
/// It requires [initialData] and will rebuild
|
||||
/// only when the new data is different than the current data
|
||||
/// The [comparator] is used to check if the new data is different
|
||||
class BetterStreamBuilder<T> extends StatefulWidget {
|
||||
class BetterStreamBuilder<T extends Object> extends StatefulWidget {
|
||||
/// Creates a new BetterStreamBuilder
|
||||
const BetterStreamBuilder({
|
||||
required this.stream,
|
||||
required this.initialData,
|
||||
required this.builder,
|
||||
this.loadingBuilder,
|
||||
this.initialData,
|
||||
this.noDataBuilder,
|
||||
this.errorBuilder,
|
||||
this.comparator,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The stream to listen to
|
||||
final Stream<T>? stream;
|
||||
final Stream<T?>? stream;
|
||||
|
||||
/// The initial data available
|
||||
final T initialData;
|
||||
final T? initialData;
|
||||
|
||||
/// Comparator used to check if the new data is different than the last one
|
||||
final bool Function(T?, T?)? comparator;
|
||||
@@ -31,7 +31,7 @@ class BetterStreamBuilder<T> extends StatefulWidget {
|
||||
final Widget Function(BuildContext context, T data) builder;
|
||||
|
||||
/// Builder that builds when the data is null
|
||||
final Widget Function(BuildContext context)? loadingBuilder;
|
||||
final Widget Function(BuildContext context)? noDataBuilder;
|
||||
|
||||
/// Builder used when there is an error
|
||||
final Widget Function(BuildContext context, Object error)? errorBuilder;
|
||||
@@ -40,21 +40,26 @@ class BetterStreamBuilder<T> extends StatefulWidget {
|
||||
_BetterStreamBuilderState createState() => _BetterStreamBuilderState<T>();
|
||||
}
|
||||
|
||||
class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> {
|
||||
class _BetterStreamBuilderState<T extends Object>
|
||||
extends State<BetterStreamBuilder<T>> {
|
||||
T? _lastEvent;
|
||||
StreamSubscription? _subscription;
|
||||
StreamSubscription<T?>? _subscription;
|
||||
Object? _lastError;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_lastError != null) {
|
||||
return widget.errorBuilder!(context, _lastError!);
|
||||
final error = _lastError;
|
||||
if (error != null) {
|
||||
final errorBuilder = widget.errorBuilder;
|
||||
if (errorBuilder != null) {
|
||||
return errorBuilder(context, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (_lastEvent == null) {
|
||||
return widget.loadingBuilder?.call(context) ?? const Offstage();
|
||||
final event = _lastEvent;
|
||||
if (event == null) {
|
||||
return widget.noDataBuilder?.call(context) ?? const Offstage();
|
||||
}
|
||||
return widget.builder(context, _lastEvent ?? widget.initialData);
|
||||
return widget.builder(context, event);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -87,22 +92,22 @@ class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> {
|
||||
|
||||
void _onError(error) {
|
||||
if (widget.errorBuilder != null && error != _lastError) {
|
||||
_lastError = error;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
_lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
void _onEvent(T event) {
|
||||
void _onEvent(T? event) {
|
||||
_lastError = null;
|
||||
final isEqual =
|
||||
widget.comparator?.call(_lastEvent, event) ?? event == _lastEvent;
|
||||
if (!isEqual) {
|
||||
_lastEvent = event;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
_lastEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
|
||||
import 'package:stream_chat_flutter_core/src/channels_bloc.dart';
|
||||
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
|
||||
import 'package:stream_chat_flutter_core/src/typedef.dart';
|
||||
@@ -137,19 +138,14 @@ class ChannelListCoreState extends State<ChannelListCore> {
|
||||
@override
|
||||
Widget build(BuildContext context) => _buildListView(_channelsBloc);
|
||||
|
||||
StreamBuilder<List<Channel>> _buildListView(
|
||||
BetterStreamBuilder<List<Channel>> _buildListView(
|
||||
ChannelsBlocState channelsBlocState,
|
||||
) =>
|
||||
StreamBuilder<List<Channel>>(
|
||||
BetterStreamBuilder<List<Channel>>(
|
||||
stream: channelsBlocState.channelsStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return widget.errorBuilder(context, snapshot.error!);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return widget.loadingBuilder(context);
|
||||
}
|
||||
final channels = snapshot.data!;
|
||||
errorBuilder: widget.errorBuilder,
|
||||
noDataBuilder: widget.loadingBuilder,
|
||||
builder: (context, channels) {
|
||||
if (channels.isEmpty) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -138,7 +138,7 @@ class MessageListCoreState extends State<MessageListCore> {
|
||||
return true;
|
||||
}
|
||||
|
||||
return BetterStreamBuilder<List<Message>?>(
|
||||
return BetterStreamBuilder<List<Message>>(
|
||||
initialData: initialData,
|
||||
comparator: const ListEquality().equals,
|
||||
stream: messagesStream!.map(
|
||||
@@ -148,9 +148,9 @@ class MessageListCoreState extends State<MessageListCore> {
|
||||
),
|
||||
),
|
||||
errorBuilder: widget.errorBuilder,
|
||||
loadingBuilder: widget.loadingBuilder,
|
||||
noDataBuilder: widget.loadingBuilder,
|
||||
builder: (context, data) {
|
||||
final messageList = data?.reversed.toList(growable: false) ?? [];
|
||||
final messageList = data.reversed.toList(growable: false);
|
||||
if (messageList.isEmpty && !_isThreadConversation) {
|
||||
if (_upToDate) {
|
||||
return widget.emptyBuilder(context);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
|
||||
import 'package:stream_chat_flutter_core/src/message_search_bloc.dart';
|
||||
import 'package:stream_chat_flutter_core/src/typedef.dart';
|
||||
|
||||
@@ -145,16 +146,11 @@ class MessageSearchListCoreState extends State<MessageSearchListCore> {
|
||||
Widget build(BuildContext context) => _buildListView(_messageSearchBloc!);
|
||||
|
||||
Widget _buildListView(MessageSearchBlocState messageSearchBloc) =>
|
||||
StreamBuilder<List<GetMessageResponse>>(
|
||||
BetterStreamBuilder<List<GetMessageResponse>>(
|
||||
stream: messageSearchBloc.messagesStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return widget.errorBuilder(context, snapshot.error!);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return widget.loadingBuilder(context);
|
||||
}
|
||||
final items = snapshot.data!;
|
||||
errorBuilder: widget.errorBuilder,
|
||||
noDataBuilder: widget.loadingBuilder,
|
||||
builder: (context, items) {
|
||||
if (items.isEmpty) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
|
||||
@@ -176,16 +176,11 @@ class UserListCoreState extends State<UserListCore>
|
||||
},
|
||||
);
|
||||
|
||||
StreamBuilder<List<ListItem>> _buildListView() => StreamBuilder(
|
||||
BetterStreamBuilder<List<ListItem>> _buildListView() => BetterStreamBuilder(
|
||||
stream: _buildUserStream(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return widget.errorBuilder(context, snapshot.error!);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return widget.loadingBuilder(context);
|
||||
}
|
||||
final items = snapshot.data!;
|
||||
errorBuilder: widget.errorBuilder,
|
||||
noDataBuilder: widget.loadingBuilder,
|
||||
builder: (context, items) {
|
||||
if (items.isEmpty) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat_flutter_core
|
||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
||||
version: 2.1.1
|
||||
version: 2.2.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -16,7 +16,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
meta: ^1.3.0
|
||||
rxdart: ^0.27.0
|
||||
stream_chat: ^2.1.1
|
||||
stream_chat: ^2.2.1
|
||||
|
||||
dev_dependencies:
|
||||
fake_async: ^1.2.0
|
||||
|
||||
@@ -558,6 +558,7 @@ void main() {
|
||||
config: ChannelConfig(),
|
||||
createdAt: DateTime.now(),
|
||||
memberCount: 1,
|
||||
cooldown: 0,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
## Upcoming
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
* Fixed typos in `Italian` translations.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
✅ 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.
|
||||
* Added translations for cooldown mode.
|
||||
* Added translations for attachmentLimitExceed.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
* Some of the `Hindi` translations have been updated/changed for better understanding.
|
||||
- 'रिप्लाई' -> 'जवाब दें'
|
||||
- 'तस्वीरें' -> 'फ़ोटोज'
|
||||
- 'बिता हुआ कल' -> 'कल'
|
||||
- 'चैनल मौन है' -> 'चैनल म्यूट है'
|
||||
|
||||
## 1.0.2
|
||||
|
||||
* Updated stream_chat_flutter dependency
|
||||
* Updated `stream_chat_flutter` dependency
|
||||
|
||||
## 1.0.1
|
||||
|
||||
@@ -8,4 +32,8 @@
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* First release
|
||||
* Initial Release with support for 4 locales
|
||||
- [English](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart)
|
||||
- [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart)
|
||||
- [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart)
|
||||
- [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart)
|
||||
|
||||
@@ -34,6 +34,9 @@ At the moment we support the following languages:
|
||||
- [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart)
|
||||
- [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart)
|
||||
- [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart)
|
||||
- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart)
|
||||
- [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart)
|
||||
- [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart)
|
||||
|
||||
More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages.
|
||||
|
||||
@@ -68,6 +71,9 @@ class MyApp extends StatelessWidget {
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
Locale('es'),
|
||||
Locale('ja'),
|
||||
Locale('ko'),
|
||||
],
|
||||
// Add GlobalStreamChatLocalizations.delegates
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
@@ -86,13 +92,13 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
### Adding a new language
|
||||
|
||||
To add a new language, you need to create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it adding it to the `delegates` array.
|
||||
To add a new language, create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it, adding it to the `delegates` array.
|
||||
|
||||
Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/add_new_lang.dart) to see how to add a new language.
|
||||
|
||||
### Override existing languages
|
||||
|
||||
To override an existing language, you need to create a new class extending that particular language class and create a delegate for it adding it to the `delegates` array.
|
||||
To override an existing language, create a new class extending that particular language class and create a delegate for it, adding it to the `delegates` array.
|
||||
|
||||
Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/override_lang.dart) to see how to override an existing language.
|
||||
|
||||
@@ -110,6 +116,9 @@ Example:
|
||||
<string>nb</string>
|
||||
<string>fr</string>
|
||||
<string>it</string>
|
||||
<string>es</string>
|
||||
<string>ja</string>
|
||||
<string>ko</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
|
||||
@@ -374,13 +374,22 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
|
||||
String get youText => 'You';
|
||||
|
||||
@override
|
||||
String get ofText => 'of';
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'$currentPage of $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'File';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'Reply to Message';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) =>
|
||||
'Attachment limit exceeded, limit: $limit';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Slow mode ON';
|
||||
}
|
||||
|
||||
void main() async {
|
||||
@@ -453,6 +462,9 @@ class MyApp extends StatelessWidget {
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
Locale('es'),
|
||||
Locale('ja'),
|
||||
Locale('ko'),
|
||||
// Add support for additional 'nn' locale
|
||||
Locale('nn'),
|
||||
],
|
||||
|
||||
@@ -72,6 +72,9 @@ class MyApp extends StatelessWidget {
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
Locale('es'),
|
||||
Locale('ja'),
|
||||
Locale('ko'),
|
||||
],
|
||||
// Add GlobalStreamChatLocalizations.delegates
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
|
||||
@@ -97,6 +97,9 @@ class MyApp extends StatelessWidget {
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
Locale('es'),
|
||||
Locale('ja'),
|
||||
Locale('ko'),
|
||||
],
|
||||
// Add overridden "CustomStreamChatLocalizationsEn.delegate" along with
|
||||
// "GlobalStreamChatLocalizations.delegates"
|
||||
|
||||
@@ -3,12 +3,18 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
part 'stream_chat_localizations_es.dart';
|
||||
|
||||
part 'stream_chat_localizations_en.dart';
|
||||
|
||||
part 'stream_chat_localizations_fr.dart';
|
||||
|
||||
part 'stream_chat_localizations_it.dart';
|
||||
|
||||
part 'stream_chat_localizations_ja.dart';
|
||||
|
||||
part 'stream_chat_localizations_ko.dart';
|
||||
|
||||
part 'stream_chat_localizations_hi.dart';
|
||||
|
||||
/// The set of supported languages, as language code strings.
|
||||
@@ -24,6 +30,9 @@ const kStreamChatSupportedLanguages = {
|
||||
'hi',
|
||||
'fr',
|
||||
'it',
|
||||
'es',
|
||||
'ja',
|
||||
'ko'
|
||||
};
|
||||
|
||||
/// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`.
|
||||
@@ -54,6 +63,12 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) {
|
||||
return const StreamChatLocalizationsFr();
|
||||
case 'it':
|
||||
return const StreamChatLocalizationsIt();
|
||||
case 'es':
|
||||
return const StreamChatLocalizationsEs();
|
||||
case 'ja':
|
||||
return const StreamChatLocalizationsJa();
|
||||
case 'ko':
|
||||
return const StreamChatLocalizationsKo();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -350,11 +350,20 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations {
|
||||
String get youText => 'You';
|
||||
|
||||
@override
|
||||
String get ofText => 'of';
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} of $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'File';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'Reply to Message';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) =>
|
||||
'Attachment limit exceeded, limit: $limit';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Slow mode ON';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
part of 'stream_chat_localizations.dart';
|
||||
|
||||
/// The translations for Spanish (`es`).
|
||||
class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
|
||||
/// Create an instance of the translation bundle for Spanish.
|
||||
const StreamChatLocalizationsEs({String localeName = 'es'})
|
||||
: super(localeName: localeName);
|
||||
|
||||
@override
|
||||
String get launchUrlError => 'No se pudo abrir la url';
|
||||
|
||||
@override
|
||||
String get loadingUsersError => 'Error de carga del usuario';
|
||||
|
||||
@override
|
||||
String get noUsersLabel => 'No hay usuarios actualmente';
|
||||
|
||||
@override
|
||||
String get retryLabel => 'Inténtelo de nuevo';
|
||||
|
||||
@override
|
||||
String get userLastOnlineText => 'Última vez en línea';
|
||||
|
||||
@override
|
||||
String get userOnlineText => 'En línea';
|
||||
|
||||
@override
|
||||
String userTypingText(Iterable<User> users) {
|
||||
if (users.isEmpty) return '';
|
||||
final first = users.first;
|
||||
if (users.length == 1) {
|
||||
return '${first.name} está escribiendo';
|
||||
}
|
||||
return '${first.name} y ${users.length - 1} están escribiendo';
|
||||
}
|
||||
|
||||
@override
|
||||
String get threadReplyLabel => 'Responder al hilo de discusión';
|
||||
|
||||
@override
|
||||
String get onlyVisibleToYouText => 'Sólo visible para usted';
|
||||
|
||||
@override
|
||||
String threadReplyCountText(int count) =>
|
||||
'$count respuestas al hilo de discusión';
|
||||
|
||||
@override
|
||||
String attachmentsUploadProgressText({
|
||||
required int remaining,
|
||||
required int total,
|
||||
}) =>
|
||||
'Transferencia en curso $remaining/$total ...';
|
||||
|
||||
@override
|
||||
String pinnedByUserText({
|
||||
required User pinnedBy,
|
||||
required User currentUser,
|
||||
}) {
|
||||
final pinnedByCurrentUser = currentUser.id == pinnedBy.id;
|
||||
if (pinnedByCurrentUser) return 'Fijado por ti';
|
||||
return 'Fijado por ${pinnedBy.name}';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emptyMessagesText => 'Actualmente no hay mensajes';
|
||||
|
||||
@override
|
||||
String get genericErrorText => 'Hubo un problema';
|
||||
|
||||
@override
|
||||
String get loadingMessagesError =>
|
||||
'Hubo un error mientras se cargaba el mensaje';
|
||||
|
||||
@override
|
||||
String resultCountText(int count) => '$count resultados';
|
||||
|
||||
@override
|
||||
String get messageDeletedText => 'Este mensaje ha sido borrado.';
|
||||
|
||||
@override
|
||||
String get messageDeletedLabel => 'Mensaje borrado';
|
||||
|
||||
@override
|
||||
String get messageReactionsLabel => 'Reacciones a los mensajes';
|
||||
|
||||
@override
|
||||
String get emptyChatMessagesText => 'Todavía no hay charlas aquí...';
|
||||
|
||||
@override
|
||||
String threadSeparatorText(int replyCount) {
|
||||
if (replyCount == 1) return '1 respuesta';
|
||||
return '$replyCount respuestas';
|
||||
}
|
||||
|
||||
@override
|
||||
String get connectedLabel => 'Conectado';
|
||||
|
||||
@override
|
||||
String get disconnectedLabel => 'Desconectado';
|
||||
|
||||
@override
|
||||
String get reconnectingLabel => 'Reconectando...';
|
||||
|
||||
@override
|
||||
String get alsoSendAsDirectMessageLabel =>
|
||||
'Enviar también como mensaje directo';
|
||||
|
||||
@override
|
||||
String get addACommentOrSendLabel => 'Añadir un comentario o enviar';
|
||||
|
||||
@override
|
||||
String get searchGifLabel => 'Búsqueda de GIFs';
|
||||
|
||||
@override
|
||||
String get writeAMessageLabel => 'Escribir un mensaje';
|
||||
|
||||
@override
|
||||
String get instantCommandsLabel => 'Comandos instantáneos';
|
||||
|
||||
@override
|
||||
String fileTooLargeAfterCompressionError(double limitInMB) =>
|
||||
'El archivo es demasiado grande para descargarlo. '
|
||||
'El tamaño máximo del archivo es de $limitInMB MB. '
|
||||
'Intentamos comprimirlo, pero no fue suficiente.';
|
||||
|
||||
@override
|
||||
String fileTooLargeError(double limitInMB) =>
|
||||
'El archivo es demasiado grande para descargarlo. '
|
||||
'El límite de tamaño de los archivos es de $limitInMB MB.';
|
||||
|
||||
@override
|
||||
String emojiMatchingQueryText(String query) =>
|
||||
'Emoji que corresponde a "$query"';
|
||||
|
||||
@override
|
||||
String get addAFileLabel => 'Añadir un archivo';
|
||||
|
||||
@override
|
||||
String get photoFromCameraLabel => 'Foto de la cámara';
|
||||
|
||||
@override
|
||||
String get uploadAFileLabel => 'Transferir un archivo';
|
||||
|
||||
@override
|
||||
String get uploadAPhotoLabel => 'Subir una foto';
|
||||
|
||||
@override
|
||||
String get uploadAVideoLabel => 'Subir una vídeo';
|
||||
|
||||
@override
|
||||
String get videoFromCameraLabel => 'Vídeo de la cámara';
|
||||
|
||||
@override
|
||||
String get okLabel => 'Vale';
|
||||
|
||||
@override
|
||||
String get somethingWentWrongError => 'Algo ha salido mal';
|
||||
|
||||
@override
|
||||
String get addMoreFilesLabel => 'Añadir más archivos';
|
||||
|
||||
@override
|
||||
String get enablePhotoAndVideoAccessMessage =>
|
||||
'Por favor, permita el acceso a sus fotos'
|
||||
'\ny vídeos para que puedas compartirlos con sus amigos.';
|
||||
|
||||
@override
|
||||
String get allowGalleryAccessMessage => 'Permitir el acceso a su galería';
|
||||
|
||||
@override
|
||||
String get flagMessageLabel => 'Reportar un mensaje';
|
||||
|
||||
@override
|
||||
String get flagMessageQuestion =>
|
||||
'¿Quiere enviar una copia de este mensaje a un'
|
||||
'\nmoderador para una mayor investigación?';
|
||||
|
||||
@override
|
||||
String get flagLabel => 'REPORTAR';
|
||||
|
||||
@override
|
||||
String get cancelLabel => 'CANCELAR';
|
||||
|
||||
@override
|
||||
String get flagMessageSuccessfulLabel => 'Mensaje reportado';
|
||||
|
||||
@override
|
||||
String get flagMessageSuccessfulText =>
|
||||
'Este mensaje ha sido reportado a un moderador.';
|
||||
|
||||
@override
|
||||
String get deleteLabel => 'BORRAR';
|
||||
|
||||
@override
|
||||
String get deleteMessageLabel => 'Borrar el mensaje';
|
||||
|
||||
@override
|
||||
String get deleteMessageQuestion =>
|
||||
'¿Estás seguro de que quieres borrar este\nmensaje de forma permanente?';
|
||||
|
||||
@override
|
||||
String get operationCouldNotBeCompletedText =>
|
||||
'La operación no pudo completarse.';
|
||||
|
||||
@override
|
||||
String get replyLabel => 'Respuesta';
|
||||
|
||||
@override
|
||||
String togglePinUnpinText({required bool pinned}) {
|
||||
if (pinned) return 'Desfijar a la conversación';
|
||||
return 'Fijar a la conversación';
|
||||
}
|
||||
|
||||
@override
|
||||
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) {
|
||||
if (isDeleteFailed) return 'Reintentar borrar el mensaje';
|
||||
return 'Borrar el mensaje';
|
||||
}
|
||||
|
||||
@override
|
||||
String get copyMessageLabel => 'Copiar el mensaje';
|
||||
|
||||
@override
|
||||
String get editMessageLabel => 'Editar el mensaje';
|
||||
|
||||
@override
|
||||
String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) {
|
||||
if (isUpdateFailed) return 'Reenviar el mensaje modificado';
|
||||
return 'Reenviar';
|
||||
}
|
||||
|
||||
@override
|
||||
String get photosLabel => 'Fotos';
|
||||
|
||||
String _getDay(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final yesterday = DateTime(now.year, now.month, now.day - 1);
|
||||
|
||||
final date = DateTime(dateTime.year, dateTime.month, dateTime.day);
|
||||
|
||||
if (date == today) {
|
||||
return 'hoy';
|
||||
} else if (date == yesterday) {
|
||||
return 'ayer';
|
||||
} else {
|
||||
return 'el ${Jiffy(date).MMMd}';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
||||
'''Enviado el ${_getDay(date)} a las ${Jiffy(time.toLocal()).format('HH:mm')}''';
|
||||
|
||||
@override
|
||||
String get todayLabel => 'Hoy';
|
||||
|
||||
@override
|
||||
String get yesterdayLabel => 'Ayer';
|
||||
|
||||
@override
|
||||
String get channelIsMutedText => 'El canal está silenciado';
|
||||
|
||||
@override
|
||||
String get noTitleText => 'Sin título';
|
||||
|
||||
@override
|
||||
String get letsStartChattingLabel => '¡Empecemos a charlar!';
|
||||
|
||||
@override
|
||||
String get sendingFirstMessageLabel =>
|
||||
'¿Qué le parece enviar su primer mensaje a un amigo?';
|
||||
|
||||
@override
|
||||
String get startAChatLabel => 'Iniciar una conversación';
|
||||
|
||||
@override
|
||||
String get loadingChannelsError => 'Error al cargar los canales';
|
||||
|
||||
@override
|
||||
String get deleteConversationLabel => 'Borrar la conversación';
|
||||
|
||||
@override
|
||||
String get deleteConversationQuestion =>
|
||||
'¿Estás seguro de que quieres borrar esta conversación?';
|
||||
|
||||
@override
|
||||
String get streamChatLabel => 'Stream Chat';
|
||||
|
||||
@override
|
||||
String get searchingForNetworkText => 'Buscando red';
|
||||
|
||||
@override
|
||||
String get offlineLabel => 'Sin conexión...';
|
||||
|
||||
@override
|
||||
String get tryAgainLabel => 'Inténtelo de nuevo';
|
||||
|
||||
@override
|
||||
String membersCountText(int count) {
|
||||
if (count == 1) return '1 miembro';
|
||||
return '$count miembros';
|
||||
}
|
||||
|
||||
@override
|
||||
String watchersCountText(int count) {
|
||||
if (count == 1) return '1 En línea';
|
||||
return '$count En línea';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewInfoLabel => 'Ver información';
|
||||
|
||||
@override
|
||||
String get leaveGroupLabel => 'Salir del Grupo';
|
||||
|
||||
@override
|
||||
String get leaveLabel => 'SALIR';
|
||||
|
||||
@override
|
||||
String get leaveConversationLabel => 'Salir de la conversación';
|
||||
|
||||
@override
|
||||
String get leaveConversationQuestion =>
|
||||
'¿Estás seguro de que quiere salir de esta conversación?';
|
||||
|
||||
@override
|
||||
String get showInChatLabel => 'Mostrar en el chat';
|
||||
|
||||
@override
|
||||
String get saveImageLabel => 'Guardar la imagen';
|
||||
|
||||
@override
|
||||
String get saveVideoLabel => 'Guardar el vídeo';
|
||||
|
||||
@override
|
||||
String get uploadErrorLabel => 'ERROR DE TRANSFERENCIA';
|
||||
|
||||
@override
|
||||
String get giphyLabel => 'Giphy';
|
||||
|
||||
@override
|
||||
String get shuffleLabel => 'Mezclar';
|
||||
|
||||
@override
|
||||
String get sendLabel => 'Enviar';
|
||||
|
||||
@override
|
||||
String get withText => 'con';
|
||||
|
||||
@override
|
||||
String get inText => 'en';
|
||||
|
||||
@override
|
||||
String get youText => 'Usted';
|
||||
|
||||
@override
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} de $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'Archivo';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'Responder al Mensaje';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) => '''
|
||||
No es posible añadir más de $limit archivos adjuntos
|
||||
''';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Modo lento activado';
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
||||
if (users.length == 1) {
|
||||
return "${first.name} est en train d'écrire";
|
||||
}
|
||||
return "${first.name} and ${users.length - 1} sont entrain d'écrire";
|
||||
return "${first.name} et ${users.length - 1} sont entrain d'écrire";
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -49,7 +49,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
||||
required int remaining,
|
||||
required int total,
|
||||
}) =>
|
||||
'Uploading $remaining/$total ...';
|
||||
'Transfert en cours $remaining/$total ...';
|
||||
|
||||
@override
|
||||
String pinnedByUserText({
|
||||
@@ -206,8 +206,8 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
||||
|
||||
@override
|
||||
String togglePinUnpinText({required bool pinned}) {
|
||||
if (pinned) return 'Détacher de la conversation';
|
||||
return 'Attacher à la conversation';
|
||||
if (pinned) return 'Décrocher de la conversation';
|
||||
return 'Épingler à la conversation';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -311,7 +311,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
||||
String get viewInfoLabel => 'Voir les informations';
|
||||
|
||||
@override
|
||||
String get leaveGroupLabel => 'Quitter le Group';
|
||||
String get leaveGroupLabel => 'Quitter le Groupe';
|
||||
|
||||
@override
|
||||
String get leaveLabel => 'QUITTER';
|
||||
@@ -324,7 +324,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
||||
'Etes-vous sûr de vouloir quitter cette conversation ?';
|
||||
|
||||
@override
|
||||
String get showInChatLabel => 'Montrer dans le Chat';
|
||||
String get showInChatLabel => 'Montrer dans la Discussion';
|
||||
|
||||
@override
|
||||
String get saveImageLabel => "Sauvegarder l'image";
|
||||
@@ -354,11 +354,21 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
||||
String get youText => 'Vous';
|
||||
|
||||
@override
|
||||
String get ofText => 'de';
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} de $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'Fichier';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'Répondre au Message';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) => '''
|
||||
Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $limit pièces jointes
|
||||
''';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Mode lent activé';
|
||||
}
|
||||
|
||||
@@ -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 => 'कोई शीर्षक नहीं';
|
||||
@@ -340,20 +340,30 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
|
||||
String get sendLabel => 'भेजें';
|
||||
|
||||
@override
|
||||
String get withText => 'विद';
|
||||
String get withText => 'विद'; //TODO: break?
|
||||
|
||||
@override
|
||||
String get inText => 'इन';
|
||||
String get inText => 'इन'; //TODO: break?
|
||||
|
||||
@override
|
||||
String get youText => 'आप';
|
||||
|
||||
@override
|
||||
String get ofText => 'ऑफ़';
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} ऑफ़ $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'फ़ाइल';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'संदेश का जवाब';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) => '''
|
||||
अटैचमेंट लिमिट: $limit अटैचमेंट से अधिक जोड़ना संभव नहीं है
|
||||
''';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'स्लो मोड चालू';
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
|
||||
String get yesterdayLabel => 'Ieri';
|
||||
|
||||
@override
|
||||
String get channelIsMutedText => 'Il canale è mutato';
|
||||
String get channelIsMutedText => 'Il canale è silenziato';
|
||||
|
||||
@override
|
||||
String get noTitleText => 'Nessun titolo';
|
||||
@@ -274,7 +274,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
|
||||
String get loadingChannelsError => 'Errore durante il caricamento dei canali';
|
||||
|
||||
@override
|
||||
String get deleteConversationLabel => 'Elemina conversazione';
|
||||
String get deleteConversationLabel => 'Elimina conversazione';
|
||||
|
||||
@override
|
||||
String get deleteConversationQuestion =>
|
||||
@@ -351,11 +351,21 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
|
||||
String get youText => 'te';
|
||||
|
||||
@override
|
||||
String get ofText => 'di';
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} di $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'file';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'Rispondi al messaggio';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) => '''
|
||||
Attenzione: il limite massimo di $limit file è stato superato.
|
||||
''';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Slowmode attiva';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
part of 'stream_chat_localizations.dart';
|
||||
|
||||
/// The translations for Japanese (`ja`).
|
||||
class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations {
|
||||
/// Create an instance of the translation bundle for Japanese.
|
||||
const StreamChatLocalizationsJa({String localeName = 'ja'})
|
||||
: super(localeName: localeName);
|
||||
|
||||
@override
|
||||
String get launchUrlError => 'URLの起動ができません';
|
||||
|
||||
@override
|
||||
String get loadingUsersError => 'ユーザーの読み込みができません';
|
||||
|
||||
@override
|
||||
String get noUsersLabel => '現在、ユーザーはいません。';
|
||||
|
||||
@override
|
||||
String get retryLabel => '再試行';
|
||||
|
||||
@override
|
||||
String get userLastOnlineText => '前回のオンライン';
|
||||
|
||||
@override
|
||||
String get userOnlineText => 'オンライン';
|
||||
|
||||
@override
|
||||
String userTypingText(Iterable<User> users) {
|
||||
if (users.isEmpty) return '';
|
||||
final first = users.first;
|
||||
if (users.length == 1) {
|
||||
return '${first.name}が入力しています';
|
||||
}
|
||||
return '${first.name}と${users.length - 1}人が入力しています';
|
||||
}
|
||||
|
||||
@override
|
||||
String get threadReplyLabel => 'スレッド返信';
|
||||
|
||||
@override
|
||||
String get onlyVisibleToYouText => '自分しか見れません';
|
||||
|
||||
@override
|
||||
String threadReplyCountText(int count) => '$countつのスレッド返信';
|
||||
|
||||
@override
|
||||
String attachmentsUploadProgressText({
|
||||
required int remaining,
|
||||
required int total,
|
||||
}) =>
|
||||
'$remaining/${total}mbのアップロード中 。。。';
|
||||
|
||||
@override
|
||||
String pinnedByUserText({
|
||||
required User pinnedBy,
|
||||
required User currentUser,
|
||||
}) {
|
||||
final pinnedByCurrentUser = currentUser.id == pinnedBy.id;
|
||||
if (pinnedByCurrentUser) return 'あなたのピン';
|
||||
return '${pinnedBy.name}のピン';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emptyMessagesText => '現在、メッセージはありません。';
|
||||
|
||||
@override
|
||||
String get genericErrorText => 'エラーが発生しました';
|
||||
|
||||
@override
|
||||
String get loadingMessagesError => 'メッセージの読み込みエラー';
|
||||
|
||||
@override
|
||||
String resultCountText(int count) => '$count件の結果';
|
||||
|
||||
@override
|
||||
String get messageDeletedText => 'このメッセージは削除されました。';
|
||||
|
||||
@override
|
||||
String get messageDeletedLabel => 'メッセージ削除';
|
||||
|
||||
@override
|
||||
String get messageReactionsLabel => 'メッセージのリアクション';
|
||||
|
||||
@override
|
||||
String get emptyChatMessagesText => 'チャットがありませんが。。。';
|
||||
|
||||
@override
|
||||
String threadSeparatorText(int replyCount) => '$replyCount件の返信';
|
||||
|
||||
@override
|
||||
String get connectedLabel => '接続しています';
|
||||
|
||||
@override
|
||||
String get disconnectedLabel => '接続切れ';
|
||||
|
||||
@override
|
||||
String get reconnectingLabel => '再接続中。。。';
|
||||
|
||||
@override
|
||||
String get alsoSendAsDirectMessageLabel => 'ダイレクトメッセージでも送信';
|
||||
|
||||
@override
|
||||
String get addACommentOrSendLabel => 'コメントの追加や送信';
|
||||
|
||||
@override
|
||||
String get searchGifLabel => 'GIFの検索';
|
||||
|
||||
@override
|
||||
String get writeAMessageLabel => 'メッセージを書く';
|
||||
|
||||
@override
|
||||
String get instantCommandsLabel => 'インスタントコマンド';
|
||||
|
||||
@override
|
||||
String fileTooLargeAfterCompressionError(double limitInMB) =>
|
||||
'ファイルのサイズが大きすぎてアップロードできません。'
|
||||
'ファイルサイズの制限は${limitInMB}MBです。'
|
||||
'圧縮してみましたが、十分ではありませんでした。';
|
||||
|
||||
@override
|
||||
String fileTooLargeError(double limitInMB) =>
|
||||
'ファイルが大きすぎてアップロードできません。ファイルサイズの制限は${limitInMB}MBです。';
|
||||
|
||||
@override
|
||||
String emojiMatchingQueryText(String query) => '「"$query"」とお揃いの絵文字';
|
||||
|
||||
@override
|
||||
String get addAFileLabel => 'ファイルの追加';
|
||||
|
||||
@override
|
||||
String get photoFromCameraLabel => 'カメラからの写真';
|
||||
|
||||
@override
|
||||
String get uploadAFileLabel => 'ファイルのアップロード';
|
||||
|
||||
@override
|
||||
String get uploadAPhotoLabel => '写真のアップロード';
|
||||
|
||||
@override
|
||||
String get uploadAVideoLabel => '動画のアップロード';
|
||||
|
||||
@override
|
||||
String get videoFromCameraLabel => 'カメラからの動画';
|
||||
|
||||
@override
|
||||
String get okLabel => 'よし';
|
||||
|
||||
@override
|
||||
String get somethingWentWrongError => 'エラーが発生しました';
|
||||
|
||||
@override
|
||||
String get addMoreFilesLabel => 'ファイルの追加';
|
||||
|
||||
@override
|
||||
String get enablePhotoAndVideoAccessMessage => 'お友達と共有できるように、写真'
|
||||
'\nやビデオへのアクセスを有効にしてください。';
|
||||
@override
|
||||
String get allowGalleryAccessMessage => 'ギャラリーへのアクセスを許可する';
|
||||
|
||||
@override
|
||||
String get flagMessageLabel => 'メッセージをフラグする';
|
||||
|
||||
@override
|
||||
String get flagMessageQuestion => 'このメッセージのコピーを'
|
||||
'\nモデレーターに送って、さらに調査してもらいますか?';
|
||||
|
||||
@override
|
||||
String get flagLabel => 'フラグする';
|
||||
|
||||
@override
|
||||
String get cancelLabel => 'キャンセル';
|
||||
|
||||
@override
|
||||
String get flagMessageSuccessfulLabel => 'メッセージにフラグが付けられました';
|
||||
|
||||
@override
|
||||
String get flagMessageSuccessfulText => 'このメッセージはモデレーターに報告されました。';
|
||||
|
||||
@override
|
||||
String get deleteLabel => '削除';
|
||||
|
||||
@override
|
||||
String get deleteMessageLabel => 'メッセージを削除する ';
|
||||
|
||||
@override
|
||||
String get deleteMessageQuestion => 'このメッセージ'
|
||||
'\nを完全に削除してもよろしいですか?';
|
||||
|
||||
@override
|
||||
String get operationCouldNotBeCompletedText => '操作を完了できませんでした。';
|
||||
|
||||
@override
|
||||
String get replyLabel => '返信';
|
||||
|
||||
@override
|
||||
String togglePinUnpinText({required bool pinned}) {
|
||||
if (pinned) return '会話のピンを外す';
|
||||
return '会話にピンする';
|
||||
}
|
||||
|
||||
@override
|
||||
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) {
|
||||
if (isDeleteFailed) return 'メッセージの削除を再試行する';
|
||||
return 'メッセージを削除する';
|
||||
}
|
||||
|
||||
@override
|
||||
String get copyMessageLabel => 'メッセージをコピーする';
|
||||
|
||||
@override
|
||||
String get editMessageLabel => 'メッセージを編集する';
|
||||
|
||||
@override
|
||||
String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) {
|
||||
if (isUpdateFailed) return '編集したメッセージを再送する';
|
||||
return '再送';
|
||||
}
|
||||
|
||||
@override
|
||||
String get photosLabel => '写真';
|
||||
|
||||
String _getDay(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final yesterday = DateTime(now.year, now.month, now.day - 1);
|
||||
|
||||
final date = DateTime(dateTime.year, dateTime.month, dateTime.day);
|
||||
|
||||
if (date == today) {
|
||||
return '今日';
|
||||
} else if (date == yesterday) {
|
||||
return '昨日';
|
||||
} else {
|
||||
return '${Jiffy(date).MMMd}に';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
||||
'${_getDay(date)}の${Jiffy(time.toLocal()).format('HH:mm')}に送信しました ';
|
||||
|
||||
@override
|
||||
String get todayLabel => '今日';
|
||||
|
||||
@override
|
||||
String get yesterdayLabel => '昨日';
|
||||
|
||||
@override
|
||||
String get channelIsMutedText => 'チャンネルが無音されています';
|
||||
|
||||
@override
|
||||
String get noTitleText => 'タイトル無し';
|
||||
|
||||
@override
|
||||
String get letsStartChattingLabel => 'チャットを始めよう!';
|
||||
|
||||
@override
|
||||
String get sendingFirstMessageLabel => '友人に最初のメッセージを送りましょうか?';
|
||||
|
||||
@override
|
||||
String get startAChatLabel => 'チャットを開始する';
|
||||
|
||||
@override
|
||||
String get loadingChannelsError => 'チャネルのロード中にエラーが発生しました';
|
||||
|
||||
@override
|
||||
String get deleteConversationLabel => '会話を削除する';
|
||||
|
||||
@override
|
||||
String get deleteConversationQuestion => '本当に会話を削除しますか?';
|
||||
|
||||
@override
|
||||
String get streamChatLabel => 'ストリームチャット';
|
||||
|
||||
@override
|
||||
String get searchingForNetworkText => 'ネットワークを検索中';
|
||||
|
||||
@override
|
||||
String get offlineLabel => 'オフライン。。。';
|
||||
|
||||
@override
|
||||
String get tryAgainLabel => '再試行する';
|
||||
|
||||
@override
|
||||
String membersCountText(int count) => '$count人のメンバー';
|
||||
|
||||
@override
|
||||
String watchersCountText(int count) => '$count人がオンライン';
|
||||
|
||||
@override
|
||||
String get viewInfoLabel => '情報を見る';
|
||||
|
||||
@override
|
||||
String get leaveGroupLabel => 'グループを離れる';
|
||||
|
||||
@override
|
||||
String get leaveLabel => '離れる';
|
||||
|
||||
@override
|
||||
String get leaveConversationLabel => '会話を離れる';
|
||||
|
||||
@override
|
||||
String get leaveConversationQuestion => '本当に会話を離れますか?';
|
||||
|
||||
@override
|
||||
String get showInChatLabel => 'チャットで表示';
|
||||
|
||||
@override
|
||||
String get saveImageLabel => '画像を保存';
|
||||
|
||||
@override
|
||||
String get saveVideoLabel => 'ビデオを保存';
|
||||
|
||||
@override
|
||||
String get uploadErrorLabel => 'アップロードエラー';
|
||||
|
||||
@override
|
||||
String get giphyLabel => 'ギフィー';
|
||||
|
||||
@override
|
||||
String get shuffleLabel => 'ミックス';
|
||||
|
||||
@override
|
||||
String get sendLabel => '送信';
|
||||
|
||||
@override
|
||||
String get withText => 'と';
|
||||
|
||||
@override
|
||||
String get inText => 'に';
|
||||
|
||||
// This is the word for 'customer' or 'user' because saying 'you' directly
|
||||
//is too informal and rude
|
||||
@override
|
||||
String get youText => 'あなた';
|
||||
|
||||
@override
|
||||
String galleryPaginationText({
|
||||
required int currentPage,
|
||||
required int totalPages,
|
||||
}) =>
|
||||
'${currentPage + 1} / $totalPages';
|
||||
|
||||
@override
|
||||
String get fileText => 'ファイル';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'メッセージに返信';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'スローモードオン';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) => '''
|
||||
添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません
|
||||
''';
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
part of 'stream_chat_localizations.dart';
|
||||
|
||||
/// The translations for Korean (`ko`).
|
||||
class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations {
|
||||
/// Create an instance of the translation bundle for Korean.
|
||||
const StreamChatLocalizationsKo({String localeName = 'ko'})
|
||||
: super(localeName: localeName);
|
||||
|
||||
@override
|
||||
String get launchUrlError => 'URL을 시작할 수 없습니다';
|
||||
|
||||
@override
|
||||
String get loadingUsersError => '사용자를 로드하는 중 오류 발생';
|
||||
|
||||
@override
|
||||
String get noUsersLabel => '현재 사용자가 없습니다';
|
||||
|
||||
@override
|
||||
String get retryLabel => '다시 시도하십시오';
|
||||
|
||||
@override
|
||||
String get userLastOnlineText => '마지막 온라인입니다';
|
||||
|
||||
@override
|
||||
String get userOnlineText => '온라인';
|
||||
|
||||
@override
|
||||
String userTypingText(Iterable<User> users) {
|
||||
if (users.isEmpty) return '';
|
||||
final first = users.first;
|
||||
if (users.length == 1) {
|
||||
return '${first.name} 타이핑중';
|
||||
}
|
||||
return '${first.name}하고 ${users.length - 1}명 타이핑중';
|
||||
}
|
||||
|
||||
@override
|
||||
String get threadReplyLabel => '스레드 응답입니다';
|
||||
|
||||
@override
|
||||
String get onlyVisibleToYouText => '당신만 볼 수 있습니다';
|
||||
|
||||
@override
|
||||
String threadReplyCountText(int count) => '$count스레드 답장';
|
||||
|
||||
@override
|
||||
String attachmentsUploadProgressText({
|
||||
required int remaining,
|
||||
required int total,
|
||||
}) =>
|
||||
'$remaining/${total}mb를 업로드중...';
|
||||
|
||||
@override
|
||||
String pinnedByUserText({
|
||||
required User pinnedBy,
|
||||
required User currentUser,
|
||||
}) {
|
||||
final pinnedByCurrentUser = currentUser.id == pinnedBy.id;
|
||||
if (pinnedByCurrentUser) return '당신의 핀';
|
||||
return '${pinnedBy.name}의 핀';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emptyMessagesText => '현재 메시지가 없습니다';
|
||||
|
||||
@override
|
||||
String get genericErrorText => '뭔가 잘못됐습니다';
|
||||
|
||||
@override
|
||||
String get loadingMessagesError => '메시지를 로드하는 동안 오류가 발생했습니다';
|
||||
|
||||
@override
|
||||
String resultCountText(int count) => '$count개의 결과';
|
||||
|
||||
@override
|
||||
String get messageDeletedText => '이 메시지는 삭제되었습니다.';
|
||||
|
||||
@override
|
||||
String get messageDeletedLabel => '메시지가 삭제되었습니다';
|
||||
|
||||
@override
|
||||
String get messageReactionsLabel => '메시지에 대한 응답';
|
||||
|
||||
@override
|
||||
String get emptyChatMessagesText => '아직 채팅이 없습니다...';
|
||||
|
||||
@override
|
||||
String threadSeparatorText(int replyCount) => '$replyCount개의 답장';
|
||||
|
||||
@override
|
||||
String get connectedLabel => '연결중';
|
||||
|
||||
@override
|
||||
String get disconnectedLabel => '연결이 끊겼습니다';
|
||||
|
||||
@override
|
||||
String get reconnectingLabel => '다시 연결하는 중...';
|
||||
|
||||
@override
|
||||
String get alsoSendAsDirectMessageLabel => '다이렉트 메시지로도 보냅니다';
|
||||
|
||||
@override
|
||||
String get addACommentOrSendLabel => '주석을 추가하거나 보냅니다';
|
||||
|
||||
@override
|
||||
String get searchGifLabel => 'GIF 검색';
|
||||
|
||||
@override
|
||||
String get writeAMessageLabel => '메시지 쓰기';
|
||||
|
||||
@override
|
||||
String get instantCommandsLabel => '인스턴트 커맨즈';
|
||||
|
||||
@override
|
||||
String fileTooLargeAfterCompressionError(double limitInMB) =>
|
||||
'파일이 너무 커서 업로드할 수 없습니다. '
|
||||
'파일 크기 제한은 ${limitInMB}MB입니다. '
|
||||
'우리는 압축해 보았지만 충분하지 않았습니다.';
|
||||
|
||||
@override
|
||||
String fileTooLargeError(double limitInMB) =>
|
||||
'파일이 너무 커서 업로드할 수 없습니다. 파일 크기 제한은 ${limitInMB}MB입니다.';
|
||||
|
||||
@override
|
||||
String emojiMatchingQueryText(String query) => '"$query"과 일치하는 이모티콘입니다';
|
||||
|
||||
@override
|
||||
String get addAFileLabel => '파일을 추가함';
|
||||
|
||||
@override
|
||||
String get photoFromCameraLabel => '카메라에서 찍은 사진';
|
||||
|
||||
@override
|
||||
String get uploadAFileLabel => '파일을 업로드함';
|
||||
|
||||
@override
|
||||
String get uploadAPhotoLabel => '사진을 업로드함';
|
||||
|
||||
@override
|
||||
String get uploadAVideoLabel => '비디오를 업로드함';
|
||||
|
||||
@override
|
||||
String get videoFromCameraLabel => '카메라의 비디오.';
|
||||
|
||||
@override
|
||||
String get okLabel => '확인';
|
||||
|
||||
@override
|
||||
String get somethingWentWrongError => '뭔가 잘못됐습느다';
|
||||
|
||||
@override
|
||||
String get addMoreFilesLabel => '파일을 추가함';
|
||||
|
||||
@override
|
||||
String get enablePhotoAndVideoAccessMessage => '친구와 공유할 수 있도록 사진과'
|
||||
'\n동영상에 액세스할 수 있도록 설정하십시오.';
|
||||
|
||||
@override
|
||||
String get allowGalleryAccessMessage => '갤러리에 대한 액세스를 허용합니다';
|
||||
|
||||
@override
|
||||
String get flagMessageLabel => ' 메시지를 플래그함';
|
||||
|
||||
@override
|
||||
String get flagMessageQuestion => '추가 조사를 위해 진행자에게 이 메시지의 복사본을 전송하시겠습니까?';
|
||||
|
||||
@override
|
||||
String get flagLabel => '플래그함';
|
||||
|
||||
@override
|
||||
String get cancelLabel => '취소';
|
||||
|
||||
@override
|
||||
String get flagMessageSuccessfulLabel => '메시지에 플래그가 지정되었습니다';
|
||||
|
||||
@override
|
||||
String get flagMessageSuccessfulText => '메시지가 진행자에게 보고되었습니다.';
|
||||
|
||||
@override
|
||||
String get deleteLabel => '삭제';
|
||||
|
||||
@override
|
||||
String get deleteMessageLabel => '메시지를 삭제합니다.';
|
||||
|
||||
@override
|
||||
String get deleteMessageQuestion => '이 메시지를 완전히 삭제하시겠습니까?';
|
||||
|
||||
@override
|
||||
String get operationCouldNotBeCompletedText => '작업을 완료할 수 없습니다.';
|
||||
|
||||
@override
|
||||
String get replyLabel => '답글';
|
||||
|
||||
@override
|
||||
String togglePinUnpinText({required bool pinned}) {
|
||||
if (pinned) return '대화의 핀을 분리합니다';
|
||||
return '대화에 고정합니다';
|
||||
}
|
||||
|
||||
@override
|
||||
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) {
|
||||
if (isDeleteFailed) return '메시지 삭제를 다시 시도합니다';
|
||||
return '메시지를 삭제합니다';
|
||||
}
|
||||
|
||||
@override
|
||||
String get copyMessageLabel => '메시지를 복사합니다.';
|
||||
|
||||
@override
|
||||
String get editMessageLabel => '메시지를 편집합니다.';
|
||||
|
||||
@override
|
||||
String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) {
|
||||
if (isUpdateFailed) return '편집된 메시지를 다시 보냅니다.';
|
||||
return '다시 보냅니다.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get photosLabel => '사진';
|
||||
|
||||
String _getDay(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final yesterday = DateTime(now.year, now.month, now.day - 1);
|
||||
|
||||
final date = DateTime(dateTime.year, dateTime.month, dateTime.day);
|
||||
|
||||
if (date == today) {
|
||||
return '오늘';
|
||||
} else if (date == yesterday) {
|
||||
return '어제';
|
||||
} else {
|
||||
return '${Jiffy(date).MMMd}에';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String sentAtText({required DateTime date, required DateTime time}) =>
|
||||
'${_getDay(date)} ${Jiffy(time.toLocal()).format('HH:mm')}에 보냈습니다';
|
||||
|
||||
@override
|
||||
String get todayLabel => '오늘';
|
||||
|
||||
@override
|
||||
String get yesterdayLabel => '어제';
|
||||
|
||||
@override
|
||||
String get channelIsMutedText => '채널이 음소거됩니다.';
|
||||
|
||||
@override
|
||||
String get noTitleText => '제목이 없습니다.';
|
||||
|
||||
@override
|
||||
String get letsStartChattingLabel => '채팅 시작해요!';
|
||||
|
||||
@override
|
||||
String get sendingFirstMessageLabel => '친구에게 첫 메시지를 보내 볼까요?';
|
||||
|
||||
@override
|
||||
String get startAChatLabel => '대화를 시작합니다.';
|
||||
|
||||
@override
|
||||
String get loadingChannelsError => '채널을 로드하는 동안 오류가 발생했습니다.';
|
||||
|
||||
@override
|
||||
String get deleteConversationLabel => '대화를 삭제합니다.';
|
||||
|
||||
@override
|
||||
String get deleteConversationQuestion => '대화를 삭제하시겠습니까?';
|
||||
|
||||
@override
|
||||
String get streamChatLabel => '스트림 채팅';
|
||||
|
||||
@override
|
||||
String get searchingForNetworkText => '네트워크를 검색하는 중입니다.';
|
||||
|
||||
@override
|
||||
String get offlineLabel => '오프라인...';
|
||||
|
||||
@override
|
||||
String get tryAgainLabel => '다시 시도합니다';
|
||||
|
||||
@override
|
||||
String membersCountText(int count) => '$count명';
|
||||
|
||||
@override
|
||||
String watchersCountText(int count) => '$count명이 온라인';
|
||||
|
||||
@override
|
||||
String get viewInfoLabel => '정보를 보기';
|
||||
|
||||
@override
|
||||
String get leaveGroupLabel => '그룹을 떠납니다.';
|
||||
|
||||
@override
|
||||
String get leaveLabel => '떠나다';
|
||||
|
||||
@override
|
||||
String get leaveConversationLabel => '대화에서 떠납니다.';
|
||||
|
||||
@override
|
||||
String get leaveConversationQuestion => '정말 이 대화에서 나가시겠습니까?';
|
||||
|
||||
@override
|
||||
String get showInChatLabel => '채팅에 표시합니다.';
|
||||
|
||||
@override
|
||||
String get saveImageLabel => '이미지를 저장합니다.';
|
||||
|
||||
@override
|
||||
String get saveVideoLabel => '비디오를 저장합니다.';
|
||||
|
||||
@override
|
||||
String get uploadErrorLabel => '업로드 오류';
|
||||
|
||||
@override
|
||||
String get giphyLabel => '지피';
|
||||
|
||||
@override
|
||||
String get shuffleLabel => '섞기';
|
||||
|
||||
@override
|
||||
String get sendLabel => '보내기';
|
||||
|
||||
@override
|
||||
String get withText => '함께';
|
||||
|
||||
@override
|
||||
String get inText => '에';
|
||||
|
||||
// This is the word for 'customer' or 'user' because saying 'you' directly
|
||||
// is too informal and rude
|
||||
@override
|
||||
String get youText => '당신';
|
||||
|
||||
@override
|
||||
String galleryPaginationText(
|
||||
{required int currentPage, required int totalPages}) =>
|
||||
'${currentPage + 1} / $totalPages';
|
||||
//3 / 11
|
||||
|
||||
@override
|
||||
String get fileText => '파일';
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => '메시지에 회신합니다.';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => '슬로모드 켜짐';
|
||||
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) =>
|
||||
'첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
name: stream_chat_localizations
|
||||
description: The Official localizations for Stream Chat Flutter, a service for building chat applications
|
||||
version: 1.0.2
|
||||
version: 1.1.0
|
||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
@@ -14,7 +14,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
stream_chat_flutter: ^2.1.1
|
||||
stream_chat_flutter: ^2.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -174,9 +174,10 @@ void main() {
|
||||
expect(localizations.withText, isNotNull);
|
||||
expect(localizations.inText, isNotNull);
|
||||
expect(localizations.youText, isNotNull);
|
||||
expect(localizations.ofText, isNotNull);
|
||||
expect(localizations.galleryPaginationText, isNotNull);
|
||||
expect(localizations.fileText, isNotNull);
|
||||
expect(localizations.replyToMessageLabel, isNotNull);
|
||||
expect(localizations.attachmentLimitExceedError(3), isNotNull);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## 2.2.0
|
||||
|
||||
- Updated llc dependency
|
||||
- Added support for message.i18n
|
||||
- Added support for user.language
|
||||
|
||||
## 2.1.1
|
||||
|
||||
- Updated llc dependency
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat_persistence
|
||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
|
||||
version: 2.1.1
|
||||
version: 2.2.0
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -18,7 +18,7 @@ dependencies:
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.1
|
||||
sqlite3_flutter_libs: ^0.5.0
|
||||
stream_chat: ^2.1.1
|
||||
stream_chat: ^2.2.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.0.1
|
||||
|
||||
Reference in New Issue
Block a user