Merge branch 'develop' into feat/limit-attachment-selection

This commit is contained in:
Salvatore Giordano
2021-08-23 11:14:08 +02:00
63 changed files with 2298 additions and 485 deletions
+3 -1
View File
@@ -8,7 +8,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,
@@ -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?,
);
}
@@ -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
@@ -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'};
@@ -605,4 +605,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');
+90 -54
View File
@@ -2,8 +2,9 @@
✅ 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`
@@ -13,13 +14,26 @@
- 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`
@@ -31,12 +45,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
@@ -53,7 +78,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
@@ -67,17 +93,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
@@ -98,7 +124,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
@@ -106,7 +132,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
@@ -115,10 +142,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
@@ -128,17 +157,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
@@ -165,21 +194,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
@@ -249,7 +281,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
@@ -306,7 +339,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
@@ -323,8 +357,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
@@ -358,7 +392,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
@@ -543,10 +578,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
@@ -648,8 +684,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
@@ -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),
),
),
),
);
@@ -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,
);
}
@@ -142,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,
),
],
@@ -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,8 +299,9 @@ 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;
@@ -659,7 +663,9 @@ 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';
@@ -670,4 +676,7 @@ class DefaultTranslations implements Translations {
@override
String attachmentLimitExceedError(int limit) =>
'Attachment limit exceeded, limit: $limit';
@override
String get slowModeOnLabel => 'Slow mode ON';
}
@@ -60,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
@@ -176,6 +185,8 @@ class MessageInput extends StatefulWidget {
this.onError,
this.attachmentLimit = 10,
this.onAttachmentLimitExceed,
this.attachmentButtonBuilder,
this.commandButtonBuilder,
}) : assert(
initialMessage == null || editMessage == null,
"Can't provide both `initialMessage` and `editMessage`",
@@ -271,6 +282,18 @@ class MessageInput extends StatefulWidget {
/// 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();
@@ -316,10 +339,15 @@ class MessageInputState extends State<MessageInput> {
bool get _hasQuotedMessage => widget.quotedMessage != null;
bool get _messageIsPresent => _textEditingController.text.trim().isNotEmpty;
late DateTime? _cooldownStartedAt;
int? _timeOut;
Timer? _slowModeTimer;
@override
void initState() {
super.initState();
_startSlowMode();
_focusNode = widget.focusNode ?? FocusNode();
_emojiNames =
Emoji.all().where((it) => it.name != null).map((e) => e.name!);
@@ -350,6 +378,25 @@ class MessageInputState extends State<MessageInput> {
});
}
void _startSlowMode() {
final channel = StreamChannel.of(context).channel;
if (channel.cooldownStartedAt != null) {
_cooldownStartedAt = channel.cooldownStartedAt;
if (DateTime.now().difference(_cooldownStartedAt!).inSeconds <
channel.cooldown!) {
_timeOut = channel.cooldown! -
DateTime.now().difference(_cooldownStartedAt!).inSeconds;
_slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_timeOut == 0) {
timer.cancel();
} else {
setState(() => _timeOut = _timeOut! - 1);
}
});
}
}
}
@override
Widget build(BuildContext context) {
Widget child = DecoratedBox(
@@ -428,11 +475,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),
],
@@ -498,24 +545,29 @@ 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 != null && _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),
@@ -553,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)),
),
@@ -694,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),
@@ -722,7 +773,7 @@ class MessageInputState extends State<MessageInput> {
),
if (!_commandEnabled &&
widget.actionsLocation == ActionsLocation.rightInside)
_buildExpandActionsButton(),
_buildExpandActionsButton(context),
if (widget.sendButtonLocation == SendButtonLocation.inside)
_animateSendButton(context),
],
@@ -783,6 +834,10 @@ class MessageInputState extends State<MessageInput> {
if (_attachments.isNotEmpty) {
return context.translations.addACommentOrSendLabel;
}
if (_timeOut != 0 && _timeOut != null) {
return context.translations.slowModeOnLabel;
}
return context.translations.writeAMessageLabel;
}
@@ -1702,10 +1757,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
@@ -1743,38 +1797,46 @@ 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;
_filePickerSize = _kMinMediaPickerSize;
});
} else {
showAttachmentModal();
}
},
);
return widget.attachmentButtonBuilder?.call(context, defaultButton) ??
defaultButton;
}
/// Show the attachment modal, making the user choose where to
/// pick a media from
@@ -1904,15 +1966,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) {
@@ -2119,6 +2180,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) {
@@ -2207,6 +2269,7 @@ class MessageInputState extends State<MessageInput> {
_emojiOverlay?.remove();
_mentionsOverlay?.remove();
_keyboardListener?.cancel();
_slowModeTimer?.cancel();
super.dispose();
}
@@ -2332,3 +2395,30 @@ class _PickerWidgetState extends State<_PickerWidget> {
);
}
}
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: [
@@ -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(
@@ -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';
@@ -48,7 +48,7 @@ void main() {
);
testWidgets(
'it should show the the other member image',
'it should show the other member image',
(tester) async {
final client = MockClient();
final clientState = MockClientState();
@@ -74,9 +74,7 @@ void main() {
userId: 'user-id2',
user: User(
id: 'user-id2',
extraData: const {
'image': 'testimage',
},
image: 'testimage',
),
)
]));
@@ -85,9 +83,7 @@ void main() {
userId: 'user-id2',
user: User(
id: 'user-id2',
extraData: const {
'image': 'testimage',
},
image: 'testimage',
),
),
Member(
@@ -98,9 +94,7 @@ void main() {
when(() => clientState.usersStream).thenAnswer((i) => Stream.value({
'user-id2': User(
id: 'user-id2',
extraData: const {
'image': 'testimage',
},
image: 'testimage',
),
}));
when(() => channel.extraData).thenReturn({
@@ -149,27 +143,21 @@ void main() {
userId: 'user-id',
user: User(
id: 'user-id',
extraData: const {
'image': 'testimage1',
},
image: 'testimage1',
),
),
Member(
userId: 'user-id2',
user: User(
id: 'user-id2',
extraData: const {
'image': 'testimage2',
},
image: 'testimage2',
),
),
Member(
userId: 'user-id3',
user: User(
id: 'user-id3',
extraData: const {
'image': 'testimage3',
},
image: 'testimage3',
),
),
];
@@ -21,6 +21,8 @@ void main() {
when(() => clientState.currentUser).thenReturn(user);
when(() => clientState.currentUserStream)
.thenAnswer((_) => Stream.value(user));
when(() => channel.lastMessageAtStream)
.thenAnswer((_) => Stream.value(lastMessageAt));
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
when(() => channel.state).thenReturn(channelState);
when(() => channel.client).thenReturn(client);
@@ -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);
},
);
}
@@ -1,3 +1,14 @@
## Upcoming
🛑️ 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
+1 -1
View File
@@ -26,7 +26,7 @@ It teaches you how to use this SDK and also shows how to make frequently require
## Example App
This repo includes a fully functional example app with setup instructions.
The example is available under the [example](https://github.com/GetStream/stream-chat-flutter-core/tree/master/example) folder.
The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/main/packages/stream_chat_flutter_core/example) folder.
## Add dependency
Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter_core.svg)](https://pub.dartlang.org/packages/stream_chat_flutter_core)
@@ -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';
@@ -132,16 +133,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);
}
@@ -558,6 +558,7 @@ void main() {
config: ChannelConfig(),
createdAt: DateTime.now(),
memberCount: 1,
cooldown: 0,
),
);
@@ -1,7 +1,20 @@
## Upcoming
* Added support for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) locale.
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 strings for cooldown mode.
🔄 Changed
* Some of the `Hindi` translations have been updated/changed for better understanding.
- 'रिप्लाई' -> 'जवाब दें'
- 'तस्वीरें' -> 'फ़ोटोज'
- 'बिता हुआ कल' -> 'कल'
- 'चैनल मौन है' -> 'चैनल म्यूट है'
## 1.0.2
* Updated `stream_chat_flutter` dependency
@@ -35,6 +35,8 @@ At the moment we support the following languages:
- [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.
@@ -70,6 +72,8 @@ class MyApp extends StatelessWidget {
Locale('fr'),
Locale('it'),
Locale('es'),
Locale('ja'),
Locale('ko'),
],
// Add GlobalStreamChatLocalizations.delegates
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
@@ -113,6 +117,8 @@ Example:
<string>fr</string>
<string>it</string>
<string>es</string>
<string>ja</string>
<string>ko</string>
</array>
```
@@ -374,7 +374,9 @@ 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';
@@ -385,6 +387,8 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
@override
String attachmentLimitExceedError(int limit) =>
'Attachment limit exceeded, limit: $limit';
String get slowModeOnLabel => 'Slow mode ON';
}
void main() async {
@@ -457,6 +461,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"
@@ -11,6 +11,10 @@ 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.
@@ -27,6 +31,8 @@ const kStreamChatSupportedLanguages = {
'fr',
'it',
'es',
'ja',
'ko'
};
/// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`.
@@ -59,6 +65,10 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) {
return const StreamChatLocalizationsIt();
case 'es':
return const StreamChatLocalizationsEs();
case 'ja':
return const StreamChatLocalizationsJa();
case 'ko':
return const StreamChatLocalizationsKo();
}
}
@@ -350,7 +350,9 @@ 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';
@@ -361,4 +363,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations {
@override
String attachmentLimitExceedError(int limit) =>
'Attachment limit exceeded, limit: $limit';
@override
String get slowModeOnLabel => 'Slow mode ON';
}
@@ -355,7 +355,9 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
String get youText => 'Usted';
@override
String get ofText => 'de';
String galleryPaginationText(
{required int currentPage, required int totalPages}) =>
'${currentPage + 1} de $totalPages';
@override
String get fileText => 'Archivo';
@@ -368,4 +370,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
// TODO: implement attachmentLimitExceedError
throw UnimplementedError();
}
@override
String get slowModeOnLabel => 'Modo lento activado';
}
@@ -207,7 +207,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
@override
String togglePinUnpinText({required bool pinned}) {
if (pinned) return 'Décrocher de la conversation';
return 'Épingler à la discussion';
return 'Épingler à la conversation';
}
@override
@@ -354,7 +354,9 @@ 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';
@@ -367,4 +369,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
// TODO: implement attachmentLimitExceedError
throw UnimplementedError();
}
@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,16 +340,18 @@ 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 => 'फ़ाइल';
@@ -362,4 +364,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
// TODO: implement attachmentLimitExceedError
throw UnimplementedError();
}
@override
String get slowModeOnLabel => 'स्लो मोड चालू';
}
@@ -351,7 +351,9 @@ 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';
@@ -364,4 +366,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
// TODO: implement attachmentLimitExceedError
throw UnimplementedError();
}
@override
String get slowModeOnLabel => 'Slowmode attiva';
}
@@ -0,0 +1,358 @@
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) {
// TODO: implement attachmentLimitExceedError
throw UnimplementedError();
}
}
@@ -0,0 +1,356 @@
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) {
// TODO: implement attachmentLimitExceedError
throw UnimplementedError();
}
}
@@ -174,7 +174,7 @@ 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);