Merge pull request #626 from GetStream/cds-189

feat(ui, llc): slow mode
This commit is contained in:
Salvatore Giordano
2021-08-23 09:58:28 +02:00
committed by GitHub
28 changed files with 398 additions and 16 deletions
+1
View File
@@ -10,6 +10,7 @@
- `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. - `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 `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. - `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 ## 2.1.1
🐞 Fixed 🐞 Fixed
@@ -201,6 +201,21 @@ class Channel {
return state!.channelStateStream.map((cs) => cs.channel?.frozen == true); return state!.channelStateStream.map((cs) => cs.channel?.frozen == true);
} }
/// Cooldown count
int? get cooldown {
_checkInitialized();
return state?._channelState.channel?.cooldown;
}
/// Cooldown count as a stream
Stream<int?>? get cooldownStream {
_checkInitialized();
return state?.channelStateStream.map((cs) => cs.channel?.cooldown);
}
/// Stores time at which cooldown was started
DateTime? cooldownStartedAt;
/// Channel creation date. /// Channel creation date.
DateTime? get createdAt { DateTime? get createdAt {
_checkInitialized(); _checkInitialized();
@@ -525,6 +540,9 @@ class Channel {
skipPush: skipPush, skipPush: skipPush,
); );
state!.addMessage(response.message); state!.addMessage(response.message);
if (cooldown! > 0) {
cooldownStartedAt = DateTime.now();
}
return response; return response;
} catch (e) { } catch (e) {
if (e is StreamChatNetworkError && e.isRetriable) { if (e is StreamChatNetworkError && e.isRetriable) {
@@ -980,6 +998,20 @@ class Channel {
return _client.updateChannelPartial(id!, type, set: set, unset: unset); return _client.updateChannelPartial(id!, type, set: set, unset: unset);
} }
/// Enable slow mode
Future<PartialUpdateChannelResponse> enableSlowMode({
required int cooldownInterval,
}) async {
_checkInitialized();
return _client.enableSlowdown(id!, type, cooldownInterval);
}
/// Disable slow mode
Future<PartialUpdateChannelResponse> disableSlowMode() async {
_checkInitialized();
return _client.disableSlowdown(id!, type);
}
/// Delete this channel. Messages are permanently removed. /// Delete this channel. Messages are permanently removed.
Future<EmptyResponse> delete() async { Future<EmptyResponse> delete() async {
_checkInitialized(); _checkInitialized();
@@ -1252,6 +1252,28 @@ class StreamChatClient {
language, 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 /// Pins provided message
/// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds /// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds
/// to be added to [DateTime.now] /// to be added to [DateTime.now]
@@ -123,6 +123,35 @@ class ChannelApi {
return PartialUpdateChannelResponse.fromJson(response.data); 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 /// Accept invitation to the channel
Future<AcceptInviteResponse> acceptChannelInvite( Future<AcceptInviteResponse> acceptChannelInvite(
String channelId, String channelId,
@@ -23,6 +23,7 @@ class ChannelModel {
this.memberCount = 0, this.memberCount = 0,
this.extraData = const {}, this.extraData = const {},
this.team, this.team,
this.cooldown = 0,
}) : assert( }) : assert(
(cid != null && cid.contains(':')) || (id != null && type != null), (cid != null && cid.contains(':')) || (id != null && type != null),
'provide either a cid or an id and type', 'provide either a cid or an id and type',
@@ -81,6 +82,10 @@ class ChannelModel {
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0)
final int memberCount; final int memberCount;
/// The number of seconds in a cooldown
@JsonKey(includeIfNull: false, defaultValue: 0)
final int cooldown;
/// Map of custom channel extraData /// Map of custom channel extraData
@JsonKey( @JsonKey(
includeIfNull: false, includeIfNull: false,
@@ -107,6 +112,7 @@ class ChannelModel {
'deleted_at', 'deleted_at',
'member_count', 'member_count',
'team', 'team',
'cooldown',
]; ];
/// Shortcut for channel name /// Shortcut for channel name
@@ -133,6 +139,7 @@ class ChannelModel {
int? memberCount, int? memberCount,
Map<String, Object?>? extraData, Map<String, Object?>? extraData,
String? team, String? team,
int? cooldown,
}) => }) =>
ChannelModel( ChannelModel(
id: id ?? this.id, id: id ?? this.id,
@@ -148,6 +155,7 @@ class ChannelModel {
memberCount: memberCount ?? this.memberCount, memberCount: memberCount ?? this.memberCount,
extraData: extraData ?? this.extraData, extraData: extraData ?? this.extraData,
team: team ?? this.team, team: team ?? this.team,
cooldown: cooldown ?? this.cooldown,
); );
/// Returns a new [ChannelModel] that is a combination of this channelModel /// Returns a new [ChannelModel] that is a combination of this channelModel
@@ -168,6 +176,7 @@ class ChannelModel {
memberCount: other.memberCount, memberCount: other.memberCount,
extraData: other.extraData, extraData: other.extraData,
team: other.team, team: other.team,
cooldown: other.cooldown,
); );
} }
} }
@@ -33,6 +33,7 @@ ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) {
memberCount: json['member_count'] as int? ?? 0, memberCount: json['member_count'] as int? ?? 0,
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, extraData: json['extra_data'] as Map<String, dynamic>? ?? {},
team: json['team'] as String?, 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('updated_at', readonly(instance.updatedAt));
writeNotNull('deleted_at', readonly(instance.deletedAt)); writeNotNull('deleted_at', readonly(instance.deletedAt));
writeNotNull('member_count', readonly(instance.memberCount)); writeNotNull('member_count', readonly(instance.memberCount));
val['cooldown'] = instance.cooldown;
val['extra_data'] = instance.extraData; val['extra_data'] = instance.extraData;
writeNotNull('team', readonly(instance.team)); writeNotNull('team', readonly(instance.team));
return val; return val;
@@ -184,6 +184,8 @@ class EventChannel extends ChannelModel {
DateTime? deletedAt, DateTime? deletedAt,
required int memberCount, required int memberCount,
Map<String, Object?>? extraData, Map<String, Object?>? extraData,
required int cooldown,
String? team,
}) : super( }) : super(
id: id, id: id,
type: type, type: type,
@@ -197,6 +199,8 @@ class EventChannel extends ChannelModel {
deletedAt: deletedAt, deletedAt: deletedAt,
memberCount: memberCount, memberCount: memberCount,
extraData: extraData ?? {}, extraData: extraData ?? {},
cooldown: cooldown,
team: team,
); );
/// Create a new instance from a json /// Create a new instance from a json
@@ -87,5 +87,7 @@ EventChannel _$EventChannelFromJson(Map<String, dynamic> json) {
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
memberCount: json['member_count'] as int? ?? 0, memberCount: json['member_count'] as int? ?? 0,
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, 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", "id": "dev",
"type": "team", "type": "team",
"frozen": true, "frozen": true,
"cooldown": 0,
"name": "#dev", "name": "#dev",
"image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png", "image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png",
"example": 1 "example": 1
@@ -1859,6 +1859,50 @@ void main() {
).called(1); ).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 { test('`.banUser`', () async {
const userId = 'test-user-id'; const userId = 'test-user-id';
const options = {'key': 'value'}; const options = {'key': 'value'};
@@ -605,4 +605,65 @@ void main() {
verify(() => client.post(path, data: {})).called(1); verify(() => client.post(path, data: {})).called(1);
verifyNoMoreInteractions(client); 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.cid, equals('livestream:test'));
expect(channel.extraData['cats'], equals(true)); expect(channel.extraData['cats'], equals(true));
expect(channel.extraData['fruit'], equals(['bananas', 'apples'])); expect(channel.extraData['fruit'], equals(['bananas', 'apples']));
expect(channel.cooldown, equals(0));
}); });
test('should serialize to json correctly', () { test('should serialize to json correctly', () {
@@ -24,7 +25,13 @@ void main() {
expect( expect(
channel.toJson(), 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( expect(
channel.toJson(), channel.toJson(),
{'id': 'id', 'type': 'type', 'name': 'cool', 'frozen': false}, {
'id': 'id',
'type': 'type',
'frozen': false,
'cooldown': 0,
'name': 'cool',
},
); );
}); });
}); });
@@ -25,6 +25,8 @@ typedef ActionButtonBuilder = Widget Function(
> **_NOTE:_** The last parameter is the default `ActionButton` > **_NOTE:_** The last parameter is the default `ActionButton`
You can call `.copyWith` to customize just a subset of properties. 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 🔄 Changed
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with
@@ -100,6 +100,9 @@ abstract class Translations {
/// The label for write a message in [MessageInput] /// The label for write a message in [MessageInput]
String get writeAMessageLabel; String get writeAMessageLabel;
/// The label for slow mode enabled in [MessageInput]
String get slowModeOnLabel;
/// The label for instant commands in [MessageInput] /// The label for instant commands in [MessageInput]
String get instantCommandsLabel; String get instantCommandsLabel;
@@ -667,4 +670,7 @@ class DefaultTranslations implements Translations {
@override @override
String get replyToMessageLabel => 'Reply to Message'; String get replyToMessageLabel => 'Reply to Message';
@override
String get slowModeOnLabel => 'Slow mode ON';
} }
@@ -315,9 +315,15 @@ class MessageInputState extends State<MessageInput> {
bool get _hasQuotedMessage => widget.quotedMessage != null; bool get _hasQuotedMessage => widget.quotedMessage != null;
late DateTime? _cooldownStartedAt;
int? _timeOut;
Timer? _slowModeTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_startSlowMode();
_focusNode = widget.focusNode ?? FocusNode(); _focusNode = widget.focusNode ?? FocusNode();
_emojiNames = _emojiNames =
Emoji.all().where((it) => it.name != null).map((e) => e.name!); Emoji.all().where((it) => it.name != null).map((e) => e.name!);
@@ -348,6 +354,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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = DecoratedBox( Widget child = DecoratedBox(
@@ -496,20 +521,25 @@ class MessageInputState extends State<MessageInput> {
); );
Widget _animateSendButton(BuildContext context) { Widget _animateSendButton(BuildContext context) {
final sendButton = widget.activeSendButton != null late Widget sendButton;
? InkWell( if (_timeOut != null && _timeOut! > 0) {
onTap: sendMessage, sendButton = _CountdownButton(
child: widget.activeSendButton, count: _timeOut!,
) );
: _buildSendButton(context); } else if (!_messageIsPresent && _attachments.isEmpty) {
return AnimatedCrossFade( sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty) } else {
? CrossFadeState.showFirst sendButton = widget.activeSendButton != null
: CrossFadeState.showSecond, ? InkWell(
firstChild: sendButton, onTap: sendMessage,
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), child: widget.activeSendButton,
duration: _messageInputTheme.sendAnimationDuration!, )
alignment: Alignment.center, : _buildSendButton(context);
}
return AnimatedSwitcher(
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
child: sendButton,
); );
} }
@@ -781,6 +811,10 @@ class MessageInputState extends State<MessageInput> {
if (_attachments.isNotEmpty) { if (_attachments.isNotEmpty) {
return context.translations.addACommentOrSendLabel; return context.translations.addACommentOrSendLabel;
} }
if (_timeOut != 0 && _timeOut != null) {
return context.translations.slowModeOnLabel;
}
return context.translations.writeAMessageLabel; return context.translations.writeAMessageLabel;
} }
@@ -2115,6 +2149,7 @@ class MessageInputState extends State<MessageInput> {
if (resp.message?.type == 'error') { if (resp.message?.type == 'error') {
_parseExistingMessage(message); _parseExistingMessage(message);
} }
_startSlowMode();
widget.onMessageSent?.call(resp.message); widget.onMessageSent?.call(resp.message);
} catch (e, stk) { } catch (e, stk) {
if (widget.onError != null) { if (widget.onError != null) {
@@ -2207,6 +2242,7 @@ class MessageInputState extends State<MessageInput> {
_emojiOverlay?.remove(); _emojiOverlay?.remove();
_mentionsOverlay?.remove(); _mentionsOverlay?.remove();
_keyboardListener?.cancel(); _keyboardListener?.cancel();
_slowModeTimer?.cancel();
super.dispose(); super.dispose();
} }
@@ -2380,3 +2416,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'),
),
),
),
);
}
@@ -69,4 +69,69 @@ void main() {
expect(find.byKey(const Key('messageInputText')), findsOneWidget); 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);
},
);
} }
@@ -558,6 +558,7 @@ void main() {
config: ChannelConfig(), config: ChannelConfig(),
createdAt: DateTime.now(), createdAt: DateTime.now(),
memberCount: 1, memberCount: 1,
cooldown: 0,
), ),
); );
@@ -5,6 +5,7 @@
* 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 [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 [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 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 🔄 Changed
@@ -383,6 +383,9 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
@override @override
String get replyToMessageLabel => 'Reply to Message'; String get replyToMessageLabel => 'Reply to Message';
@override
String get slowModeOnLabel => 'Slow mode ON';
} }
void main() async { void main() async {
@@ -359,4 +359,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations {
@override @override
String get replyToMessageLabel => 'Reply to Message'; String get replyToMessageLabel => 'Reply to Message';
@override
String get slowModeOnLabel => 'Slow mode ON';
} }
@@ -364,4 +364,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
@override @override
String get replyToMessageLabel => 'Responder al Mensaje'; String get replyToMessageLabel => 'Responder al Mensaje';
@override
String get slowModeOnLabel => 'Modo lento activado';
} }
@@ -363,4 +363,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
@override @override
String get replyToMessageLabel => 'Répondre au Message'; String get replyToMessageLabel => 'Répondre au Message';
@override
String get slowModeOnLabel => 'Mode lent activé';
} }
@@ -358,4 +358,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
@override @override
String get replyToMessageLabel => 'संदेश का जवाब'; String get replyToMessageLabel => 'संदेश का जवाब';
@override
String get slowModeOnLabel => 'स्लो मोड चालू';
} }
@@ -360,4 +360,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
@override @override
String get replyToMessageLabel => 'Rispondi al messaggio'; String get replyToMessageLabel => 'Rispondi al messaggio';
@override
String get slowModeOnLabel => 'Slowmode attiva';
} }
@@ -346,4 +346,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations {
@override @override
String get replyToMessageLabel => 'メッセージに返信'; String get replyToMessageLabel => 'メッセージに返信';
@override
String get slowModeOnLabel => 'スローモードオン';
} }
@@ -344,4 +344,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations {
@override @override
String get replyToMessageLabel => '메시지에 회신합니다.'; String get replyToMessageLabel => '메시지에 회신합니다.';
@override
String get slowModeOnLabel => '슬로모드 켜짐';
} }