From 61d7661297c8473859bd0f2e20927e950ef6b103 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 22 Jul 2021 10:11:34 -0400 Subject: [PATCH 001/165] chore: add cooldown to channel_model.dart and test it --- .../stream_chat/lib/src/core/models/channel_model.dart | 8 ++++++++ .../stream_chat/test/src/core/models/channel_test.dart | 1 + 2 files changed, 9 insertions(+) diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 37e587ff..9fdf3723 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -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,9 @@ class ChannelModel { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) final int memberCount; + @JsonKey(includeIfNull: false) + final int cooldown; + /// Map of custom channel extraData @JsonKey( includeIfNull: false, @@ -107,6 +111,7 @@ class ChannelModel { 'deleted_at', 'member_count', 'team', + 'cooldown', ]; /// Shortcut for channel name @@ -133,6 +138,7 @@ class ChannelModel { int? memberCount, Map? extraData, String? team, + int? cooldown, }) => ChannelModel( id: id ?? this.id, @@ -148,6 +154,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 +175,7 @@ class ChannelModel { memberCount: other.memberCount, extraData: other.extraData, team: other.team, + cooldown: other.cooldown, ); } } diff --git a/packages/stream_chat/test/src/core/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart index ca734c44..31162bd6 100644 --- a/packages/stream_chat/test/src/core/models/channel_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_test.dart @@ -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', () { From da3cf4d6de168f1e183758fb6978cb61134b9a8c Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 22 Jul 2021 10:20:31 -0400 Subject: [PATCH 002/165] chore: start adding cooldown and cooldown stream to channel.dart --- packages/stream_chat/lib/src/client/channel.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 94a07f34..6bec5ce7 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -118,6 +118,18 @@ class Channel { return state?.channelStateStream.map((cs) => cs.channel?.frozen); } + /// Cooldown count + int? get cooldown { + _checkInitialized(); + return state?._channelState.channel?.cooldown; + } + + /// Cooldown count as a stream + Stream? get cooldownStream { + _checkInitialized(); + return state?.cooldownStateStream.map((cs) => cs.channel?.cooldown); + } + /// Channel creation date DateTime? get createdAt { _checkInitialized(); From c2b6ea13d816fd12818dba5ff1c1475aba23d111 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 22 Jul 2021 13:46:04 -0400 Subject: [PATCH 003/165] fix: use the right stream for cooldown stream --- packages/stream_chat/lib/src/client/channel.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 6bec5ce7..4a7cbbb5 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -127,7 +127,7 @@ class Channel { /// Cooldown count as a stream Stream? get cooldownStream { _checkInitialized(); - return state?.cooldownStateStream.map((cs) => cs.channel?.cooldown); + return state?.channelStateStream.map((cs) => cs.channel?.cooldown); } /// Channel creation date From 848aa550f763c2cef2bbaa500e9ff96e5e870b87 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 22 Jul 2021 13:47:58 -0400 Subject: [PATCH 004/165] chore: add doc for cooldown in channel_model.dart --- packages/stream_chat/lib/src/core/models/channel_model.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 9fdf3723..3df04007 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -82,6 +82,7 @@ class ChannelModel { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) final int memberCount; + /// The number of seconds in a cooldown @JsonKey(includeIfNull: false) final int cooldown; From 9fb60e93c1c93d13b7b540a4f2bd3ef5dee6e9e7 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 26 Jul 2021 11:42:24 -0400 Subject: [PATCH 005/165] progress on slow mode --- .../stream_chat/lib/src/client/channel.dart | 20 ++++ .../stream_chat/lib/src/client/client.dart | 22 +++++ .../lib/src/core/api/channel_api.dart | 29 ++++++ .../lib/src/core/models/channel_model.dart | 2 +- .../lib/src/core/models/channel_model.g.dart | 2 + .../lib/src/core/models/own_user.g.dart | 3 + .../stream_chat_flutter/example/lib/main.dart | 14 ++- .../lib/src/message_input.dart | 98 +++++++++++++++++-- .../lib/src/message_list_view.dart | 4 +- 9 files changed, 178 insertions(+), 16 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 4a7cbbb5..8e298fc9 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -130,6 +130,9 @@ class Channel { return state?.channelStateStream.map((cs) => cs.channel?.cooldown); } + /// + DateTime? cooldownStartedAt; + /// Channel creation date DateTime? get createdAt { _checkInitialized(); @@ -429,6 +432,9 @@ class Channel { skipPush: skipPush, ); state!.addMessage(response.message); + if (cooldown! > 0) { + cooldownStartedAt = DateTime.now(); + } return response; } catch (e) { if (e is StreamChatNetworkError && e.isRetriable) { @@ -827,6 +833,20 @@ class Channel { return _client.updateChannelPartial(id!, type, set: set, unset: unset); } + /// Enable slow mode + Future enableSlowMode({ + required int cooldownInterval, + }) async { + _checkInitialized(); + return _client.enableSlowdown(id!, type, cooldownInterval); + } + + /// Disable slow mode + Future disableSlowMode() async { + _checkInitialized(); + return _client.disableSlowdown(id!, type); + } + /// Delete this channel. Messages are permanently removed. Future delete() async { _checkInitialized(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 050d36f1..2ea20e60 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1244,6 +1244,28 @@ class StreamChatClient { language, ); + /// Enables slow mode + Future enableSlowdown( + String channelId, + String channelType, + int cooldown, + ) async => + _chatApi.channel.enableSlowdown( + channelId, + channelType, + cooldown, + ); + + /// Disables slow mode + Future 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] diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 1ba7f333..31a64cf8 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -123,6 +123,35 @@ class ChannelApi { return PartialUpdateChannelResponse.fromJson(response.data); } + /// Enable slowdown + Future enableSlowdown( + String channelId, + String channelType, + int cooldown, + ) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'cooldown': cooldown, + }, + ); + return UpdateChannelResponse.fromJson(response.data); + } + + /// Disable slowdown + Future disableSlowdown( + String channelId, + String channelType, + ) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'cooldown': 0, + }, + ); + return UpdateChannelResponse.fromJson(response.data); + } + /// Accept invitation to the channel Future acceptChannelInvite( String channelId, diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 3df04007..a4e6db79 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -83,7 +83,7 @@ class ChannelModel { final int memberCount; /// The number of seconds in a cooldown - @JsonKey(includeIfNull: false) + @JsonKey(includeIfNull: false, defaultValue: 0) final int cooldown; /// Map of custom channel extraData diff --git a/packages/stream_chat/lib/src/core/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index 4bde3d4a..d9adf94b 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -33,6 +33,7 @@ ChannelModel _$ChannelModelFromJson(Map json) { memberCount: json['member_count'] as int? ?? 0, extraData: json['extra_data'] as Map? ?? {}, team: json['team'] as String?, + cooldown: json['cooldown'] as int? ?? 0, ); } @@ -57,6 +58,7 @@ Map _$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; diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index 26e4786e..465faff2 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -36,5 +36,8 @@ OwnUser _$OwnUserFromJson(Map json) { online: json['online'] as bool? ?? false, extraData: json['extra_data'] as Map? ?? {}, banned: json['banned'] as bool? ?? false, + teams: + (json['teams'] as List?)?.map((e) => e as String).toList() ?? + [], ); } diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 89362d81..6f038616 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -13,7 +13,7 @@ void main() async { /// Create a new instance of [StreamChatClient] passing the apikey obtained /// from your project dashboard. final client = StreamChatClient( - 's2dxdhpxd94g', + 'kv7mcsxr24p8', logLevel: Level.INFO, )..chatPersistenceClient = chatPersistentClient; @@ -24,15 +24,19 @@ void main() async { /// Please see the following for more information: /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( - User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + User(id: 'salvatore'), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.pgiJz7sIc7iP29BHKFwe3nLm5-OaR_1l2P-SlgiC9a8', ); - final channel = client.channel('messaging', id: 'godevs'); + final channel = client.channel('messaging', id: 'godevs2', extraData: { + 'members': ['salvatore'], + }); await channel.watch(); + await channel.enableSlowMode(cooldownInterval: 20); + print('Channel cooldown set to ${channel.cooldown}'); + runApp( MyApp( client: client, diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 5a4f6822..c3c2e30a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -291,9 +291,13 @@ class MessageInputState extends State { bool get _hasQuotedMessage => widget.quotedMessage != null; + late DateTime? _cooldownStartedAt; + int? _timeOut; + @override void initState() { super.initState(); + _startSlowMode(); _focusNode = widget.focusNode ?? FocusNode(); _emojiNames = Emoji.all().where((it) => it.name != null).map((e) => e.name!); @@ -324,6 +328,25 @@ class MessageInputState extends State { }); } + void _startSlowMode() { + if (StreamChannel.of(context).channel.cooldownStartedAt != null) { + _cooldownStartedAt = StreamChannel.of(context).channel.cooldownStartedAt; + if (DateTime.now().difference(_cooldownStartedAt!).inSeconds < + StreamChannel.of(context).channel.cooldown!) { + _timeOut = StreamChannel.of(context).channel.cooldown! - + DateTime.now().difference(_cooldownStartedAt!).inSeconds; + Timer.periodic(const Duration(seconds: 1), (timer) { + if (_timeOut == 0) { + timer.cancel(); + } else { + print('Time left until cooldown is over: $_timeOut'); + setState(() => _timeOut = _timeOut! - 1); + } + }); + } + } + } + @override Widget build(BuildContext context) { Widget child = DecoratedBox( @@ -472,13 +495,44 @@ class MessageInputState extends State { ); Widget _animateSendButton(BuildContext context) { - final sendButton = widget.activeSendButton != null - ? InkWell( - onTap: sendMessage, - child: widget.activeSendButton, - ) - : _buildSendButton(context); - return AnimatedCrossFade( + 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); + } + + /*if (_timeOut == null || _timeOut == 0) { + sendButton = widget.activeSendButton != null + ? InkWell( + onTap: sendMessage, + child: widget.activeSendButton, + ) + : _buildSendButton(context); + } else { + sendButton = _CountdownButton( + count: _timeOut!, + ); + } + + if (!_messageIsPresent && _attachments.isEmpty) { + sendButton = widget.idleSendButton ?? _buildIdleSendButton(context); + }*/ + + return AnimatedSwitcher( + duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, + child: sendButton, + ); + /*return AnimatedCrossFade( crossFadeState: (_messageIsPresent || _attachments.isNotEmpty) ? CrossFadeState.showFirst : CrossFadeState.showSecond, @@ -486,7 +540,7 @@ class MessageInputState extends State { secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, alignment: Alignment.center, - ); + );*/ } Widget _buildExpandActionsButton() { @@ -2082,6 +2136,7 @@ class MessageInputState extends State { if (resp.message?.type == 'error') { _parseExistingMessage(message); } + _startSlowMode(); widget.onMessageSent?.call(resp.message); } catch (e, stk) { if (widget.onError != null) { @@ -2347,3 +2402,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'), + ), + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 3d0a3acd..8a236cbd 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -942,7 +942,7 @@ class _MessageListViewState extends State { final currentUser = StreamChat.of(context).user; final members = StreamChannel.of(context).channel.state?.members ?? []; final currentUserMember = - members.firstWhere((e) => e.user!.id == currentUser!.id); + members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); Widget messageWidget = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), @@ -1049,7 +1049,7 @@ class _MessageListViewState extends State { } FocusScope.of(context).unfocus(); }, - showPinButton: widget.pinPermissions.contains(currentUserMember.role), + showPinButton: widget.pinPermissions.contains(currentUserMember?.role), ); if (widget.messageBuilder != null) { From 255fc50510d79ec01d24950fa3e6f5c7684fa3ed Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Thu, 29 Jul 2021 15:22:34 -0400 Subject: [PATCH 006/165] support spanish --- .../lib/src/stream_chat_localizations.dart | 11 +- .../lib/src/stream_chat_localizations_es.dart | 364 ++++++++++++++++++ 2 files changed, 369 insertions(+), 6 deletions(-) create mode 100644 packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index f98932b5..d388059d 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +part 'stream_chat_localizations_es.dart'; + part 'stream_chat_localizations_en.dart'; part 'stream_chat_localizations_fr.dart'; @@ -19,12 +21,7 @@ part 'stream_chat_localizations_hi.dart'; /// See also: /// /// * [getStreamChatTranslation], whose documentation describes these values. -const kStreamChatSupportedLanguages = { - 'en', - 'hi', - 'fr', - 'it', -}; +const kStreamChatSupportedLanguages = {'en', 'hi', 'fr', 'it', 'es'}; /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. /// @@ -54,6 +51,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { return const StreamChatLocalizationsFr(); case 'it': return const StreamChatLocalizationsIt(); + case 'es': + return const StreamChatLocalizationsEs(); } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart new file mode 100644 index 00000000..a3387c83 --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -0,0 +1,364 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for French (`fr`). +class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for French. + const StreamChatLocalizationsEs({String localeName = 'es'}) + : super(localeName: localeName); + + @override + String get launchUrlError => 'No se puede lanzar la url'; + + @override + String get loadingUsersError => 'Error de carga del usuario'; + + @override + String get noUsersLabel => 'No hay usuarios actualmente'; + + @override + String get retryLabel => 'Inténtalo de nuevo'; + + @override + String get userLastOnlineText => 'Última vez en línea'; + + @override + String get userOnlineText => 'En línea'; + + @override + String userTypingText(Iterable users) { + if (users.isEmpty) return ''; + final first = users.first; + if (users.length == 1) { + return '${first.name} está escribiendo'; + } + return '${first.name} y ${users.length - 1} están escribiendo'; + } + + @override + String get threadReplyLabel => 'Responder al hilo de discusión'; + + @override + String get onlyVisibleToYouText => 'Sólo visible para usted'; + + @override + String threadReplyCountText(int count) => + '$count Respuestas al hilo de discusión'; + + @override + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }) => + 'Transferencia en curso $remaining/$total ...'; + + @override + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }) { + final pinnedByCurrentUser = currentUser.id == pinnedBy.id; + if (pinnedByCurrentUser) return 'Fijado por ti'; + return 'Fijado por ${pinnedBy.name}'; + } + + @override + String get emptyMessagesText => 'Actualmente no hay mensajes'; + + @override + String get genericErrorText => 'Hubo un problema'; + + @override + String get loadingMessagesError => 'Mensajes de error de carga'; + + @override + String resultCountText(int count) => '$count resultados'; + + @override + String get messageDeletedText => 'Este mensaje ha sido borrado.'; + + @override + String get messageDeletedLabel => 'Mensaje borrado'; + + @override + String get messageReactionsLabel => 'Reacciones a los mensajes'; + + @override + String get emptyChatMessagesText => 'Todavía no hay charlas aquí...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 Respuesta'; + return '$replyCount Respuestas'; + } + + @override + String get connectedLabel => 'Conectado'; + + @override + String get disconnectedLabel => 'Desconectado'; + + @override + String get reconnectingLabel => 'Reconectando...'; + + @override + String get alsoSendAsDirectMessageLabel => + 'Enviar también como mensaje directo'; + + @override + String get addACommentOrSendLabel => 'Añadir un comentario o enviar'; + + @override + String get searchGifLabel => 'Búsqueda de GIFs'; + + @override + String get writeAMessageLabel => 'Escribir un mensaje'; + + @override + String get instantCommandsLabel => 'Mandos instantáneos'; + + @override + String fileTooLargeAfterCompressionError(double limitInMB) => + 'El archivo es demasiado grande para descargarlo. ' + 'El tamaño máximo del archivo es de $limitInMB MB. ' + 'Intentamos comprimirlo, pero no fue suficiente.'; + + @override + String fileTooLargeError(double limitInMB) => + 'El archivo es demasiado grande para descargarlo. ' + 'El límite de tamaño de los archivos es de $limitInMB MB.'; + + @override + String emojiMatchingQueryText(String query) => + 'Emoji que corresponde a "$query"'; + + @override + String get addAFileLabel => 'Añadir un archivo'; + + @override + String get photoFromCameraLabel => 'Foto de la cámara'; + + @override + String get uploadAFileLabel => 'Transferir un archivo'; + + @override + String get uploadAPhotoLabel => 'Subir una foto'; + + @override + String get uploadAVideoLabel => 'Subir una vídeo'; + + @override + String get videoFromCameraLabel => 'Vídeo de la cámara'; + + @override + String get okLabel => 'Vale'; + + @override + String get somethingWentWrongError => 'Algo ha salido mal'; + + @override + String get addMoreFilesLabel => 'Añadir más archivos'; + + @override + String get enablePhotoAndVideoAccessMessage => + 'Por favor, permita el acceso a sus fotos' + '\ny vídeos para que puedas compartirlos con sus amigos.'; + + @override + String get allowGalleryAccessMessage => 'Permitir el acceso a su galería'; + + @override + String get flagMessageLabel => 'Reportar un mensaje'; + + @override + String get flagMessageQuestion => + '¿Quiere enviar una copia de este mensaje a un' + '\nmoderador para una mayor investigación?'; + + @override + String get flagLabel => 'REPORTAR'; + + @override + String get cancelLabel => 'CANCELAR'; + + @override + String get flagMessageSuccessfulLabel => 'Mensaje reportado'; + + @override + String get flagMessageSuccessfulText => + 'Este mensaje ha sido reportado a un moderador.'; + + @override + String get deleteLabel => 'BORRAR'; + + @override + String get deleteMessageLabel => 'Borrar el mensaje'; + + @override + String get deleteMessageQuestion => + '¿Estás seguro de que quieres borrar este\nmensaje de forma permanente?'; + + @override + String get operationCouldNotBeCompletedText => + 'La operación no pudo completarse.'; + + @override + String get replyLabel => 'Respuesta'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'Desfijar a la conversación'; + return 'Fijar a la conversación'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'Reintentar borrar el mensaje'; + return 'Borrar el mensaje'; + } + + @override + String get copyMessageLabel => 'Copiar el mensaje'; + + @override + String get editMessageLabel => 'Editar el mensaje'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'Reenviar el mensaje modificado'; + return 'Enviar de vuelta'; + } + + @override + String get photosLabel => 'Fotos'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'hoy'; + } else if (date == yesterday) { + return 'ayer'; + } else { + return 'el ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Enviado ${_getDay(date)} a ${Jiffy(time.toLocal()).format('HH:mm')}'; + + @override + String get todayLabel => 'Hoy'; + + @override + String get yesterdayLabel => 'Ayer'; + + @override + String get channelIsMutedText => 'El canal está cortado'; + + @override + String get noTitleText => 'Sin título'; + + @override + String get letsStartChattingLabel => '¡Empecemos a charlar!'; + + @override + String get sendingFirstMessageLabel => + '¿Qué le parece enviar su primer mensaje a un amigo?'; + + @override + String get startAChatLabel => 'Iniciar una discusión'; + + @override + String get loadingChannelsError => 'Error al cargar los canales'; + + @override + String get deleteConversationLabel => 'Borrar la conversación'; + + @override + String get deleteConversationQuestion => + '¿Estás seguro de que quieres borrar esta conversación?'; + + @override + String get streamChatLabel => 'Stream Chat'; + + @override + String get searchingForNetworkText => 'Búsqueda en la red'; + + @override + String get offlineLabel => 'Sin conexión...'; + + @override + String get tryAgainLabel => 'Inténtalo de nuevo'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 Membre'; + return '$count Membres'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 En línea'; + return '$count En línea'; + } + + @override + String get viewInfoLabel => 'Ver información'; + + @override + String get leaveGroupLabel => 'Dejar el Grupo'; + + @override + String get leaveLabel => 'DEJAR'; + + @override + String get leaveConversationLabel => 'Dejar la conversación'; + + @override + String get leaveConversationQuestion => + '¿Estás seguro de que quieres dejar esta conversación?'; + + @override + String get showInChatLabel => 'Mostrar en el chat'; + + @override + String get saveImageLabel => 'Guardar la imagen'; + + @override + String get saveVideoLabel => 'Guardar el vídeo'; + + @override + String get uploadErrorLabel => 'ERROR DE TRANSFERENCIA'; + + @override + String get giphyLabel => 'Giphy'; + + @override + String get shuffleLabel => 'Mezclar'; + + @override + String get sendLabel => 'Enviar'; + + @override + String get withText => 'con'; + + @override + String get inText => 'en'; + + @override + String get youText => 'Usted'; + + @override + String get ofText => 'de'; + + @override + String get fileText => 'Archivo'; + + @override + String get replyToMessageLabel => 'Responder al Mensaje'; +} From d54bb5406a3edb8da201fe469d9210e603c9fa70 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Thu, 29 Jul 2021 15:22:44 -0400 Subject: [PATCH 007/165] fix some typos in french translations --- .../lib/src/stream_chat_localizations_fr.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 1bbfbc5a..9d8586d5 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -31,7 +31,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { if (users.length == 1) { return "${first.name} est en train d'écrire"; } - return "${first.name} and ${users.length - 1} sont entrain d'écrire"; + return "${first.name} et ${users.length - 1} sont entrain d'écrire"; } @override @@ -49,7 +49,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { required int remaining, required int total, }) => - 'Uploading $remaining/$total ...'; + 'Transfert en cours $remaining/$total ...'; @override String pinnedByUserText({ @@ -206,8 +206,8 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { @override String togglePinUnpinText({required bool pinned}) { - if (pinned) return 'Détacher de la conversation'; - return 'Attacher à la conversation'; + if (pinned) return 'Décrocher de la conversation'; + return 'Épingler à la discussion'; } @override @@ -311,7 +311,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get viewInfoLabel => 'Voir les informations'; @override - String get leaveGroupLabel => 'Quitter le Group'; + String get leaveGroupLabel => 'Quitter le Groupe'; @override String get leaveLabel => 'QUITTER'; @@ -324,7 +324,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { 'Etes-vous sûr de vouloir quitter cette conversation ?'; @override - String get showInChatLabel => 'Montrer dans le Chat'; + String get showInChatLabel => 'Montrer dans la Discussion'; @override String get saveImageLabel => "Sauvegarder l'image"; From 462f9c8f0538837a6a0e9459a2c1effed97d757a Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Thu, 29 Jul 2021 15:51:45 -0400 Subject: [PATCH 008/165] fix docs: typo mentioning french --- .../lib/src/stream_chat_localizations_es.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index a3387c83..424d95e2 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -1,8 +1,8 @@ part of 'stream_chat_localizations.dart'; -/// The translations for French (`fr`). +/// The translations for Spanish (`es`). class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { - /// Create an instance of the translation bundle for French. + /// Create an instance of the translation bundle for Spanish. const StreamChatLocalizationsEs({String localeName = 'es'}) : super(localeName: localeName); From b05a822d601a6bc42cad20e80c56a7a2ba3e37db Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 30 Jul 2021 10:54:20 +0200 Subject: [PATCH 009/165] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jc Miñarro --- .../lib/src/stream_chat_localizations_es.dart | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 424d95e2..75bed1e3 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -7,7 +7,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { : super(localeName: localeName); @override - String get launchUrlError => 'No se puede lanzar la url'; + String get launchUrlError => 'No se pudo abrir la url'; @override String get loadingUsersError => 'Error de carga del usuario'; @@ -16,7 +16,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get noUsersLabel => 'No hay usuarios actualmente'; @override - String get retryLabel => 'Inténtalo de nuevo'; + String get retryLabel => 'Inténtelo de nuevo'; @override String get userLastOnlineText => 'Última vez en línea'; @@ -42,7 +42,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String threadReplyCountText(int count) => - '$count Respuestas al hilo de discusión'; + '$count respuestas al hilo de discusión'; @override String attachmentsUploadProgressText({ @@ -68,7 +68,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get genericErrorText => 'Hubo un problema'; @override - String get loadingMessagesError => 'Mensajes de error de carga'; + String get loadingMessagesError => 'Hubo un error mientras se cargaba el mensaje'; @override String resultCountText(int count) => '$count resultados'; @@ -87,8 +87,8 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String threadSeparatorText(int replyCount) { - if (replyCount == 1) return '1 Respuesta'; - return '$replyCount Respuestas'; + if (replyCount == 1) return '1 respuesta'; + return '$replyCount respuestas'; } @override @@ -114,7 +114,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get writeAMessageLabel => 'Escribir un mensaje'; @override - String get instantCommandsLabel => 'Mandos instantáneos'; + String get instantCommandsLabel => 'Comandos instantáneos'; @override String fileTooLargeAfterCompressionError(double limitInMB) => @@ -225,7 +225,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { if (isUpdateFailed) return 'Reenviar el mensaje modificado'; - return 'Enviar de vuelta'; + return 'Reenviar'; } @override @@ -249,7 +249,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String sentAtText({required DateTime date, required DateTime time}) => - 'Enviado ${_getDay(date)} a ${Jiffy(time.toLocal()).format('HH:mm')}'; + 'Enviado el ${_getDay(date)} a las ${Jiffy(time.toLocal()).format('HH:mm')}'; @override String get todayLabel => 'Hoy'; @@ -258,7 +258,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get yesterdayLabel => 'Ayer'; @override - String get channelIsMutedText => 'El canal está cortado'; + String get channelIsMutedText => 'El canal está silenciado'; @override String get noTitleText => 'Sin título'; @@ -271,7 +271,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { '¿Qué le parece enviar su primer mensaje a un amigo?'; @override - String get startAChatLabel => 'Iniciar una discusión'; + String get startAChatLabel => 'Iniciar una conversación'; @override String get loadingChannelsError => 'Error al cargar los canales'; @@ -287,18 +287,18 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get streamChatLabel => 'Stream Chat'; @override - String get searchingForNetworkText => 'Búsqueda en la red'; + String get searchingForNetworkText => 'Buscando red'; @override String get offlineLabel => 'Sin conexión...'; @override - String get tryAgainLabel => 'Inténtalo de nuevo'; + String get tryAgainLabel => 'Inténtelo de nuevo'; @override String membersCountText(int count) { - if (count == 1) return '1 Membre'; - return '$count Membres'; + if (count == 1) return '1 miembro'; + return '$count miembros'; } @override @@ -311,17 +311,17 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get viewInfoLabel => 'Ver información'; @override - String get leaveGroupLabel => 'Dejar el Grupo'; + String get leaveGroupLabel => 'Salir del Grupo'; @override - String get leaveLabel => 'DEJAR'; + String get leaveLabel => 'SALIR'; @override - String get leaveConversationLabel => 'Dejar la conversación'; + String get leaveConversationLabel => 'Salir de la conversación'; @override String get leaveConversationQuestion => - '¿Estás seguro de que quieres dejar esta conversación?'; + '¿Estás seguro de que quiere salir de esta conversación?'; @override String get showInChatLabel => 'Mostrar en el chat'; From b989fbc3ad3908b690705d08bd6bc21c432ed6e3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 30 Jul 2021 11:48:23 +0200 Subject: [PATCH 010/165] fix(llc): use channel partial update for slowmode --- .../stream_chat/lib/src/client/channel.dart | 4 ++-- .../stream_chat/lib/src/client/client.dart | 4 ++-- .../lib/src/core/api/channel_api.dart | 24 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index fb579a56..1f6d3edd 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -834,7 +834,7 @@ class Channel { } /// Enable slow mode - Future enableSlowMode({ + Future enableSlowMode({ required int cooldownInterval, }) async { _checkInitialized(); @@ -842,7 +842,7 @@ class Channel { } /// Disable slow mode - Future disableSlowMode() async { + Future disableSlowMode() async { _checkInitialized(); return _client.disableSlowdown(id!, type); } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 7b5cbb35..788bc9cf 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1248,7 +1248,7 @@ class StreamChatClient { ); /// Enables slow mode - Future enableSlowdown( + Future enableSlowdown( String channelId, String channelType, int cooldown, @@ -1260,7 +1260,7 @@ class StreamChatClient { ); /// Disables slow mode - Future disableSlowdown( + Future disableSlowdown( String channelId, String channelType, ) async => diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 31a64cf8..e8782feb 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -124,32 +124,32 @@ class ChannelApi { } /// Enable slowdown - Future enableSlowdown( + Future enableSlowdown( String channelId, String channelType, int cooldown, ) async { - final response = await _client.post( - _getChannelUrl(channelId, channelType), - data: { + final response = await updateChannelPartial( + channelId, + channelType, + set: { 'cooldown': cooldown, }, ); - return UpdateChannelResponse.fromJson(response.data); + return response; } /// Disable slowdown - Future disableSlowdown( + Future disableSlowdown( String channelId, String channelType, ) async { - final response = await _client.post( - _getChannelUrl(channelId, channelType), - data: { - 'cooldown': 0, - }, + final response = await updateChannelPartial( + channelId, + channelType, + unset: ['cooldown'], ); - return UpdateChannelResponse.fromJson(response.data); + return response; } /// Accept invitation to the channel From 1e778fa3d312a6add15263ab896351e1598ba27c Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Mon, 2 Aug 2021 10:18:14 -0400 Subject: [PATCH 011/165] update docusaurus: adding_localization --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 7e6cb7e2..9eb4dd68 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -25,6 +25,7 @@ At the moment we support the following languages: - [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart) - [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart) - [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart) +- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages. @@ -62,6 +63,7 @@ class MyApp extends StatelessWidget { Locale('hi'), Locale('fr'), Locale('it'), + Locale('es'), ], // Add GlobalStreamChatLocalizations.delegates localizationsDelegates: GlobalStreamChatLocalizations.delegates, @@ -130,6 +132,7 @@ Here is an example of how that would look like: Locale('hi'), Locale('fr'), Locale('it'), + Locale('es'), ], // locales are the locales of the device // supportedLocales are the app supported locales @@ -172,5 +175,6 @@ Example: nb fr it + es ``` From ab1b65846dcc39cecee75e558fd7e60e8edf79b8 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Mon, 2 Aug 2021 10:19:38 -0400 Subject: [PATCH 012/165] apply review: add column --- .../lib/src/stream_chat_localizations.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index d388059d..a6a66b93 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -21,7 +21,13 @@ part 'stream_chat_localizations_hi.dart'; /// See also: /// /// * [getStreamChatTranslation], whose documentation describes these values. -const kStreamChatSupportedLanguages = {'en', 'hi', 'fr', 'it', 'es'}; +const kStreamChatSupportedLanguages = { + 'en', + 'hi', + 'fr', + 'it', + 'es', +}; /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. /// From e14258c3906554b659ad73439f27b9273308ad16 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Mon, 2 Aug 2021 15:19:12 -0400 Subject: [PATCH 013/165] feat: support for japanese --- .../lib/src/stream_chat_localizations.dart | 5 + .../lib/src/stream_chat_localizations_ja.dart | 360 ++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index a6a66b93..e65aab1f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -11,6 +11,8 @@ part 'stream_chat_localizations_fr.dart'; part 'stream_chat_localizations_it.dart'; +part 'stream_chat_localizations_ja.dart'; + part 'stream_chat_localizations_hi.dart'; /// The set of supported languages, as language code strings. @@ -27,6 +29,7 @@ const kStreamChatSupportedLanguages = { 'fr', 'it', 'es', + 'ja', }; /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. @@ -59,6 +62,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { return const StreamChatLocalizationsIt(); case 'es': return const StreamChatLocalizationsEs(); + case 'ja': + return const StreamChatLocalizationsJa(); } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart new file mode 100644 index 00000000..96ffec25 --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -0,0 +1,360 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for English (`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 => 'ユーアーレルの起動ができない'; + + @override + String get loadingUsersError => 'ユーザーの読み込みエラー'; + + @override + String get noUsersLabel => '現在、ユーザーはいません。'; + + @override + String get retryLabel => '再試行'; + + @override + String get userLastOnlineText => '前回のオンライン'; + + @override + String get userOnlineText => 'オンライン'; + + @override + String userTypingText(Iterable 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) { + if (replyCount == 1) return '1 回答'; + return '$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)} at ${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) { + if (count == 1) return '1人のメンバー'; + return '$count人のメンバー'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1オンライン'; + return '$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 => 'は'; + + @override + String get youText => '君'; + + @override + String get ofText => 'の';//TODO: break + // galleryPaginationText( + // {required int currentPage, required int totalPages}) + + @override + String get fileText => 'ファイル'; + + @override + String get replyToMessageLabel => 'メッセージに返信'; +} From e44a4ce3a1dc88ba89137f4c3baa3fcaabb4d794 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Mon, 2 Aug 2021 15:19:55 -0400 Subject: [PATCH 014/165] update fr translation: discussion->conversation --- .../lib/src/stream_chat_localizations_fr.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 9d8586d5..373558b9 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -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,7 @@ 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'; From 59b4f5568c77ccae4b56d919216bd82058050ffb Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Mon, 2 Aug 2021 15:30:58 -0400 Subject: [PATCH 015/165] breaking change: ofText->galleryPaginationText --- .../lib/src/gallery_footer.dart | 6 ++--- .../lib/src/localization/translations.dart | 9 ++++--- .../test/src/default_translations_test.dart | 2 +- .../example/lib/add_new_lang.dart | 4 +++- .../lib/src/stream_chat_localizations_en.dart | 4 +++- .../lib/src/stream_chat_localizations_es.dart | 7 ++++-- .../lib/src/stream_chat_localizations_fr.dart | 4 +++- .../lib/src/stream_chat_localizations_hi.dart | 8 ++++--- .../lib/src/stream_chat_localizations_it.dart | 4 +++- .../lib/src/stream_chat_localizations_ja.dart | 24 +++++++------------ .../test/translations_test.dart | 2 +- 11 files changed, 42 insertions(+), 32 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index 03d575ae..6df301d3 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -136,9 +136,9 @@ class _GalleryFooterState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - '${widget.currentPage + 1} ' - '${context.translations.ofText} ' - '${widget.totalPages}', + context.translations.galleryPaginationText( + currentPage: widget.currentPage, + totalPages: widget.totalPages), style: galleryFooterThemeData.titleTextStyle, ), ], diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index d6e5add0..d3b3e8cb 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -296,8 +296,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; @@ -657,7 +658,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'; diff --git a/packages/stream_chat_flutter/test/src/default_translations_test.dart b/packages/stream_chat_flutter/test/src/default_translations_test.dart index e78c0115..14a4a318 100644 --- a/packages/stream_chat_flutter/test/src/default_translations_test.dart +++ b/packages/stream_chat_flutter/test/src/default_translations_test.dart @@ -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); }); diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 1412f4e0..2f373691 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -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'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 5041d398..13de9729 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -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'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 75bed1e3..f6cd5e7c 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -68,7 +68,8 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get genericErrorText => 'Hubo un problema'; @override - String get loadingMessagesError => 'Hubo un error mientras se cargaba el mensaje'; + String get loadingMessagesError => + 'Hubo un error mientras se cargaba el mensaje'; @override String resultCountText(int count) => '$count resultados'; @@ -354,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'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 373558b9..2331b522 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -354,7 +354,9 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get youText => 'Vous'; @override - String galleryPaginationText({required int currentPage, required int totalPages}) => '${currentPage+1} de $totalPages'; + String galleryPaginationText( + {required int currentPage, required int totalPages}) => + '${currentPage + 1} de $totalPages'; @override String get fileText => 'Fichier'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 850e481b..11d7d486 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -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 => 'फ़ाइल'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 8b2e1692..313e1e26 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -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'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 96ffec25..6522fbf7 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -177,8 +177,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'フラグ付メッセージ'; @override - String get flagMessageSuccessfulText => - 'このメッセージはモデレーターに報告されました。'; + String get flagMessageSuccessfulText => 'このメッセージはモデレーターに報告されました。'; @override String get deleteLabel => '消す'; @@ -187,13 +186,11 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'メッセージを削除する '; @override - String get deleteMessageQuestion => - 'このメッセージ' + String get deleteMessageQuestion => 'このメッセージ' '\nを完全に削除してもよろしいですか?'; @override - String get operationCouldNotBeCompletedText => - '操作を完了できませんでした。'; + String get operationCouldNotBeCompletedText => '操作を完了できませんでした。'; @override String get replyLabel => '返信'; @@ -261,8 +258,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'さあ、チャットを始めよう'; @override - String get sendingFirstMessageLabel => - 'どのように友人にあなたの最初のメッセージを送ることについてはどうですか?'; + String get sendingFirstMessageLabel => 'どのように友人にあなたの最初のメッセージを送ることについてはどうですか?'; @override String get startAChatLabel => 'チャットを開始する'; @@ -274,8 +270,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get deleteConversationLabel => '会話を削除する'; @override - String get deleteConversationQuestion => - 'この会話を削除してもよろしいですか?'; + String get deleteConversationQuestion => 'この会話を削除してもよろしいですか?'; @override String get streamChatLabel => 'ストリームチャット'; @@ -314,8 +309,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get leaveConversationLabel => '会話を離れる'; @override - String get leaveConversationQuestion => - 'この会話を離れてもよろしいですか?'; + String get leaveConversationQuestion => 'この会話を離れてもよろしいですか?'; @override String get showInChatLabel => 'チャットで表示'; @@ -348,9 +342,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get youText => '君'; @override - String get ofText => 'の';//TODO: break - // galleryPaginationText( - // {required int currentPage, required int totalPages}) + String galleryPaginationText( + {required int currentPage, required int totalPages}) => + '$totalPagesの${currentPage + 1}'; @override String get fileText => 'ファイル'; diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index 0e621159..c7314a58 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -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); }); From 8ca7dab6da9d6f235f95dd476874b076c5baf988 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Mon, 2 Aug 2021 15:36:43 -0400 Subject: [PATCH 016/165] update docs --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 9eb4dd68..3a81187a 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -26,7 +26,7 @@ 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) 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. ### Add dependency @@ -64,6 +64,7 @@ class MyApp extends StatelessWidget { Locale('fr'), Locale('it'), Locale('es'), + Locale('ja'), ], // Add GlobalStreamChatLocalizations.delegates localizationsDelegates: GlobalStreamChatLocalizations.delegates, @@ -133,6 +134,7 @@ Here is an example of how that would look like: Locale('fr'), Locale('it'), Locale('es'), + Locale('ja'), ], // locales are the locales of the device // supportedLocales are the app supported locales @@ -176,5 +178,6 @@ Example: fr it es + ja ``` From 95e3467ac49a19aea1e9cc3f8cf6af48e59f38cd Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Mon, 2 Aug 2021 16:50:57 -0400 Subject: [PATCH 017/165] support for korean --- .../lib/src/stream_chat_localizations.dart | 5 + .../lib/src/stream_chat_localizations_ko.dart | 360 ++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index e65aab1f..4094dcaa 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -13,6 +13,8 @@ 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. @@ -30,6 +32,7 @@ const kStreamChatSupportedLanguages = { 'it', 'es', 'ja', + 'ko' }; /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. @@ -64,6 +67,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { return const StreamChatLocalizationsEs(); case 'ja': return const StreamChatLocalizationsJa(); + case 'ko': + return const StreamChatLocalizationsKo(); } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart new file mode 100644 index 00000000..6fe285b7 --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -0,0 +1,360 @@ +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 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, + }) => + '${total}mb 중 ${remaining}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) { + if (replyCount == 1) return '1 회신합니다.'; + return '$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) { + if (count == 1) return '회원 1명입니다.'; + return '$count 구성원'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 온라인입니다.'; + return '$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 => '에 있습니다.'; + + @override + String get youText => '너'; + + @override + String galleryPaginationText( + {required int currentPage, required int totalPages}) => + '$totalPages장 ${currentPage + 1}장입니다'; + //11장 3장. + + @override + String get fileText => '파일'; + + @override + String get replyToMessageLabel => '메시지에 회신합니다.'; +} From bd8e4675c66595c0187ae2cf763166fbaf115588 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Wed, 4 Aug 2021 09:06:56 -0400 Subject: [PATCH 018/165] update korean based on Eric's review --- .../lib/src/stream_chat_localizations_ko.dart | 158 +++++++++--------- 1 file changed, 75 insertions(+), 83 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 6fe285b7..689544c7 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -7,48 +7,48 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { : super(localeName: localeName); @override - String get launchUrlError => 'URL을 시작할 수 없습니다.'; + String get launchUrlError => 'URL을 시작할 수 없습니다'; @override String get loadingUsersError => '사용자를 로드하는 중 오류 발생'; @override - String get noUsersLabel => '현재 사용자가 없습니다.'; + String get noUsersLabel => '현재 사용자가 없습니다'; @override - String get retryLabel => '다시 시도하십시오.'; + String get retryLabel => '다시 시도하십시오'; @override - String get userLastOnlineText => '마지막 온라인입니다.'; + String get userLastOnlineText => '마지막 온라인입니다'; @override - String get userOnlineText => '온라인.'; + String get userOnlineText => '온라인'; @override String userTypingText(Iterable users) { if (users.isEmpty) return ''; final first = users.first; if (users.length == 1) { - return '${first.name} 타이핑하고 있어요.'; + return '${first.name} 타이핑중'; } - return '${first.name}스와 ${users.length - 1}명이 타자를 치고 있습니다'; + return '${first.name}하고 ${users.length - 1}명 타이핑중'; } @override - String get threadReplyLabel => '스레드 응답입니다.'; + String get threadReplyLabel => '스레드 응답입니다'; @override - String get onlyVisibleToYouText => '당신만 볼 수 있습니다.'; + String get onlyVisibleToYouText => '당신만 볼 수 있습니다'; @override - String threadReplyCountText(int count) => '$count스레드 응답입니다.'; + String threadReplyCountText(int count) => '$count스레드 답장'; @override String attachmentsUploadProgressText({ required int remaining, required int total, }) => - '${total}mb 중 ${remaining}mb를 업로드하는 중입니다...'; + '$remaining/${total}mb를 업로드중...'; @override String pinnedByUserText({ @@ -56,60 +56,59 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { required User currentUser, }) { final pinnedByCurrentUser = currentUser.id == pinnedBy.id; - if (pinnedByCurrentUser) return '당신에 의해 고정됩니다.'; - return '${pinnedBy.name} 핀으로 고정했습니다.'; + if (pinnedByCurrentUser) return '당신의 핀'; + return '${pinnedBy.name}의 핀'; } @override - String get emptyMessagesText => '현재 메시지가 없습니다.'; + String get emptyMessagesText => '현재 메시지가 없습니다'; @override - String get genericErrorText => '뭔가 잘못됐어요'; + String get genericErrorText => '뭔가 잘못됐습니다'; @override - String get loadingMessagesError => '메시지를 로드하는 동안 오류가 발생했습니다.'; + String get loadingMessagesError => '메시지를 로드하는 동안 오류가 발생했습니다'; @override - String resultCountText(int count) => '$count개 결과입니다.'; + String resultCountText(int count) => '$count개의 결과'; @override String get messageDeletedText => '이 메시지는 삭제되었습니다.'; @override - String get messageDeletedLabel => '메시지가 삭제되었습니다.'; + String get messageDeletedLabel => '메시지가 삭제되었습니다'; @override - String get messageReactionsLabel => '메시지에 대한 응답.'; + String get messageReactionsLabel => '메시지에 대한 응답'; @override String get emptyChatMessagesText => '아직 채팅이 없습니다...'; @override String threadSeparatorText(int replyCount) { - if (replyCount == 1) return '1 회신합니다.'; - return '$replyCount개의 응답입니다.'; + return '$replyCount개의 답장'; } @override - String get connectedLabel => '연결된'; + String get connectedLabel => '연결중'; @override - String get disconnectedLabel => '연결이 끊겼습니다.'; + String get disconnectedLabel => '연결이 끊겼습니다'; @override String get reconnectingLabel => '다시 연결하는 중...'; @override - String get alsoSendAsDirectMessageLabel => '다이렉트 메시지로도 보냅니다.'; + String get alsoSendAsDirectMessageLabel => '다이렉트 메시지로도 보냅니다'; @override - String get addACommentOrSendLabel => '주석을 추가하거나 보냅니다.'; + String get addACommentOrSendLabel => '주석을 추가하거나 보냅니다'; @override - String get searchGifLabel => 'GIF를 검색합니다.'; + String get searchGifLabel => 'GIF 검색'; @override - String get writeAMessageLabel => '메시지를 쓰세요.'; + String get writeAMessageLabel => '메시지 쓰기'; @override String get instantCommandsLabel => '인스턴트 커맨즈'; @@ -125,88 +124,85 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { '파일이 너무 커서 업로드할 수 없습니다. 파일 크기 제한은 ${limitInMB}MB입니다.'; @override - String emojiMatchingQueryText(String query) => '"$query"과 일치하는 이모티콘입니다.'; + String emojiMatchingQueryText(String query) => '"$query"과 일치하는 이모티콘입니다'; @override - String get addAFileLabel => '파일을 추가합니다.'; + String get addAFileLabel => '파일을 추가함'; @override - String get photoFromCameraLabel => '카메라에서 찍은 사진입니다.'; + String get photoFromCameraLabel => '카메라에서 찍은 사진'; @override - String get uploadAFileLabel => '파일을 업로드합니다.'; + String get uploadAFileLabel => '파일을 업로드함'; @override - String get uploadAPhotoLabel => '사진을 업로드합니다.'; + String get uploadAPhotoLabel => '사진을 업로드함'; @override - String get uploadAVideoLabel => '비디오를 업로드하세요'; + String get uploadAVideoLabel => '비디오를 업로드함'; @override - String get videoFromCameraLabel => '카메라의 비디오입니다.'; + String get videoFromCameraLabel => '카메라의 비디오.'; @override - String get okLabel => '알았지'; + String get okLabel => 'ㅇㅋ'; @override - String get somethingWentWrongError => '뭔가 잘못됐어요'; + String get somethingWentWrongError => '뭔가 잘못됐습느다'; @override - String get addMoreFilesLabel => '파일을 추가합니다.'; + String get addMoreFilesLabel => '파일을 추가함'; @override String get enablePhotoAndVideoAccessMessage => '친구와 공유할 수 있도록 사진과' '\n동영상에 액세스할 수 있도록 설정하십시오.'; @override - String get allowGalleryAccessMessage => '갤러리에 대한 액세스를 허용합니다.'; + String get allowGalleryAccessMessage => '갤러리에 대한 액세스를 허용합니다'; @override - String get flagMessageLabel => '플래그 메시지'; + String get flagMessageLabel => ' 메시지를 플래그함'; @override String get flagMessageQuestion => '추가 조사를 위해 진행자에게 이 메시지의 복사본을 전송하시겠습니까?'; @override - String get flagLabel => '플래그를 지정합니다.'; + String get flagLabel => '플래그함'; @override - String get cancelLabel => '취소하십시오.'; + String get cancelLabel => '취소'; @override - String get flagMessageSuccessfulLabel => '메시지에 플래그가 지정되었습니다.'; + String get flagMessageSuccessfulLabel => '메시지에 플래그가 지정되었습니다'; @override - String get flagMessageSuccessfulText => - '메시지가 진행자에게 보고되었습니다.'; + String get flagMessageSuccessfulText => '메시지가 진행자에게 보고되었습니다.'; @override - String get deleteLabel => '삭제합니다.'; + String get deleteLabel => '삭제'; @override String get deleteMessageLabel => '메시지를 삭제합니다.'; @override - String get deleteMessageQuestion => - '이 메시지를 완전히 삭제하시겠습니까?'; + String get deleteMessageQuestion => '이 메시지를 완전히 삭제하시겠습니까?'; @override - String get operationCouldNotBeCompletedText => - '작업을 완료할 수 없습니다.'; + String get operationCouldNotBeCompletedText => '작업을 완료할 수 없습니다.'; @override String get replyLabel => '답글'; @override String togglePinUnpinText({required bool pinned}) { - if (pinned) return '대화에서 연결을 해제합니다.'; - return '대화에 고정합니다.'; + if (pinned) return '대화의 핀을 분리합니다'; + return '대화에 고정합니다'; } @override String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { - if (isDeleteFailed) return '메시지 삭제를 다시 시도하십시오.'; - return '메시지를 삭제합니다.'; + if (isDeleteFailed) return '메시지 삭제를 다시 시도합니다'; + return '메시지를 삭제합니다'; } @override @@ -222,7 +218,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { } @override - String get photosLabel => '사진들'; + String get photosLabel => '사진'; String _getDay(DateTime dateTime) { final now = DateTime.now(); @@ -232,23 +228,23 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { final date = DateTime(dateTime.year, dateTime.month, dateTime.day); if (date == today) { - return '오늘이요'; + return '오늘'; } else if (date == yesterday) { - return '어제요'; + return '어제'; } else { - return '${Jiffy(date).MMMd}입니다.'; + return '${Jiffy(date).MMMd}에'; } } @override String sentAtText({required DateTime date, required DateTime time}) => - '${_getDay(date)} 오전 ${Jiffy(time.toLocal()).format('HH:mm')}에 보냈습니다'; + '${_getDay(date)} ${Jiffy(time.toLocal()).format('HH:mm')}에 보냈습니다'; @override - String get todayLabel => '오늘이요'; + String get todayLabel => '오늘'; @override - String get yesterdayLabel => '어제요'; + String get yesterdayLabel => '어제'; @override String get channelIsMutedText => '채널이 음소거됩니다.'; @@ -260,8 +256,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => '채팅 시작해요!'; @override - String get sendingFirstMessageLabel => - '친구에게 당신의 첫 번째 메시지를 보내는 것은 어떤가요?'; + String get sendingFirstMessageLabel => '친구에게 첫 메시지를 보내 볼까요?'; @override String get startAChatLabel => '대화를 시작합니다.'; @@ -273,11 +268,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get deleteConversationLabel => '대화를 삭제합니다.'; @override - String get deleteConversationQuestion => - '이 대화를 삭제하시겠습니까?'; + String get deleteConversationQuestion => '대화를 삭제하시겠습니까?'; @override - String get streamChatLabel => '채팅을 스트리밍합니다.'; + String get streamChatLabel => '스트림 채팅'; @override String get searchingForNetworkText => '네트워크를 검색하는 중입니다.'; @@ -286,35 +280,32 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get offlineLabel => '오프라인...'; @override - String get tryAgainLabel => '다시 한 번 해 봐!'; + String get tryAgainLabel => '다시 시도합니다'; @override String membersCountText(int count) { - if (count == 1) return '회원 1명입니다.'; - return '$count 구성원'; + return '$count명'; } @override String watchersCountText(int count) { - if (count == 1) return '1 온라인입니다.'; - return '$count 온라인입니다'; + return '$count명이 온라인'; } @override - String get viewInfoLabel => '정보를 봅니다.'; + String get viewInfoLabel => '정보를 보기'; @override String get leaveGroupLabel => '그룹을 떠납니다.'; @override - String get leaveLabel => '떠나요'; + String get leaveLabel => '떠나다'; @override String get leaveConversationLabel => '대화에서 떠납니다.'; @override - String get leaveConversationQuestion => - '정말 이 대화에서 나가시겠습니까?'; + String get leaveConversationQuestion => '정말 이 대화에서 나가시겠습니까?'; @override String get showInChatLabel => '채팅에 표시합니다.'; @@ -326,31 +317,32 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get saveVideoLabel => '비디오를 저장합니다.'; @override - String get uploadErrorLabel => '업로드 오류입니다.'; + String get uploadErrorLabel => '업로드 오류'; @override - String get giphyLabel => '지피요'; + String get giphyLabel => '지피'; @override - String get shuffleLabel => '섞으세요'; + String get shuffleLabel => '섞기'; @override - String get sendLabel => '보냅니다'; + String get sendLabel => '보내기'; @override - String get withText => '함께요'; + String get withText => '함께'; @override - String get inText => '에 있습니다.'; + String get inText => '에'; @override - String get youText => '너'; + String get youText => '고객님'; + // This is the word for 'customer' or 'user' because saying 'you' directly is too informal and rude @override String galleryPaginationText( {required int currentPage, required int totalPages}) => - '$totalPages장 ${currentPage + 1}장입니다'; - //11장 3장. + '${currentPage + 1} / $totalPages'; + //3 / 11 @override String get fileText => '파일'; From f311c13e4bc621b5e5a5b973861fb1be019d1c18 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Wed, 4 Aug 2021 09:20:28 -0400 Subject: [PATCH 019/165] update japanese based on Eric's input --- .../lib/src/stream_chat_localizations_ja.dart | 103 +++++++++--------- 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 6522fbf7..a30bfe2f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -1,16 +1,16 @@ part of 'stream_chat_localizations.dart'; -/// The translations for English (`ja`). +/// 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 => 'ユーアーレルの起動ができない'; + String get launchUrlError => 'URLの起動ができません'; @override - String get loadingUsersError => 'ユーザーの読み込みエラー'; + String get loadingUsersError => 'ユーザーの読み込みができません'; @override String get noUsersLabel => '現在、ユーザーはいません。'; @@ -29,26 +29,26 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { if (users.isEmpty) return ''; final first = users.first; if (users.length == 1) { - return '${first.name}がタイプしている'; + return '${first.name}が入力しています'; } - return '${first.name}とあと${users.length - 1}人が入力しています'; + return '${first.name}と${users.length - 1}人が入力しています'; } @override String get threadReplyLabel => 'スレッド返信'; @override - String get onlyVisibleToYouText => '自分にしか見えない'; + String get onlyVisibleToYouText => '自分しか見れません'; @override - String threadReplyCountText(int count) => '$countスレッドの返信'; + String threadReplyCountText(int count) => '$countつのスレッド返信'; @override String attachmentsUploadProgressText({ required int remaining, required int total, }) => - '$remaining/${total}mbのアップロード 。。。'; + '$remaining/${total}mbのアップロード中 。。。'; @override String pinnedByUserText({ @@ -56,21 +56,21 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { required User currentUser, }) { final pinnedByCurrentUser = currentUser.id == pinnedBy.id; - if (pinnedByCurrentUser) return 'あなたのピン留'; - return '${pinnedBy.name}のピン留'; + if (pinnedByCurrentUser) return 'あなたのピン'; + return '${pinnedBy.name}のピン'; } @override String get emptyMessagesText => '現在、メッセージはありません。'; @override - String get genericErrorText => '何かが間違っていた'; + String get genericErrorText => 'エラーが発生しました'; @override String get loadingMessagesError => 'メッセージの読み込みエラー'; @override - String resultCountText(int count) => '$countつの結果'; + String resultCountText(int count) => '$count件の結果'; @override String get messageDeletedText => 'このメッセージは削除されました。'; @@ -79,34 +79,32 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get messageDeletedLabel => 'メッセージ削除'; @override - String get messageReactionsLabel => 'メッセージに対する反応'; + String get messageReactionsLabel => 'メッセージのリアクション'; @override - String get emptyChatMessagesText => 'ここではまだ会話はありませんが。。。'; + String get emptyChatMessagesText => 'チャットがありませんが。。。'; @override - String threadSeparatorText(int replyCount) { - if (replyCount == 1) return '1 回答'; - return '$replyCount件の返信'; - } + String threadSeparatorText(int replyCount)=> '$replyCount件の返信'; + @override - String get connectedLabel => 'コネクテッド'; + String get connectedLabel => '接続しています'; @override String get disconnectedLabel => '接続切れ'; @override - String get reconnectingLabel => 'リコネクティング。。。'; + String get reconnectingLabel => '再接続中。。。'; @override - String get alsoSendAsDirectMessageLabel => 'ダイレクトメッセージでも送信可能'; + String get alsoSendAsDirectMessageLabel => 'ダイレクトメッセージでも送信'; @override String get addACommentOrSendLabel => 'コメントの追加や送信'; @override - String get searchGifLabel => '検索用GIF'; + String get searchGifLabel => 'GIFの検索'; @override String get writeAMessageLabel => 'メッセージを書く'; @@ -149,7 +147,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get okLabel => 'よし'; @override - String get somethingWentWrongError => '何かが間違っていたのだ。'; + String get somethingWentWrongError => 'エラーが発生しました'; @override String get addMoreFilesLabel => 'ファイルの追加'; @@ -158,29 +156,29 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get enablePhotoAndVideoAccessMessage => 'お友達と共有できるように、写真' '\nやビデオへのアクセスを有効にしてください。'; @override - String get allowGalleryAccessMessage => 'お客様のギャラリーへのアクセスを許可する'; + String get allowGalleryAccessMessage => 'ギャラリーへのアクセスを許可する'; @override - String get flagMessageLabel => 'メッセージフラグが'; + String get flagMessageLabel => 'メッセージをフラグする'; @override String get flagMessageQuestion => 'このメッセージのコピーを' '\nモデレーターに送って、さらに調査してもらいますか?'; @override - String get flagLabel => 'フラグが'; + String get flagLabel => 'フラグする'; @override String get cancelLabel => 'キャンセル'; @override - String get flagMessageSuccessfulLabel => 'フラグ付メッセージ'; + String get flagMessageSuccessfulLabel => 'メッセージにフラグが付けられました'; @override String get flagMessageSuccessfulText => 'このメッセージはモデレーターに報告されました。'; @override - String get deleteLabel => '消す'; + String get deleteLabel => '削除'; @override String get deleteMessageLabel => 'メッセージを削除する '; @@ -197,25 +195,25 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String togglePinUnpinText({required bool pinned}) { - if (pinned) return 'カンバセーションからピンを外す '; - return '会話へのピン'; + if (pinned) return '会話のピンを外す'; + return '会話にピンする'; } @override String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { - if (isDeleteFailed) return 'メッセージの削除を再試行してください'; + if (isDeleteFailed) return 'メッセージの削除を再試行する'; return 'メッセージを削除する'; } @override - String get copyMessageLabel => 'コピーメッセージ'; + String get copyMessageLabel => 'メッセージをコピーする'; @override - String get editMessageLabel => 'メッセージの編集'; + String get editMessageLabel => 'メッセージを編集する'; @override String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { - if (isUpdateFailed) return '編集したメッセージの再送'; + if (isUpdateFailed) return '編集したメッセージを再送する'; return '再送'; } @@ -240,7 +238,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String sentAtText({required DateTime date, required DateTime time}) => - '${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}に送信 '; + '${_getDay(date)}の${Jiffy(time.toLocal()).format('HH:mm')}に送信しました '; @override String get todayLabel => '今日'; @@ -249,16 +247,16 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get yesterdayLabel => '昨日'; @override - String get channelIsMutedText => 'チャンネルがミュートされています'; + String get channelIsMutedText => 'チャンネルが無音されています'; @override String get noTitleText => 'タイトル無し'; @override - String get letsStartChattingLabel => 'さあ、チャットを始めよう'; + String get letsStartChattingLabel => 'チャットを始めよう!'; @override - String get sendingFirstMessageLabel => 'どのように友人にあなたの最初のメッセージを送ることについてはどうですか?'; + String get sendingFirstMessageLabel => '友人に最初のメッセージを送りましょうか?'; @override String get startAChatLabel => 'チャットを開始する'; @@ -270,13 +268,13 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get deleteConversationLabel => '会話を削除する'; @override - String get deleteConversationQuestion => 'この会話を削除してもよろしいですか?'; + String get deleteConversationQuestion => '本当に会話を削除しますか?'; @override String get streamChatLabel => 'ストリームチャット'; @override - String get searchingForNetworkText => 'ネットワークの検索'; + String get searchingForNetworkText => 'ネットワークを検索中'; @override String get offlineLabel => 'オフライン。。。'; @@ -285,16 +283,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get tryAgainLabel => '再試行する'; @override - String membersCountText(int count) { - if (count == 1) return '1人のメンバー'; - return '$count人のメンバー'; - } + String membersCountText(int count) => '$count人のメンバー'; @override - String watchersCountText(int count) { - if (count == 1) return '1オンライン'; - return '$countオンライン'; - } + String watchersCountText(int count) => '$count人がオンライン'; @override String get viewInfoLabel => '情報を見る'; @@ -309,7 +301,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get leaveConversationLabel => '会話を離れる'; @override - String get leaveConversationQuestion => 'この会話を離れてもよろしいですか?'; + String get leaveConversationQuestion => '本当に会話を離れますか?'; @override String get showInChatLabel => 'チャットで表示'; @@ -336,15 +328,18 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get withText => 'と'; @override - String get inText => 'は'; + String get inText => 'に'; @override - String get youText => '君'; + String get youText => 'お客様'; + // This is the word for 'customer' or 'user' because saying 'you' directly is too informal and rude @override - String galleryPaginationText( - {required int currentPage, required int totalPages}) => - '$totalPagesの${currentPage + 1}'; + String galleryPaginationText({ + required int currentPage, + required int totalPages, + }) => + '${currentPage + 1} / $totalPages'; @override String get fileText => 'ファイル'; From 74f3e109fd02615d55dda0abd80c092db70a85a0 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Wed, 4 Aug 2021 09:59:29 -0400 Subject: [PATCH 020/165] =?UTF-8?q?update=20okLabel:=20=E3=85=87=E3=85=8B-?= =?UTF-8?q?>=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/stream_chat_localizations_ko.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 689544c7..36c578e3 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -145,7 +145,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get videoFromCameraLabel => '카메라의 비디오.'; @override - String get okLabel => 'ㅇㅋ'; + String get okLabel => '확인'; @override String get somethingWentWrongError => '뭔가 잘못됐습느다'; From 1c3927101513e9012ffb6429aeab0ecb0f2439f9 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 17:30:56 +0200 Subject: [PATCH 021/165] docs: fix links, grammar --- .../docs/Flutter/basics/introduction.mdx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docusaurus/docs/Flutter/basics/introduction.mdx b/docusaurus/docs/Flutter/basics/introduction.mdx index 756bd33f..ad1df5af 100644 --- a/docusaurus/docs/Flutter/basics/introduction.mdx +++ b/docusaurus/docs/Flutter/basics/introduction.mdx @@ -8,13 +8,14 @@ Exploring The Basics Of Stream Chat ![](../assets/sdk_title.png) -Stream Chat is a service that helps you easily build a full chat experience in your Flutter (and more) apps. +Stream Chat is a service that helps you easily build a full chat experience in your Flutter apps. +We also support a variety of other SDKs. This section of the documentation focuses on our Flutter SDK which helps you easily -ship high quality messaging experiences in apps and programs built with the [Flutter toolkit made +ship high quality messaging experiences in apps and programs built with the [Flutter toolkit by Google](https://flutter.dev). -The Stream Chat Flutter SDK comprises of four different packages to choose from ranging from ones +The Stream Chat Flutter SDK comprises five different packages to choose from, ranging from ones giving you complete control to ones that give you a rich out-of-the-box chat experience. The packages that make up the Stream Chat SDK are: @@ -30,28 +31,28 @@ reusable and customisable UI components. saving chat data locally. 5. Localizations (stream_chat_localizations): provides a set of localizations for the SDK. -We recommend building prototypes using the full UI package since it contains UI widgets already -integrated with Stream's API. [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter) -is the fastest way to get up and running using Stream chat in your app. +We recommend building prototypes using the full UI package, [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter), +since it contains UI widgets already integrated with Stream's API. Is the fastest way to get up +and running using Stream chat in your app. The Flutter SDK enables you to build any type of chat or messaging experience for Android, iOS, Web and Desktop. If you're building a very custom UI and would prefer a more lean package, -our [core package](https://pub.dev/packages/stream_chat_flutter) will be suited to this use case. Core allows you to build custom, -expressive UIs while retaining the benefits of our full Flutter SDK. -APIs for accessing and controlling users, sending messages, etc are seamlessly integrated into -this package and accessible via providers and builders. +[stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) will be suited to this +use case. Core allows you to build custom, expressive UIs while retaining the benefits of our full +Flutter SDK. APIs for accessing and controlling users, sending messages, and so forth are seamlessly integrated +into this package and accessible via providers and builders. Before going into the docs, let's take a small detour to look at how the elements of Stream Chat are structured. ### Basic Structure -There are two core elements in chat, Users and Channels. +There are two core elements in chat, Users and Channels. Channels are groups of one or more users that can message each other. In an app, you need to have a user connected to query channels. -There is no specific distinction between a chat between two people and a group chat, +There is no specific distinction between a chat with only two people and a group chat, but there is a way to create a unique chat between a certain number of people by creating a distinct channel. ![](../assets/chat_basics.png) @@ -68,7 +69,7 @@ While this is a simplistic overview of the service, the Flutter SDK handles the Before reading the docs, consider trying our [online API tour](https://getstream.io/chat/get_started/), it is a nice way to learn how the API works. -It's in-browser so Javascript-based but the ideas are pretty much the same as Dart. +It's in-browser so you'll need to use Javascript but the core conceps are pretty much the same as Dart. You may also like to look at the [Flutter tutorial](https://getstream.io/chat/flutter/tutorial/) which focuses on using the UI package to get Stream Chat integrated into a Flutter app. From 3cddec630122dfe89fea816b604f6d4a31bab8ce Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 17:34:34 +0200 Subject: [PATCH 022/165] docs: mention localization --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0faad799..3bb5a923 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,8 @@ Stream's Flutter code is kept in a single mono-repository consisting of multiple `stream_chat_persistence` - This package provides a persistence client for fetching and saving chat data locally. Stream Chat Persistence uses Moor as a disk cache. +`stream_chat_localizations` - This package provides a set of localizations for the SDK. + ### Local Setup Congratulations! 🎉. You've successfully cloned our repo, and you are ready to make your first contribution. Before you can start making code changes, there are a few things to configure. From 3ef2c4027c2189fd3f5cad3a0349e029e853b648 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 17:55:56 +0200 Subject: [PATCH 023/165] docs: grammar fixes --- docusaurus/docs/Flutter/basics/introduction.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/Flutter/basics/introduction.mdx b/docusaurus/docs/Flutter/basics/introduction.mdx index ad1df5af..99170774 100644 --- a/docusaurus/docs/Flutter/basics/introduction.mdx +++ b/docusaurus/docs/Flutter/basics/introduction.mdx @@ -13,7 +13,7 @@ We also support a variety of other SDKs. This section of the documentation focuses on our Flutter SDK which helps you easily ship high quality messaging experiences in apps and programs built with the [Flutter toolkit -by Google](https://flutter.dev). +made by Google](https://flutter.dev). The Stream Chat Flutter SDK comprises five different packages to choose from, ranging from ones giving you complete control to ones that give you a rich out-of-the-box chat experience. @@ -32,7 +32,7 @@ saving chat data locally. 5. Localizations (stream_chat_localizations): provides a set of localizations for the SDK. We recommend building prototypes using the full UI package, [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter), -since it contains UI widgets already integrated with Stream's API. Is the fastest way to get up +since it contains UI widgets already integrated with Stream's API. It is the fastest way to get up and running using Stream chat in your app. The Flutter SDK enables you to build any type of chat or messaging experience for Android, iOS, Web From 0995f663978571808014db3ecde367e1c969edac Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 01:07:46 +0530 Subject: [PATCH 024/165] chore(localization): fix analyzer and formatting errors Signed-off-by: Sahil Kumar --- .../lib/src/stream_chat_localizations_es.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 75bed1e3..b43e4ba8 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -68,7 +68,8 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get genericErrorText => 'Hubo un problema'; @override - String get loadingMessagesError => 'Hubo un error mientras se cargaba el mensaje'; + String get loadingMessagesError => + 'Hubo un error mientras se cargaba el mensaje'; @override String resultCountText(int count) => '$count resultados'; @@ -249,7 +250,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String sentAtText({required DateTime date, required DateTime time}) => - 'Enviado el ${_getDay(date)} a las ${Jiffy(time.toLocal()).format('HH:mm')}'; + '''Enviado el ${_getDay(date)} a las ${Jiffy(time.toLocal()).format('HH:mm')}'''; @override String get todayLabel => 'Hoy'; From a6e96c79f6999b6e612d98615c87016872fbcc36 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 01:08:09 +0530 Subject: [PATCH 025/165] chore(localization): update CHANGELOG.md Signed-off-by: Sahil Kumar --- packages/stream_chat_localizations/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 4667ab93..7565224e 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,6 +1,10 @@ +## Upcoming + +* Added support for `Spanish ('es')` locale. + ## 1.0.2 -* Updated stream_chat_flutter dependency +* Updated `stream_chat_flutter` dependency ## 1.0.1 From 5c3a9485a454f73e36e93036f6af69c68336aec4 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 01:24:04 +0530 Subject: [PATCH 026/165] chore(doc, ui): minor formatting changes Signed-off-by: Sahil Kumar --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- packages/stream_chat_flutter/example/lib/main.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 9eb4dd68..c3585b1b 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -175,6 +175,6 @@ Example: nb fr it - es + es ``` diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index c2663a6a..1589c909 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -74,6 +74,7 @@ class MyApp extends StatelessWidget { Locale('hi'), Locale('fr'), Locale('it'), + Locale('es'), ], localizationsDelegates: GlobalStreamChatLocalizations.delegates, builder: (context, widget) => StreamChat( From 47349ae3dd18fdf08128e87fec10d77bb53512bc Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 01:27:47 +0530 Subject: [PATCH 027/165] chore(localization): update CHANGELOG.md Signed-off-by: Sahil Kumar --- packages/stream_chat_localizations/CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 7565224e..865741c4 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,6 +1,6 @@ ## Upcoming -* Added support for `Spanish ('es')` 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. ## 1.0.2 @@ -12,4 +12,8 @@ ## 1.0.0 -* First release +* Initial Release with support for 4 locales + - [English](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart) + - [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart) + - [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart) + - [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart) From 984b9af1825012e2aec8c776e3dabd0f16bb3a0f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 01:32:30 +0530 Subject: [PATCH 028/165] chore(localization): update README.md Signed-off-by: Sahil Kumar --- packages/stream_chat_localizations/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_localizations/README.md b/packages/stream_chat_localizations/README.md index bf648e45..c2ea8a72 100644 --- a/packages/stream_chat_localizations/README.md +++ b/packages/stream_chat_localizations/README.md @@ -34,6 +34,7 @@ At the moment we support the following languages: - [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart) - [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart) - [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart) +- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages. @@ -68,6 +69,7 @@ class MyApp extends StatelessWidget { Locale('hi'), Locale('fr'), Locale('it'), + Locale('es'), ], // Add GlobalStreamChatLocalizations.delegates localizationsDelegates: GlobalStreamChatLocalizations.delegates, @@ -86,13 +88,13 @@ class MyApp extends StatelessWidget { ### Adding a new language -To add a new language, you need to create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it adding it to the `delegates` array. +To add a new language, create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it, adding it to the `delegates` array. Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/add_new_lang.dart) to see how to add a new language. ### Override existing languages -To override an existing language, you need to create a new class extending that particular language class and create a delegate for it adding it to the `delegates` array. +To override an existing language, create a new class extending that particular language class and create a delegate for it, adding it to the `delegates` array. Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/override_lang.dart) to see how to override an existing language. @@ -110,6 +112,7 @@ Example: nb fr it + es ``` From e1d97feaabb50b51edd87c1b468cceb8c41563af Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Wed, 4 Aug 2021 16:33:52 -0400 Subject: [PATCH 029/165] add korean to tranlsation docs --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 3a81187a..5a8813c2 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -27,6 +27,7 @@ At the moment we support the following languages: - [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. ### Add dependency @@ -65,6 +66,7 @@ class MyApp extends StatelessWidget { Locale('it'), Locale('es'), Locale('ja'), + Locale('ko'), ], // Add GlobalStreamChatLocalizations.delegates localizationsDelegates: GlobalStreamChatLocalizations.delegates, @@ -135,6 +137,7 @@ Here is an example of how that would look like: Locale('it'), Locale('es'), Locale('ja'), + Locale('ko'), ], // locales are the locales of the device // supportedLocales are the app supported locales @@ -179,5 +182,6 @@ Example: it es ja + ko ``` From 2b57076a32ddd7ac4a2ea9d65f0c43d045f25e98 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Aug 2021 15:11:12 +0530 Subject: [PATCH 030/165] added new guide --- .../guides/adding_local_data_persistence.mdx | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx diff --git a/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx new file mode 100644 index 00000000..76e84524 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx @@ -0,0 +1,76 @@ +--- +id: adding_local_data_persistence +sidebar_position: 9 +title: Adding Local Data Persistence +--- + +Adding Local Data Persistence + +### Introduction + +Most messaging apps need to work regardless of whether the app is currently connected to the internet. +Local data persistence stores the fetched data from the backend on a local SQLite database using the +moor package in Flutter. All packages in the SDK can use local data persistence to store messages +across multiple platforms. + +### Implementation + +To add data persistence you can extend the class ChatPersistenceClient and pass an instance to the StreamChatClient. + +```dart +class CustomChatPersistentClient extends ChatPersistenceClient { +... +} + +final client = StreamChatClient( + apiKey ?? kDefaultStreamApiKey, + logLevel: Level.INFO, +)..chatPersistenceClient = CustomChatPersistentClient(); +``` + +We provide an official persistent client in the [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) +package that works using the library [moor](https://moor.simonbinder.eu), an SQLite ORM. + +Add this to your package's `pubspec.yaml` file, using the latest version. + +```yaml +dependencies: + stream_chat_persistence: ^latest_version +``` + +You should then run `flutter packages get` + +The usage is pretty simple. + +1. Create a new instance of `StreamChatPersistenceClient` providing `logLevel` and `connectionMode` + +```dart +final chatPersistentClient = StreamChatPersistenceClient( + logLevel: Level.INFO, + connectionMode: ConnectionMode.background, +); +``` + +2. Pass the instance to the official `StreamChatClient` + +```dart + final client = StreamChatClient( + apiKey ?? kDefaultStreamApiKey, + logLevel: Level.INFO, + )..chatPersistenceClient = chatPersistentClient; +``` + +And you are ready to go... + +Note that passing `ConnectionMode.background` the database uses a background isolate to unblock the main thread. +The `StreamChatClient` uses the `chatPersistentClient` to synchronize the database with the newest +information every time it receives new data about channels/messages/users. + +### Multi-user + +The DB file is named after the `userId`, so if you instantiate a client using a different `userId` you will use a different database. +Calling `client.disconnect(flushOfflineStorage: true)` flushes all current database data. + +### Updating/deleting/sending a message while offline + +The information about the action is saved in offline storage. When the client returns online, everything is retried. \ No newline at end of file From 291d33434da1981c6ec021a70afb5ee217898a43 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Aug 2021 15:15:03 +0530 Subject: [PATCH 031/165] Update docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx Co-authored-by: Sahil Kumar --- .../docs/Flutter/guides/adding_local_data_persistence.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx index 76e84524..e6283ca0 100644 --- a/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx +++ b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx @@ -69,8 +69,8 @@ information every time it receives new data about channels/messages/users. ### Multi-user The DB file is named after the `userId`, so if you instantiate a client using a different `userId` you will use a different database. -Calling `client.disconnect(flushOfflineStorage: true)` flushes all current database data. +Calling `client.disconnectUser(flushChatPersistence: true)` flushes all current database data. ### Updating/deleting/sending a message while offline -The information about the action is saved in offline storage. When the client returns online, everything is retried. \ No newline at end of file +The information about the action is saved in offline storage. When the client returns online, everything is retried. From 57e9552248230c16291989f4509e074bc33afd61 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Thu, 5 Aug 2021 13:36:15 -0400 Subject: [PATCH 032/165] updating youText in ja + ko based on Eric's input --- .../lib/src/stream_chat_localizations_ja.dart | 2 +- .../lib/src/stream_chat_localizations_ko.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index a30bfe2f..e8d9759e 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -331,7 +331,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get inText => 'に'; @override - String get youText => 'お客様'; + String get youText => 'あなた'; // This is the word for 'customer' or 'user' because saying 'you' directly is too informal and rude @override diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 36c578e3..dbd9609d 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -335,7 +335,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get inText => '에'; @override - String get youText => '고객님'; + String get youText => '당신'; // This is the word for 'customer' or 'user' because saying 'you' directly is too informal and rude @override From 91c0a7031a8f47327b5c6ad8d16b5913b87138f2 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 6 Aug 2021 11:39:51 -0400 Subject: [PATCH 033/165] feat(ui): added backgroundColor property to the various header widgets Also updated CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 1 + packages/stream_chat_flutter/lib/src/channel_header.dart | 6 +++++- .../stream_chat_flutter/lib/src/channel_list_header.dart | 7 ++++++- packages/stream_chat_flutter/lib/src/gallery_header.dart | 7 ++++++- packages/stream_chat_flutter/lib/src/thread_header.dart | 6 +++++- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 4fe9e6e9..55dfcc62 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -4,6 +4,7 @@ - [#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 various header widgets 🔄 Changed diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 11dfc95a..ada04271 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -67,6 +67,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { this.subtitle, this.leading, this.actions, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -102,6 +103,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the [ChannelAvatar] final List? actions; + /// The background color for this [ChannelHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; @@ -141,7 +145,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { brightness: Theme.of(context).brightness, elevation: 1, leading: leadingWidget, - backgroundColor: channelHeaderTheme.color, + backgroundColor: backgroundColor ?? channelHeaderTheme.color, actions: actions ?? [ Padding( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 692db477..4ce8b0d2 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -61,6 +61,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { this.subtitle, this.leading, this.actions, + this.backgroundColor, }) : super(key: key); /// Pass this if you don't have a [StreamChatClient] in your widget tree. @@ -93,6 +94,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the new chat button final List? actions; + /// The background color for this [ChannelListHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; @@ -124,7 +128,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, - backgroundColor: channelListHeaderThemeData.color, + backgroundColor: + backgroundColor ?? channelListHeaderThemeData.color, centerTitle: true, leading: leading ?? Center( diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 3e732898..74eeb4ee 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -19,6 +19,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { this.onImageTap, this.userName = '', this.sentAt = '', + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -50,6 +51,9 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Stores the current index of media shown final int currentIndex; + /// The background color of this [GalleryHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final galleryHeaderThemeData = GalleryHeaderTheme.of(context); @@ -66,7 +70,8 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { onPressed: onBackPressed, ) : const SizedBox(), - backgroundColor: galleryHeaderThemeData.backgroundColor, + backgroundColor: + backgroundColor ?? galleryHeaderThemeData.backgroundColor, actions: [ if (!message.isEphemeral) IconButton( diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 66b6b8be..abbd9bf0 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -70,6 +70,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { this.actions, this.onTitleTap, this.showTypingIndicator = true, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -102,6 +103,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// if a user is typing in this thread final bool showTypingIndicator; + /// The background color of this [ThreadHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final channelHeaderTheme = ChannelHeaderTheme.of(context); @@ -136,7 +140,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { showUnreads: true, ) : const SizedBox()), - backgroundColor: channelHeaderTheme.color, + backgroundColor: backgroundColor ?? channelHeaderTheme.color, centerTitle: true, actions: actions, title: InkWell( From b8de269c1f88985b7262d3b7b27a411800bd76bf Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 6 Aug 2021 12:16:22 -0400 Subject: [PATCH 034/165] chore: updated CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 55dfcc62..cbcad168 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -4,7 +4,11 @@ - [#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 various header widgets +- Added a `backgroundColor` property to the following widgets: + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `ThreadHeader` 🔄 Changed From bdc527f8d312418563adbd84633a05b05a8e4bc7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 7 Aug 2021 00:58:46 +0530 Subject: [PATCH 035/165] feat(ui): added backgroundColor property to GalleryFooter Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/gallery_footer.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index 74a7ac45..365368ac 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -27,6 +27,7 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { this.totalPages = 0, this.mediaAttachments = const [], this.mediaSelectedCallBack, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -55,6 +56,9 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// Callback when media is selected final ValueChanged? mediaSelectedCallBack; + /// The background color of this [GalleryFooter]. + final Color? backgroundColor; + @override _GalleryFooterState createState() => _GalleryFooterState(); @@ -90,7 +94,8 @@ class _GalleryFooterState extends State { context: context, removeTop: true, child: BottomAppBar( - color: galleryFooterThemeData.backgroundColor, + color: + widget.backgroundColor ?? galleryFooterThemeData.backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ From f30704dbeb8b489a5eb2d1abe3c0867a577e1004 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 7 Aug 2021 00:59:02 +0530 Subject: [PATCH 036/165] chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index cbcad168..5289ec30 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -8,6 +8,7 @@ - `ChannelHeader` - `ChannelListHeader` - `GalleryHeader` + - `GalleryFooter` - `ThreadHeader` From 2c0eaaafab89179e2f8ed94468672cfa1876310e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Sun, 8 Aug 2021 22:27:40 +0530 Subject: [PATCH 037/165] added new guide --- .../docs/Flutter/guides/understanding_filters.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docusaurus/docs/Flutter/guides/understanding_filters.mdx diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx new file mode 100644 index 00000000..47d72e1f --- /dev/null +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -0,0 +1,15 @@ +--- +id: understanding_filters +sidebar_position: 10 +title: Understanding Filters +--- + +Understanding Filters + +### Introduction + +Filters are used to get a specific subset of objects (channels, users, messages, members, etc) which +fit the conditions specified. Earlier versions of the SDK contained String-based filters which are now replaced by type-safe +filters. This guide aims to explain the different types of filters and how to use them. + + From ff4baabcad05c9b75deae446fef636e24b731673 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Aug 2021 16:38:02 +0530 Subject: [PATCH 038/165] added guides --- .../Flutter/guides/understanding_filters.mdx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index 47d72e1f..8a6fc925 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -12,4 +12,132 @@ Filters are used to get a specific subset of objects (channels, users, messages, fit the conditions specified. Earlier versions of the SDK contained String-based filters which are now replaced by type-safe filters. This guide aims to explain the different types of filters and how to use them. +### Types Of Filters +#### Filter.equal + +The 'equal' filter gets the objects where the given key has the specified value. + +```dart +Filter.equal('type', 'messaging'), +``` + +#### Filter.notEqual + +The 'notEqual' filter gets the objects where the given key does not have the specified value. + +```dart +Filter.notEqual('type', 'messaging'), +``` + +#### Filter.greater + +The 'greater' filter gets the objects where the given key has a higher value than the specified value. + +```dart +Filter.greater('count', 5), +``` + +#### Filter.greaterOrEqual + +The 'greaterOrEqual' filter gets the objects where the given key has an equal or higher value than the specified value. + +```dart +Filter.greaterOrEqual('count', 5), +``` + +#### Filter.less + +The 'less' filter gets the objects where the given key has a lesser value than the specified value. + +```dart +Filter.less('count', 5), +``` + +#### Filter.lessOrEqual + +The 'lessOrEqual' filter gets the objects where the given key has a lesser or equal value than the specified value. + +```dart +Filter.lessOrEqual('count', 5), +``` + +#### Filter.in_ + +The 'in_' filter allows getting objects where the key matches any in a specified array. + +```dart +Filter.in_('members', [user.id]) +``` + +:::note +Since 'in' is a keyword in Dart, the filter has an underscore added. This does not apply to the 'notIn' +keyword. +::: + +#### Filter.notIn + +The 'notIn' filter allows getting objects where the key matches none in a specified array. + +```dart +Filter.notIn('members', [user.id]) +``` + +#### Filter.query + +Matches values by performing text search with the specified value. + +```dart +Filter.query('name', 'demo') +``` + +#### Filter.autoComplete + +Matches values with the specified prefix. + +```dart +Filter.autoComplete('name', 'demo') +``` + +#### Filter.exists + +Matches values that exist/don't exist based on the specified boolean value. + +```dart +Filter.exists('name', true) +``` + +### Group Queries + +#### FilterOperator.and + +The 'and' operator combines multiple queries. + +```dart +final filter = Filter.and([ + Filter.equal('type', 'messaging'), + Filter.in_('members', [user.id]) +]) +``` + +#### FilterOperator.or + +Combines the provided filters and matches the values matched by at least one of the filters. + +```dart +final filter = Filter.or([ + Filter.in_('bannedUsers', [user.id]), + Filter.in_('shadowBannedUsers', [user.id]) +]) +``` + +#### FilterOperator.nor + +Combines the provided filters and matches the values not matched by all the filters. + +```dart +final filter = Filter.nor([ + Filter.in_('bannedUsers', [user.id]), + Filter.in_('shadowBannedUsers', [user.id]) +]) +``` From f8a6539d09fb364f345f88750d95a45a2cb2ab7e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 9 Aug 2021 16:48:23 +0530 Subject: [PATCH 039/165] feat(ui): add support for `attachmentButtonBuilder`, `commandButtonBuilder` in `MessageInput` Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 144 +++++++++++------- .../lib/src/extension.dart | 51 ++++++- .../lib/src/message_input.dart | 104 ++++++++----- .../lib/stream_chat_flutter.dart | 1 + 4 files changed, 198 insertions(+), 102 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5289ec30..341d481b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -2,21 +2,35 @@ ✅ Added -- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): Added `StreamChatThemeData.placeholderUserImage` for - building a widget when the `UserAvatar` image is loading +- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): + Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image + is loading - Added a `backgroundColor` property to the following widgets: - - `ChannelHeader` - - `ChannelListHeader` - - `GalleryHeader` - - `GalleryFooter` - - `ThreadHeader` + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `GalleryFooter` + - `ThreadHeader` +- Added `MessageInput.attachmentButtonBuilder` and `MessageInput.commandButtonBuilder` for more + customizations. + +```dart +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, + ); +``` + +> **_NOTE:_** The last parameter is the default `ActionButton` +You can call `.copyWith` to customize just a subset of properties. 🔄 Changed -Theming has been upgraded! Most theme classes now have `InheritedTheme` classes -associated with them, and have been upgraded with some goodies like `lerp` functions. -Here's the full naming breakdown: +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` @@ -32,8 +46,9 @@ Here's the full naming breakdown: 🐞 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 @@ -50,7 +65,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 @@ -64,17 +80,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 @@ -95,7 +111,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 @@ -103,7 +119,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 @@ -112,10 +129,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 @@ -125,17 +144,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 @@ -162,21 +181,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 @@ -246,7 +268,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 @@ -303,7 +326,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 @@ -320,8 +344,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 @@ -355,7 +379,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 @@ -540,10 +565,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 @@ -645,8 +671,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 diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index b975941e..2780e1c8 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -46,9 +46,9 @@ extension PlatformFileX on PlatformFile { ); } -/// +/// Extension on [InputDecoration] extension InputDecorationX on InputDecoration { - /// + /// Merges this [AvatarThemeData] 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, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 9431cd22..f5e4158a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -50,6 +50,14 @@ typedef MentionTileBuilder = Widget Function( Member member, ); +/// Widget builder for action button +/// [defaultActionButton] is the default [IconButton] configuration +/// Use [defaultActionButton.copyWith] to easily customize it +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, +); + /// Location for actions on the [MessageInput] enum ActionsLocation { /// Align to left @@ -164,6 +172,8 @@ class MessageInput extends StatefulWidget { this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoFrameRate = 30, this.onError, + this.attachmentButtonBuilder, + this.commandButtonBuilder, }) : super(key: key); /// Message to edit @@ -247,6 +257,12 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; + /// Builder for customizing attachment button. + final ActionButtonBuilder? attachmentButtonBuilder; + + /// Builder for customizing command button. + final ActionButtonBuilder? commandButtonBuilder; + @override MessageInputState createState() => MessageInputState(); @@ -403,11 +419,11 @@ class MessageInputState extends State { children: [ 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), ], @@ -490,7 +506,7 @@ class MessageInputState extends State { ); } - Widget _buildExpandActionsButton() { + Widget _buildExpandActionsButton(BuildContext context) { final channel = StreamChannel.of(context).channel; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -528,12 +544,13 @@ class MessageInputState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - 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)), ), @@ -669,9 +686,7 @@ class MessageInputState extends State { : (widget.actionsLocation == ActionsLocation.leftInside ? Row( mainAxisSize: MainAxisSize.min, - children: [ - _buildExpandActionsButton(), - ], + children: [_buildExpandActionsButton(context)], ) : null), suffixIconConstraints: const BoxConstraints.tightFor(height: 40), @@ -697,7 +712,7 @@ class MessageInputState extends State { ), if (!_commandEnabled && widget.actionsLocation == ActionsLocation.rightInside) - _buildExpandActionsButton(), + _buildExpandActionsButton(context), if (widget.sendButtonLocation == SendButtonLocation.inside) _animateSendButton(context), ], @@ -1681,10 +1696,9 @@ class MessageInputState extends State { } } - 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 @@ -1722,38 +1736,46 @@ class MessageInputState extends State { } }, ); + + 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 diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 89c6450c..42188fa0 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -12,6 +12,7 @@ export 'src/channel_preview.dart'; export 'src/connection_status_builder.dart'; export 'src/date_divider.dart'; export 'src/deleted_message.dart'; +export 'src/extension.dart' show IconButtonX; export 'src/full_screen_media.dart'; export 'src/gallery_footer.dart'; export 'src/gallery_header.dart'; From aa223ce25877d9c9ba5aec6f247f27a40f0a7f3b Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Mon, 9 Aug 2021 14:30:07 +0200 Subject: [PATCH 040/165] chore: format comments --- .../stream_chat/lib/src/client/channel.dart | 232 ++++++++++-------- 1 file changed, 123 insertions(+), 109 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index a05587ab..4d4a3858 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -63,145 +63,145 @@ class Channel { _extraData.addAll(extraData); } - /// Returns true if the channel is muted + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes .any((element) => element.channel.cid == cid) == true; - /// Returns true if the channel is muted as a stream + /// Returns true if the channel is muted, as a stream. Stream? get isMutedStream => _client.state.currentUserStream .map((event) => event!.channelMutes.any((element) => element.channel.cid == cid) == true) .distinct(); - /// True if the channel is a group + /// True if the channel is a group. bool get isGroup => memberCount != 2; - /// True if the channel is distinct + /// True if the channel is distinct. bool get isDistinct => id?.startsWith('!members') == true; - /// Channel configuration + /// Channel configuration. ChannelConfig? get config { _checkInitialized(); return state?._channelState.channel?.config; } - /// Channel configuration as a stream + /// Channel configuration as a stream. Stream? get configStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.config); } - /// Channel user creator + /// Channel user creator. User? get createdBy { _checkInitialized(); return state?._channelState.channel?.createdBy; } - /// Channel user creator as a stream + /// Channel user creator as a stream. Stream? get createdByStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.createdBy); } - /// Channel frozen status + /// Channel frozen status. bool? get frozen { _checkInitialized(); return state?._channelState.channel?.frozen; } - /// Channel frozen status as a stream + /// Channel frozen status as a stream. Stream? get frozenStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.frozen); } - /// Channel creation date + /// Channel creation date. DateTime? get createdAt { _checkInitialized(); return state?._channelState.channel?.createdAt; } - /// Channel creation date as a stream + /// Channel creation date as a stream. Stream? get createdAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.createdAt); } - /// Channel last message date + /// Channel last message date. DateTime? get lastMessageAt { _checkInitialized(); return state?._channelState.channel?.lastMessageAt; } - /// Channel last message date as a stream + /// Channel last message date as a stream. Stream? get lastMessageAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.lastMessageAt); } - /// Channel updated date + /// Channel updated date. DateTime? get updatedAt { _checkInitialized(); return state?._channelState.channel?.updatedAt; } - /// Channel updated date as a stream + /// Channel updated date as a stream. Stream? get updatedAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.updatedAt); } - /// Channel deletion date + /// Channel deletion date. DateTime? get deletedAt { _checkInitialized(); return state?._channelState.channel?.deletedAt; } - /// Channel deletion date as a stream + /// Channel deletion date as a stream. Stream? get deletedAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.deletedAt); } - /// Channel member count + /// Channel member count. int? get memberCount { _checkInitialized(); return state?._channelState.channel?.memberCount; } - /// Channel member count as a stream + /// Channel member count as a stream. Stream? get memberCountStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.memberCount); } - /// Channel id + /// Channel id. String? get id => state?._channelState.channel?.id ?? _id; - /// Channel type + /// Channel type. String get type => state?._channelState.channel?.type ?? _type; - /// Channel cid + /// Channel cid. String? get cid => state?._channelState.channel?.cid ?? _cid; - /// Channel team + /// Channel team. String? get team { _checkInitialized(); return state?._channelState.channel?.team; } - /// Channel extra data + /// Channel extra data. Map get extraData { var data = state?._channelState.channel?.extraData; if (data == null || data.isEmpty) { @@ -210,7 +210,7 @@ class Channel { return data; } - /// Channel extra data as a stream + /// Channel extra data as a stream. Stream> get extraDataStream { _checkInitialized(); return state!.channelStateStream.map( @@ -224,9 +224,10 @@ class Channel { final Completer _initializedCompleter = Completer(); - /// True if this is initialized + /// True if this is initialized. + /// /// Call [watch] to initialize the client or instantiate it using - /// [Channel.fromState] + /// [Channel.fromState]. Future get initialized => _initializedCompleter.future; final _cancelableAttachmentUploadRequest = {}; @@ -362,7 +363,9 @@ class Channel { } /// Send a [message] to this channel. - /// If [skipPush] is true the message will not send a push notification + /// + /// If [skipPush] is true the message will not send a push notification. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually sending the message. Future sendMessage( @@ -427,6 +430,7 @@ class Channel { } /// Updates the [message] in this channel. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually updating the message. Future updateMessage(Message message) async { @@ -489,8 +493,10 @@ class Channel { } /// Partially updates the [message] in this channel. - /// Use [set] to define values to be set - /// Use [unset] to define values to be unset + /// + /// Use [set] to define values to be set. + /// + /// Use [unset] to define values to be unset. Future partialUpdateMessage( Message message, { Map? set, @@ -590,7 +596,7 @@ class Channel { ); } - /// Unpins provided message + /// Unpins provided message. Future unpinMessage(Message message) => partialUpdateMessage( message, @@ -599,7 +605,7 @@ class Channel { }, ); - /// Send a file to this channel + /// Send a file to this channel. Future sendFile( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -615,7 +621,7 @@ class Channel { ); } - /// Send an image to this channel + /// Send an image to this channel. Future sendImage( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -631,7 +637,7 @@ class Channel { ); } - /// A message search. + /// Search for a message with the given options. Future search({ String? query, Filter? messageFilters, @@ -648,7 +654,7 @@ class Channel { ); } - /// Delete a file from this channel + /// Delete a file from this channel. Future deleteFile( String url, { CancelToken? cancelToken, @@ -662,7 +668,7 @@ class Channel { ); } - /// Delete an image from this channel + /// Delete an image from this channel. Future deleteImage( String url, { CancelToken? cancelToken, @@ -676,14 +682,15 @@ class Channel { ); } - /// Send an event on this channel + /// Send an event on this channel. Future sendEvent(Event event) { _checkInitialized(); return _client.sendEvent(id!, type, event); } - /// Send a reaction to this channel - /// Set [enforceUnique] to true to remove the existing user reaction + /// Send a reaction to this channel. + /// + /// Set [enforceUnique] to true to remove the existing user reaction. Future sendReaction( Message message, String type, { @@ -746,7 +753,7 @@ class Channel { } } - /// Delete a reaction from this channel + /// Delete a reaction from this channel. Future deleteReaction( Message message, Reaction reaction) async { final type = reaction.type; @@ -821,25 +828,25 @@ class Channel { return _client.deleteChannel(id!, type); } - /// Removes all messages from the channel + /// Removes all messages from the channel. Future truncate() async { _checkInitialized(); return _client.truncateChannel(id!, type); } - /// Accept invitation to the channel + /// Accept invitation to the channel. Future acceptInvite([Message? message]) async { _checkInitialized(); return _client.acceptChannelInvite(id!, type, message: message); } - /// Reject invitation to the channel + /// Reject invitation to the channel. Future rejectInvite([Message? message]) async { _checkInitialized(); return _client.rejectChannelInvite(id!, type, message: message); } - /// Add members to the channel + /// Add members to the channel. Future addMembers( List memberIds, [ Message? message, @@ -848,7 +855,7 @@ class Channel { return _client.addChannelMembers(id!, type, memberIds, message: message); } - /// Invite members to the channel + /// Invite members to the channel. Future inviteMembers( List memberIds, [ Message? message, @@ -857,7 +864,7 @@ class Channel { return _client.inviteChannelMembers(id!, type, memberIds, message: message); } - /// Remove members from the channel + /// Remove members from the channel. Future removeMembers( List memberIds, [ Message? message, @@ -866,7 +873,7 @@ class Channel { return _client.removeChannelMembers(id!, type, memberIds, message: message); } - /// Send action for a specific message of this channel + /// Send action for a specific message of this channel. Future sendAction( Message message, Map formData, @@ -913,9 +920,10 @@ class Channel { return res; } - /// Mark all messages as read + /// Mark all messages as read. + /// /// Optionally provide a [messageId] if you want to mark a - /// particular message as read + /// particular message as read. Future markRead({String? messageId}) async { _checkInitialized(); client.state.totalUnreadCount = @@ -924,7 +932,7 @@ class Channel { return _client.markChannelRead(id!, type, messageId: messageId); } - /// Loads the initial channel state and watches for changes + /// Loads the initial channel state and watches for changes. Future watch() async { ChannelState response; @@ -955,15 +963,16 @@ class Channel { } } - /// Stop watching the channel + /// Stop watching the channel. Future stopWatching() async { _checkInitialized(); return _client.stopChannelWatching(id!, type); } - /// List the message replies for a parent message + /// List the message replies for a parent message. + /// /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// in the offline storage. Future getReplies( String parentId, { PaginationParams? options, @@ -987,7 +996,7 @@ class Channel { return repliesResponse; } - /// List the reactions for a message in the channel + /// List the reactions for a message in the channel. Future getReactions( String messageId, { PaginationParams? pagination, @@ -997,7 +1006,7 @@ class Channel { pagination: pagination, ); - /// Retrieves a list of messages by ID + /// Retrieves a list of messages by given [messageIDs]. Future getMessagesById( List messageIDs, ) async { @@ -1008,7 +1017,7 @@ class Channel { return res; } - /// Retrieves a list of messages by ID + /// Translate a message by given [messageId] and [language]. Future translateMessage( String messageId, String language, @@ -1018,12 +1027,13 @@ class Channel { language, ); - /// Creates a new channel + /// Creates a new channel. Future create() async => query(state: false); - /// Query the API, get messages, members or other channel fields - /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// Query the API, get messages, members or other channel fields. + /// + /// Set [preferOffline] to true to avoid the API call if the data is already + /// in the offline storage. Future query({ bool state = true, bool watch = false, @@ -1077,7 +1087,7 @@ class Channel { } } - /// Query channel members + /// Query channel members. Future queryMembers({ Filter? filter, List? sort, @@ -1092,19 +1102,19 @@ class Channel { pagination: pagination, ); - /// Mutes the channel + /// Mutes the channel. Future mute({Duration? expiration}) { _checkInitialized(); return _client.muteChannel(cid!, expiration: expiration); } - /// Unmutes the channel + /// Unmute the channel. Future unmute() { _checkInitialized(); return _client.unmuteChannel(cid!); } - /// Bans a user from the channel + /// Bans the user with given [userID] from the channel. Future banUser( String userID, Map options, @@ -1118,7 +1128,7 @@ class Channel { return _client.banUser(userID, opts); } - /// Remove the ban for a user in the channel + /// Remove the ban for the user with given [userID] in the channel. Future unbanUser(String userID) async { _checkInitialized(); return _client.unbanUser(userID, { @@ -1127,7 +1137,7 @@ class Channel { }); } - /// Shadow bans a user from the channel + /// Shadow bans the user with the given [userID] from the channel. Future shadowBan( String userID, Map options, @@ -1141,7 +1151,7 @@ class Channel { return _client.shadowBan(userID, opts); } - /// Remove the shadow ban for a user in the channel + /// Remove the shadow ban for the user with the given [userID] in the channel. Future removeShadowBan(String userID) async { _checkInitialized(); return _client.removeShadowBan(userID, { @@ -1151,8 +1161,10 @@ class Channel { } /// Hides the channel from [StreamChatClient.queryChannels] for the user - /// until a message is added If [clearHistory] is set to true - all messages - /// will be removed for the user + /// until a message is added. + /// + /// If [clearHistory] is set to true - all messages + /// will be removed for the user. Future hide({bool clearHistory = false}) async { _checkInitialized(); final response = await _client.hideChannel( @@ -1170,7 +1182,7 @@ class Channel { return response; } - /// Removes the hidden status for the channel + /// Removes the hidden status for the channel. Future show() async { _checkInitialized(); return _client.showChannel(id!, type); @@ -1178,7 +1190,7 @@ class Channel { /// Stream of [Event] coming from websocket connection specific for the /// channel. Pass an eventType as parameter in order to filter just a type - /// of event + /// of event. Stream on([ String? eventType, String? eventType2, @@ -1216,7 +1228,7 @@ class Channel { } } - /// Sets last typing to null and sends the typing.stop event + /// Sets last typing to null and sends the typing.stop event. Future stopTyping([String? parentId]) async { if (config?.typingEvents == false) { return; @@ -1230,7 +1242,7 @@ class Channel { )); } - /// Call this method to dispose the channel client + /// Call this method to dispose the channel client. void dispose() { state?.dispose(); } @@ -1244,9 +1256,9 @@ class Channel { } } -/// The class that handles the state of the channel listening to the events +/// The class that handles the state of the channel listening to the events. class ChannelClientState { - /// Creates a new instance listening to events and updating the state + /// Creates a new instance listening to events and updating the state. ChannelClientState( this._channel, ChannelState channelState, @@ -1393,23 +1405,25 @@ class ChannelClientState { } /// Flag which indicates if [ChannelClientState] contain latest/recent messages or not. + /// /// This flag should be managed by UI sdks. - /// When false, any new message (received by WebSocket event - /// - [EventType.messageNew]) will not be pushed on to message list. + /// + /// When false, any new message received by WebSocket event + /// [EventType.messageNew] will not be pushed on to message list. bool get isUpToDate => _isUpToDateController.value; set isUpToDate(bool isUpToDate) => _isUpToDateController.add(isUpToDate); - /// [isUpToDate] flag count as a stream + /// [isUpToDate] flag count as a stream. Stream get isUpToDateStream => _isUpToDateController.stream; final BehaviorSubject _isUpToDateController = BehaviorSubject.seeded(true); - /// The retry queue associated to this channel + /// The retry queue associated to this channel. late final RetryQueue _retryQueue; - /// Retry failed message + /// Retry failed message. Future retryFailedMessages() async { final failedMessages = [...messages, ...threads.values.expand((v) => v)] @@ -1502,7 +1516,7 @@ class ChannelClientState { })); } - /// Add a message to this channel + /// Add a message to this channel. void addMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { final newMessages = List.from(_channelState.messages); @@ -1567,36 +1581,36 @@ class ChannelClientState { ); } - /// Channel message list + /// Channel message list. List get messages => _channelState.messages; - /// Channel message list as a stream + /// Channel message list as a stream. Stream?> get messagesStream => channelStateStream .map((cs) => cs.messages) .distinct(const ListEquality().equals); - /// Channel pinned message list + /// Channel pinned message list. List? get pinnedMessages => _channelState.pinnedMessages.toList(); - /// Channel pinned message list as a stream + /// Channel pinned message list as a stream. Stream?> get pinnedMessagesStream => channelStateStream.map((cs) => cs.pinnedMessages.toList()); - /// Get channel last message + /// Get channel last message. Message? get lastMessage => _channelState.messages.isNotEmpty == true ? _channelState.messages.last : null; - /// Get channel last message + /// Get channel last message. Stream get lastMessageStream => messagesStream .map((event) => event?.isNotEmpty == true ? event!.last : null); - /// Channel members list + /// Channel members list. List get members => _channelState.members .map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id])) .toList(); - /// Channel members list as a stream + /// Channel members list as a stream. Stream> get membersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.members), @@ -1605,19 +1619,19 @@ class ChannelClientState { members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(), ).distinct(const ListEquality().equals); - /// Channel watcher count + /// Channel watcher count. int? get watcherCount => _channelState.watcherCount; - /// Channel watcher count as a stream + /// Channel watcher count as a stream. Stream get watcherCountStream => channelStateStream.map((cs) => cs.watcherCount); - /// Channel watchers list + /// Channel watchers list. List get watchers => _channelState.watchers .map((e) => _channel.client.state.users[e.id] ?? e) .toList(); - /// Channel watchers list as a stream + /// Channel watchers list as a stream. Stream> get watchersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.watchers), @@ -1625,20 +1639,20 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); - /// Channel read list + /// Channel read list. List? get read => _channelState.read; - /// Channel read list as a stream + /// Channel read list as a stream. Stream?> get readStream => channelStateStream.map((cs) => cs.read); final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); set unreadCount(int value) => _unreadCountController.add(value); - /// Unread count getter as a stream + /// Unread count getter as a stream. Stream get unreadCountStream => _unreadCountController.stream.distinct(); - /// Unread count getter + /// Unread count getter. int get unreadCount => _unreadCountController.value; bool _countMessageAsUnread(Message message) { @@ -1654,7 +1668,7 @@ class ChannelClientState { !userIsMuted; } - /// Update threads with updated information about messages + /// Update threads with updated information about messages. void updateThreadInfo(String parentId, List messages) { final newThreads = Map>.from(threads); @@ -1676,7 +1690,7 @@ class ChannelClientState { _threads = newThreads; } - /// Delete all channel messages + /// Delete all channel messages. void truncate() { _channelState = _channelState.copyWith( messages: [], @@ -1685,7 +1699,7 @@ class ChannelClientState { final List _updatedMessagesIds = []; - /// Update channelState with updated information + /// Update channelState with updated information. void updateChannelState(ChannelState updatedState) { final newMessages = [ ...updatedState.messages, @@ -1737,13 +1751,13 @@ class ChannelClientState { int _sortByCreatedAt(Message a, Message b) => a.createdAt.compareTo(b.createdAt); - /// The channel state related to this client + /// The channel state related to this client. ChannelState get _channelState => _channelStateController.value; - /// The channel state related to this client as a stream + /// The channel state related to this client as a stream. Stream get channelStateStream => _channelStateController.stream; - /// The channel state related to this client + /// The channel state related to this client. ChannelState get channelState => _channelStateController.value; late BehaviorSubject _channelStateController; @@ -1754,11 +1768,11 @@ class ChannelClientState { _debouncedUpdatePersistenceChannelState.call([v]); } - /// The channel threads related to this channel + /// The channel threads related to this channel. Map> get threads => _threadsController.value.map((key, value) => MapEntry(key, value)); - /// The channel threads related to this channel as a stream + /// The channel threads related to this channel as a stream. Stream>> get threadsStream => _threadsController.stream; final BehaviorSubject>> _threadsController = @@ -1772,10 +1786,10 @@ class ChannelClientState { _threadsController.add(v); } - /// Channel related typing users last value + /// Channel related typing users last value. Map get typingEvents => _typingEventsController.value; - /// Channel related typing users stream + /// Channel related typing users stream. Stream> get typingEventsStream => _typingEventsController.stream; @@ -1903,7 +1917,7 @@ class ChannelClientState { }); } - /// Call this method to dispose this object + /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); _unreadCountController.close(); From eae7cb10206264bfd1570dcf96f3d998910d41f6 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 09:59:08 +0200 Subject: [PATCH 041/165] feat: add image get, set and update to channel --- .../stream_chat/lib/src/client/channel.dart | 110 ++++++++++++++++-- .../test/src/api/channel_test.dart | 54 +++++++++ 2 files changed, 155 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 4d4a3858..7ea1a50b 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -15,20 +15,49 @@ import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/stream_chat.dart'; -/// This a the class that manages a specific channel. +/// Class that manages a specific channel. +/// +/// {@template image} +/// If an optional [image] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'image'. +/// +/// ```dart +/// final channel = Channel(client, type, id, image: 'https://getstream.io/image.png'); +/// print(channel.image == channel.extraData['image']); // true +/// ``` +/// +/// Before the channel is initialized the image can be set directly: +/// ```dart +/// channel.image = 'https://getstream.io/new-image'; +/// ``` +/// +/// To update the image after the channel has been initialized call: +/// ```dart +/// channel.updateImage('https://getstream.io/new-image'); +/// ``` +/// +/// This will do a partial update to update the image. +/// {@endtemplate} class Channel { - /// Create a channel client instance. + /// Class that manages a specific channel. + /// + /// Optional [extraData] and [image] properties can be provided. The [image] + /// is exposed to easily set a key of 'image' on [extraData]. Channel( this._client, this._type, this._id, { Map? extraData, + String? image, }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = extraData ?? {} { - _client.logger.info('New Channel instance not initialized created'); + _extraData = { + ...?extraData, + if (image != null) 'image': image, + } { + _client.logger.info('New Channel instance created, not yet initialized'); } - /// Create a channel client instance from a [ChannelState] object + /// Create a channel client instance from a [ChannelState] object. Channel.fromState(this._client, ChannelState channelState) : assert( channelState.channel != null, @@ -40,7 +69,7 @@ class Channel { _extraData = channelState.channel!.extraData { state = ChannelClientState(this, channelState); _initializedCompleter.complete(true); - _client.logger.info('New Channel instance initialized created'); + _client.logger.info('New Channel instance initialized'); } /// This client state @@ -63,6 +92,19 @@ class Channel { _extraData.addAll(extraData); } + /// Shortcut to set channel image. + /// + /// {@macro image} + set image(String? image) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use channel.update ' + 'to update channel image', + ); + } + _extraData.addAll({'image': image}); + } + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes @@ -218,7 +260,26 @@ class Channel { ); } - /// The main Stream chat client + /// Shortcut to get channel image. + /// + /// {@macro image} + String? get image => extraData['image'] as String?; + + /// Channel [image] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro image} + Stream get imageStream { + _checkInitialized(); + return state!.channelStateStream.map( + (cs) => + (cs.channel?.extraData['image'] as String?) ?? + (_extraData['image'] as String?), + ); + } + + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; @@ -799,7 +860,36 @@ class Channel { } } - /// Edit the channel custom data + /// Update the channel's [image]. + /// + /// This is equivelant to calling [updatePartial] and providing a map with an + /// 'image' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'image': 'https://getstream.io/new-image'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateImage('https://getstream.io/new-image'); + /// ``` + Future updateImage( + String image, + ) { + _checkInitialized(); + + return _client.updateChannelPartial( + id!, + type, + set: { + 'image': image, + }, + ); + } + + /// Edit the channel custom data. Future update( Map channelData, [ Message? updateMessage, @@ -813,7 +903,9 @@ class Channel { ); } - /// Edit the channel custom data + /// Edit the channel custom data. + // TODO: This is the same description as [update]. Distinguish the two + // and provide a better description for set and unset. Future updatePartial({ Map? set, List? unset, diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 5be55491..381c208b 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -72,6 +72,23 @@ 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); + }); }); // TODO : test all persistence related logic in this group @@ -192,6 +209,14 @@ void main() { } }); + test('should throw if trying to set `image`', () { + try { + channel.image = 'https://stream.io/some-image'; + } catch (e) { + expect(e, isA()); + } + }); + group('`.sendMessage`', () { test('should work fine', () async { final message = Message(id: 'test-message-id'); @@ -1192,6 +1217,35 @@ 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( + any(), + any(), + 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( + any(), + any(), + set: {'image': image}, + ), + ).called(1); + }); + test('`.updatePartial`', () async { const set = { 'name': 'Stream Team', From ae8ebe2dba6e6a1c3a175bb506c18610bbf7c7ab Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 10:00:02 +0200 Subject: [PATCH 042/165] chore: prefer image and name property on image --- .../test/src/core/models/reaction_test.dart | 28 ++++++++++--------- .../test/src/channel_image_test.dart | 26 +++++------------ 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index 0891b548..fbe493ef 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -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'); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index c9293e79..f6d110dd 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.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', ), ), ]; From 131019d77af5c5186a2005b1fbe13cbb92c6df0a Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 10:09:37 +0200 Subject: [PATCH 043/165] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index e3e31db6..513c1cdb 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -8,7 +8,7 @@ - `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. ## 2.1.1 🐞 Fixed From adfc0adea97362909f7ed91f3b1652cc5812f4ab Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 11:53:30 +0200 Subject: [PATCH 044/165] feat: add name get, set and update on Channel --- .../stream_chat/lib/src/client/channel.dart | 113 +++++++++++++++--- .../test/src/api/channel_test.dart | 59 +++++++++ .../example/lib/main.dart | 16 +-- 3 files changed, 160 insertions(+), 28 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 7ea1a50b..aeee3540 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -17,6 +17,32 @@ import 'package:stream_chat/stream_chat.dart'; /// Class that manages a specific channel. /// +/// #### Channel name +/// +/// {@template name} +/// If an optional [name] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'name'. +/// +/// ```dart +/// final channel = Channel(client, type, id, name: 'Channel name'); +/// print(channel.name == channel.extraData['name']); // true +/// ``` +/// +/// Before the channel is initialized the name can be set directly: +/// ```dart +/// channel.name = 'New channel name'; +/// ``` +/// +/// To update the name after the channel has been initialized, call: +/// ```dart +/// channel.updateName('Updated channel name'); +/// ``` +/// +/// This will do a partial update to update the name. +/// {@endtemplate} +/// +/// #### Channel image +/// /// {@template image} /// If an optional [image] argument is provided in the constructor then it /// will be set on [extraData] with a key of 'image'. @@ -31,7 +57,7 @@ import 'package:stream_chat/stream_chat.dart'; /// channel.image = 'https://getstream.io/new-image'; /// ``` /// -/// To update the image after the channel has been initialized call: +/// To update the image after the channel has been initialized, call: /// ```dart /// channel.updateImage('https://getstream.io/new-image'); /// ``` @@ -49,10 +75,12 @@ class Channel { this._id, { Map? extraData, String? image, + String? name, }) : _cid = _id != null ? '$_type:$_id' : null, _extraData = { ...?extraData, if (image != null) 'image': image, + if (name != null) 'name': name, } { _client.logger.info('New Channel instance created, not yet initialized'); } @@ -98,13 +126,26 @@ class Channel { set image(String? image) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.update ' - 'to update channel image', + 'Once the channel is initialized you should use channel.updateImage ' + 'to update the channel image', ); } _extraData.addAll({'image': image}); } + /// Shortcut to set channel name. + /// + /// {@macro name} + set name(String? name) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use channel.updateName ' + 'to update the channel image', + ); + } + _extraData.addAll({'name': name}); + } + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes @@ -279,6 +320,39 @@ class Channel { ); } + /// Shortcut to get channel name. + /// + /// If no name is set this returns the channel cid, else null. + /// + /// {@macro name} + String? get name { + if (extraData.containsKey('name')) { + final name = extraData['name']! as String; + if (name.isNotEmpty) return name; + } + return cid; + } + + /// Channel [name] as a stream. + /// + /// If no name is set the stream returns the channel cid. + /// + /// The channel needs to be initialized. + /// + /// {@macro name} + Stream get nameStream { + _checkInitialized(); + return state!.channelStateStream.map( + (cs) { + if (cs.channel?.extraData.containsKey('name') ?? false) { + final name = cs.channel!.extraData['name']! as String; + if (name.isNotEmpty) return name; + } + return name!; + }, + ); + } + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; @@ -862,7 +936,7 @@ class Channel { /// Update the channel's [image]. /// - /// This is equivelant to calling [updatePartial] and providing a map with an + /// This is the same as calling [updatePartial] and providing a map with an /// 'image' key: /// /// ```dart @@ -877,17 +951,28 @@ class Channel { /// ``` Future updateImage( String image, - ) { - _checkInitialized(); + ) => + updatePartial(set: {'image': image}); - return _client.updateChannelPartial( - id!, - type, - set: { - 'image': image, - }, - ); - } + /// Update the channel's [name]. + /// + /// This is the same as calling [updatePartial] and providing a map with a + /// 'name' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'name': 'Updated channel name'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateName('Updated channel name'); + /// ``` + Future updateName( + String name, + ) => + updatePartial(set: {'name': name}); /// Edit the channel custom data. Future update( diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 381c208b..1166405a 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -89,6 +89,28 @@ void main() { expect(newChannelInstance.image, newImage); expect(newChannelInstance.extraData['image'], newImage); }); + + test('should be able to get and set `name`', () { + expect(channel.extraData.isEmpty, isTrue); + expect( + channel.name, + channelId, + reason: 'if name is not set then use channel id', + ); + + 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 @@ -217,6 +239,14 @@ void main() { } }); + test('should throw if trying to set `name`', () { + try { + channel.name = 'New name'; + } catch (e) { + expect(e, isA()); + } + }); + group('`.sendMessage`', () { test('should work fine', () async { final message = Message(id: 'test-message-id'); @@ -1246,6 +1276,35 @@ void main() { ).called(1); }); + test('`.updateName`', () async { + const name = 'Name'; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: {'name': name}, + ); + + when(() => client.updateChannelPartial( + any(), + any(), + 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( + any(), + any(), + set: {'name': name}, + ), + ).called(1); + }); + test('`.updatePartial`', () async { const set = { 'name': 'Stream Team', diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 03e791de..31410e63 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -329,21 +329,9 @@ class _MessageScreenState extends State { } } -/// 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; - } - } -} From 472b50397f4ae19266a50647217aa99fd56e69f9 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 11:54:31 +0200 Subject: [PATCH 045/165] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 513c1cdb..52496a96 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -9,6 +9,7 @@ - `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. - `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. - `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. +- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized. ## 2.1.1 🐞 Fixed From 250162b7bdf3de0c4ad6364d36f97cd7019eea9d Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 11:56:32 +0200 Subject: [PATCH 046/165] use image getter --- packages/stream_chat/lib/src/client/channel.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index aeee3540..5181eaad 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -314,9 +314,7 @@ class Channel { Stream get imageStream { _checkInitialized(); return state!.channelStateStream.map( - (cs) => - (cs.channel?.extraData['image'] as String?) ?? - (_extraData['image'] as String?), + (cs) => (cs.channel?.extraData['image'] as String?) ?? image, ); } From 4dff4d4cbe66fed7140344f6b0fdb1ff26f229af Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 12:17:19 +0200 Subject: [PATCH 047/165] chore: additional descriptions for channel update --- .../stream_chat/lib/src/client/channel.dart | 23 +++++++++++++++---- .../stream_chat/lib/src/client/client.dart | 9 ++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 5181eaad..7bf91caf 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -972,7 +972,13 @@ class Channel { ) => updatePartial(set: {'name': name}); - /// Edit the channel custom data. + /// Update the channel custom data. This replaces all of the channel data + /// with the given [channelData]. + /// + /// If you instead want to do a partial update, use [updatePartial]. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future update( Map channelData, [ Message? updateMessage, @@ -986,9 +992,18 @@ class Channel { ); } - /// Edit the channel custom data. - // TODO: This is the same description as [update]. Distinguish the two - // and provide a better description for set and unset. + /// A partial update can be used to set and unset specific custom data fields + /// when it is necessary to retain additional custom data fields on the + /// object. + /// + /// - [set] will add, or update existing attributes. + /// - [unset] will remove the attributes with the provided list of + /// values (keys). + /// + /// If you want to do a full update/replacement, use [update] instead. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future updatePartial({ Map? set, List? unset, diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 104b23fc..74abcb39 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -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 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 updateChannelPartial( String channelId, String channelType, { From 5e2c6975e21642f871b9febd519882d4824a7cb7 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 13:59:41 +0200 Subject: [PATCH 048/165] chore: update error description --- packages/stream_chat/lib/src/client/channel.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 7bf91caf..656d7f38 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -140,7 +140,7 @@ class Channel { if (_initializedCompleter.isCompleted) { throw StateError( 'Once the channel is initialized you should use channel.updateName ' - 'to update the channel image', + 'to update the channel name', ); } _extraData.addAll({'name': name}); From b3a1d42f92d49a15f8746859269a2ca1e4096798 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 12:47:20 +0530 Subject: [PATCH 049/165] chore(llc): fix tests, minor changes Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 112 +++++++++--------- .../test/src/api/channel_test.dart | 37 +++--- 2 files changed, 73 insertions(+), 76 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 656d7f38..5eee8060 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -73,14 +73,14 @@ class Channel { this._client, this._type, this._id, { - Map? extraData, - String? image, String? name, + String? image, + Map? extraData, }) : _cid = _id != null ? '$_type:$_id' : null, _extraData = { ...?extraData, - if (image != null) 'image': image, if (name != null) 'name': name, + if (image != null) 'image': image, } { _client.logger.info('New Channel instance created, not yet initialized'); } @@ -110,14 +110,17 @@ class Channel { String? _cid; final Map _extraData; - set extraData(Map extraData) { + /// Shortcut to set channel name. + /// + /// {@macro name} + set name(String? name) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.update ' - 'to update channel data', + 'Once the channel is initialized you should use `channel.updateName` ' + 'to update the channel name', ); } - _extraData.addAll(extraData); + _extraData.addAll({'name': name}); } /// Shortcut to set channel image. @@ -126,24 +129,21 @@ class Channel { set image(String? image) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.updateImage ' + 'Once the channel is initialized you should use `channel.updateImage` ' 'to update the channel image', ); } _extraData.addAll({'image': image}); } - /// Shortcut to set channel name. - /// - /// {@macro name} - set name(String? name) { + set extraData(Map extraData) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.updateName ' - 'to update the channel name', + 'Once the channel is initialized you should use `channel.update` ' + 'to update channel data', ); } - _extraData.addAll({'name': name}); + _extraData.addAll(extraData); } /// Returns true if the channel is muted. @@ -301,23 +301,6 @@ class Channel { ); } - /// Shortcut to get channel image. - /// - /// {@macro image} - String? get image => extraData['image'] as String?; - - /// Channel [image] as a stream. - /// - /// The channel needs to be initialized. - /// - /// {@macro image} - Stream get imageStream { - _checkInitialized(); - return state!.channelStateStream.map( - (cs) => (cs.channel?.extraData['image'] as String?) ?? image, - ); - } - /// Shortcut to get channel name. /// /// If no name is set this returns the channel cid, else null. @@ -342,15 +325,34 @@ class Channel { _checkInitialized(); return state!.channelStateStream.map( (cs) { - if (cs.channel?.extraData.containsKey('name') ?? false) { - final name = cs.channel!.extraData['name']! as String; + final extraData = cs.channel?.extraData; + if (extraData != null && extraData.containsKey('name')) { + final name = extraData['name']! as String; if (name.isNotEmpty) return name; } + // this can never be null once the channel is initialized return name!; }, ); } + /// Shortcut to get channel image. + /// + /// {@macro image} + String? get image => extraData['image'] as String?; + + /// Channel [image] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro image} + Stream get imageStream { + _checkInitialized(); + return state!.channelStateStream.map( + (cs) => (cs.channel?.extraData['image'] as String?) ?? image, + ); + } + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; @@ -932,26 +934,6 @@ class Channel { } } - /// Update the channel's [image]. - /// - /// This is the same as calling [updatePartial] and providing a map with an - /// 'image' key: - /// - /// ```dart - /// channel.updatePartial( - /// set: {'image': 'https://getstream.io/new-image'} - /// ); - /// ``` - /// - /// Instead do: - /// ```dart - /// channel.updateImage('https://getstream.io/new-image'); - /// ``` - Future updateImage( - String image, - ) => - updatePartial(set: {'image': image}); - /// Update the channel's [name]. /// /// This is the same as calling [updatePartial] and providing a map with a @@ -967,11 +949,27 @@ class Channel { /// ```dart /// channel.updateName('Updated channel name'); /// ``` - Future updateName( - String name, - ) => + Future updateName(String name) => updatePartial(set: {'name': name}); + /// Update the channel's [image]. + /// + /// This is the same as calling [updatePartial] and providing a map with an + /// 'image' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'image': 'https://getstream.io/new-image'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateImage('https://getstream.io/new-image'); + /// ``` + Future updateImage(String image) => + updatePartial(set: {'image': image}); + /// Update the channel custom data. This replaces all of the channel data /// with the given [channelData]. /// diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 1166405a..6b3dfda2 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -39,6 +39,7 @@ void main() { late final client = MockStreamChatClient(); const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; + const channelCid = '$channelType:$channelId'; late Channel channel; setUpAll(() { @@ -94,7 +95,7 @@ void main() { expect(channel.extraData.isEmpty, isTrue); expect( channel.name, - channelId, + channelCid, reason: 'if name is not set then use channel id', ); @@ -1256,24 +1257,23 @@ void main() { ); when(() => client.updateChannelPartial( - any(), - any(), + 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( - any(), - any(), - set: {'image': image}, - ), - ).called(1); + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + )).called(1); }); test('`.updateName`', () async { @@ -1285,24 +1285,23 @@ void main() { ); when(() => client.updateChannelPartial( - any(), - any(), + 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( - any(), - any(), - set: {'name': name}, - ), - ).called(1); + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + )).called(1); }); test('`.updatePartial`', () async { From 702f7ee9379f20e6e9b5bb6069aac4ddec31b70d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 15:00:05 +0530 Subject: [PATCH 050/165] fix(llc): don't return `cid` in case `name` is null, minor improvements Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 30 +++---------------- .../test/src/api/channel_test.dart | 6 ---- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 5eee8060..b22a57cc 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -303,37 +303,17 @@ class Channel { /// Shortcut to get channel name. /// - /// If no name is set this returns the channel cid, else null. - /// /// {@macro name} - String? get name { - if (extraData.containsKey('name')) { - final name = extraData['name']! as String; - if (name.isNotEmpty) return name; - } - return cid; - } + String? get name => extraData['name'] as String?; /// Channel [name] as a stream. /// - /// If no name is set the stream returns the channel cid. - /// /// The channel needs to be initialized. /// /// {@macro name} - Stream get nameStream { + Stream get nameStream { _checkInitialized(); - return state!.channelStateStream.map( - (cs) { - final extraData = cs.channel?.extraData; - if (extraData != null && extraData.containsKey('name')) { - final name = extraData['name']! as String; - if (name.isNotEmpty) return name; - } - // this can never be null once the channel is initialized - return name!; - }, - ); + return extraDataStream.map((it) => it['name'] as String?); } /// Shortcut to get channel image. @@ -348,9 +328,7 @@ class Channel { /// {@macro image} Stream get imageStream { _checkInitialized(); - return state!.channelStateStream.map( - (cs) => (cs.channel?.extraData['image'] as String?) ?? image, - ); + return extraDataStream.map((it) => it['image'] as String?); } /// The main Stream chat client. diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 6b3dfda2..7c5386c6 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -39,7 +39,6 @@ void main() { late final client = MockStreamChatClient(); const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; - const channelCid = '$channelType:$channelId'; late Channel channel; setUpAll(() { @@ -93,11 +92,6 @@ void main() { test('should be able to get and set `name`', () { expect(channel.extraData.isEmpty, isTrue); - expect( - channel.name, - channelCid, - reason: 'if name is not set then use channel id', - ); const name = 'Channel name'; channel.name = name; From e15575f0ae86e4649108a7dba28f2d691eaa1325 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 16:34:24 +0530 Subject: [PATCH 051/165] refactor(ui): Replace `channel.extraDataStream` with `channel.nameStream`, `channel.imageStream` in `ChannelName` and `ChannelAvatar` widget respectively. Signed-off-by: xsahil03x --- .../lib/src/channel_avatar.dart | 13 ++--- .../lib/src/channel_name.dart | 36 ++++++------ .../test/src/channel_header_test.dart | 55 +++++++++---------- .../test/src/channel_image_test.dart | 47 ++++++---------- .../test/src/channel_name_test.dart | 12 ++-- .../test/src/channel_preview_test.dart | 12 ++-- .../test/src/thread_header_test.dart | 8 +-- 7 files changed, 79 insertions(+), 104 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index 551fd278..a2390e81 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -90,12 +90,11 @@ class ChannelAvatar extends StatelessWidget { final colorTheme = chatThemeData.colorTheme; final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; - return BetterStreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, extraData) { - final channelImage = extraData['image']; - + return StreamBuilder( + stream: channel.imageStream, + initialData: channel.image, + builder: (context, snapshot) { + final channelImage = snapshot.data; if (channelImage != null) { Widget child = ClipRRect( borderRadius: borderRadius ?? previewTheme?.borderRadius, @@ -108,7 +107,7 @@ class ChannelAvatar extends StatelessWidget { imageUrl: channelImage, errorWidget: (_, __, ___) => Center( child: Text( - extraData['name']?[0] ?? '', + channel.name?[0] ?? '', style: TextStyle( color: colorTheme.barsBg, fontWeight: FontWeight.bold, diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 4ae4f935..6fb76d82 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -27,40 +27,40 @@ class ChannelName extends StatelessWidget { final client = StreamChat.of(context); final channel = StreamChannel.of(context).channel; - return BetterStreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, data) => _buildName( - data, - channel.state?.members, + assert(channel.state != null, 'Channel ${channel.id} is not initialized'); + + return StreamBuilder( + stream: channel.nameStream, + initialData: channel.name, + builder: (context, snapshot) => _buildName( + snapshot.data, + channel.state!.members, client, ), ); } Widget _buildName( - Map extraData, - List? members, + String? name, + List members, StreamChatState client, ) => LayoutBuilder( builder: (context, constraints) { - var title = context.translations.noTitleText; - if (extraData['name'] != null) { - title = extraData['name']; - } else { + var title = name; + if (title == null && members.isNotEmpty) { final otherMembers = members - ?.where((member) => member.userId != client.currentUser!.id); - if (otherMembers?.length == 1) { - if (otherMembers!.first.user != null) { + .where((member) => member.userId != client.currentUser!.id); + if (otherMembers.length == 1) { + if (otherMembers.first.user != null) { title = otherMembers.first.user!.name; } - } else if (otherMembers?.isNotEmpty == true) { + } else if (otherMembers.isNotEmpty == true) { final maxWidth = constraints.maxWidth; final maxChars = maxWidth / (textStyle?.fontSize ?? 1); var currentChars = 0; final currentMembers = []; - otherMembers!.forEach((element) { + otherMembers.forEach((element) { final newLength = currentChars + (element.user?.name.length ?? 0); if (newLength < maxChars) { @@ -77,7 +77,7 @@ class ChannelName extends StatelessWidget { } return Text( - title, + title ?? context.translations.noTitleText, style: textStyle, overflow: textOverflow, ); diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index f7157871..95d2e751 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -26,12 +26,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); @@ -91,12 +90,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -159,12 +157,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -305,12 +302,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -373,12 +369,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index f6d110dd..be2ade55 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -20,14 +20,11 @@ void main() { when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - 'image': 'imagetest', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - 'image': 'imagetest', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); await tester.pumpWidget(MaterialApp( home: StreamChat( @@ -43,7 +40,7 @@ void main() { final image = tester.widget(find.byType(CachedNetworkImage)); - expect(image.imageUrl, 'imagetest'); + expect(image.imageUrl, 'https://bit.ly/321RmWb'); }, ); @@ -59,12 +56,10 @@ void main() { when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream).thenAnswer((i) => Stream.value(null)); + when(() => channel.image).thenReturn(null); when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ Member( userId: 'user-id', @@ -132,12 +127,9 @@ void main() { when(() => clientState.currentUser).thenReturn(currentUser); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream).thenAnswer((i) => Stream.value(null)); final members = [ Member( userId: 'user-id', @@ -198,14 +190,11 @@ void main() { when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - 'image': 'imagetest', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - 'image': 'imagetest', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); await tester.pumpWidget(MaterialApp( home: StreamChat( diff --git a/packages/stream_chat_flutter/test/src/channel_name_test.dart b/packages/stream_chat_flutter/test/src/channel_name_test.dart index fc868eb0..dcfda04f 100644 --- a/packages/stream_chat_flutter/test/src/channel_name_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_name_test.dart @@ -21,17 +21,13 @@ void main() { when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); - when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ + when(() => channelState.membersStream).thenAnswer((_) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 7d0c549e..0ffa9a36 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -26,12 +26,12 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test name', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test name', - }); + when(() => channel.nameStream) + .thenAnswer((i) => Stream.value('test name')); + when(() => channel.name).thenReturn('test name'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => clientState.channels).thenReturn({ channel.cid!: channel, }); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index 1bc4c865..babe4318 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -22,12 +22,8 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.name).thenReturn('test'); + when(() => channel.nameStream).thenAnswer((i) => Stream.value('test')); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); From 9dba5a61d2a304d7702da1a9a0883ce23226cd98 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 17:01:50 +0530 Subject: [PATCH 052/165] feat(ui): add possibility to limit attachments in `MessageInput` Signed-off-by: xsahil03x --- .../stream_chat_flutter/example/lib/main.dart | 2 +- .../lib/src/message_input.dart | 462 +++++++++--------- .../stream_chat_flutter/lib/src/utils.dart | 48 ++ 3 files changed, 268 insertions(+), 244 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index eb03f89f..3d292b09 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -106,7 +106,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: MessageListView(), ), - MessageInput(), + MessageInput(attachmentLimit: 3), ], ), ); diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 9431cd22..4f61258c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -37,6 +37,16 @@ typedef ErrorListener = void Function( StackTrace? stackTrace, ); +/// A callback that can be passed to [MessageInput.onAttachmentLimitExceed]. +/// +/// This callback should not throw. +/// +/// It exists merely for showing custom error, and should not be used otherwise. +typedef AttachmentLimitExceedListener = void Function( + int limit, + String error, +); + /// Builder for attachment thumbnails typedef AttachmentThumbnailBuilder = Widget Function( BuildContext, @@ -164,7 +174,13 @@ class MessageInput extends StatefulWidget { this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoFrameRate = 30, this.onError, - }) : super(key: key); + this.attachmentLimit = 10, + this.onAttachmentLimitExceed, + }) : assert( + initialMessage == null || editMessage == null, + "Can't provide both `initialMessage` and `editMessage`", + ), + super(key: key); /// Message to edit final Message? editMessage; @@ -247,6 +263,11 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; + final int attachmentLimit; + + /// A callback for error reporting + final AttachmentLimitExceedListener? onAttachmentLimitExceed; + @override MessageInputState createState() => MessageInputState(); @@ -270,7 +291,6 @@ class MessageInputState extends State { final _imagePicker = ImagePicker(); late final FocusNode _focusNode; bool _inputEnabled = true; - bool _messageIsPresent = false; bool _commandEnabled = false; OverlayEntry? _commandsOverlay, _mentionsOverlay, _emojiOverlay; late Iterable _emojiNames; @@ -285,13 +305,15 @@ class MessageInputState extends State { KeyboardVisibilityController(); /// The editing controller passed to the input TextField - late final TextEditingController textEditingController; + late final TextEditingController _textEditingController; late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => widget.quotedMessage != null; + bool get _messageIsPresent => _textEditingController.text.trim().isNotEmpty; + @override void initState() { super.initState(); @@ -303,19 +325,19 @@ class MessageInputState extends State { _keyboardListener = _keyboardVisibilityController.onChange.listen((visible) { if (_focusNode.hasFocus) { - _onChanged(context, textEditingController.text); + _onChanged(context, _textEditingController.text); } }); } - textEditingController = + _textEditingController = widget.textEditingController ?? TextEditingController(); if (widget.editMessage != null || widget.initialMessage != null) { _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); } - textEditingController.addListener(() { - _onChanged(context, textEditingController.text); + _textEditingController.addListener(() { + _onChanged(context, _textEditingController.text); }); _focusNode.addListener(() { @@ -582,7 +604,7 @@ class MessageInputState extends State { maxLines: null, onSubmitted: (_) => sendMessage(), keyboardType: widget.keyboardType, - controller: textEditingController, + controller: _textEditingController, focusNode: _focusNode, style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, @@ -728,7 +750,6 @@ class MessageInputState extends State { .catchError((e) {}); setState(() { - _messageIsPresent = s.trim().isNotEmpty; _actionsShrunk = s.trim().isNotEmpty && ((widget.actions?.length ?? 0) + (widget.showCommandsButton ? 1 : 0) + @@ -764,15 +785,15 @@ class MessageInputState extends State { void _checkEmoji(String s, BuildContext context) { if (s.isNotEmpty && - textEditingController.selection.baseOffset > 0 && - textEditingController.text + _textEditingController.selection.baseOffset > 0 && + _textEditingController.text .substring( 0, - textEditingController.selection.baseOffset, + _textEditingController.selection.baseOffset, ) .contains(':')) { - final textToSelection = textEditingController.text - .substring(0, textEditingController.value.selection.start); + final textToSelection = _textEditingController.text + .substring(0, _textEditingController.value.selection.start); final splits = textToSelection.split(':'); final query = splits[splits.length - 2].toLowerCase(); final emoji = Emoji.byName(query); @@ -791,9 +812,9 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (s.isNotEmpty && - textEditingController.selection.baseOffset > 0 && - textEditingController.text - .substring(0, textEditingController.selection.baseOffset) + _textEditingController.selection.baseOffset > 0 && + _textEditingController.text + .substring(0, _textEditingController.selection.baseOffset) .split(' ') .last .contains('@')) { @@ -816,8 +837,7 @@ class MessageInputState extends State { if (matchedCommandsList.length == 1) { _chosenCommand = matchedCommandsList[0]; - textEditingController.clear(); - _messageIsPresent = false; + _textEditingController.clear(); setState(() { _commandEnabled = true; }); @@ -833,7 +853,7 @@ class MessageInputState extends State { } OverlayEntry? _buildCommandsOverlayEntry() { - final text = textEditingController.text.trimLeft(); + final text = _textEditingController.text.trimLeft(); final commands = StreamChannel.of(context) .channel .config @@ -1037,7 +1057,7 @@ class MessageInputState extends State { onPressed: _attachmentContainsFile && _attachments.isNotEmpty ? null : () { - pickFile(DefaultAttachmentTypes.image, true); + pickFile(DefaultAttachmentTypes.image, camera: true); }, ), IconButton( @@ -1048,7 +1068,7 @@ class MessageInputState extends State { onPressed: _attachmentContainsFile && _attachments.isNotEmpty ? null : () { - pickFile(DefaultAttachmentTypes.video, true); + pickFile(DefaultAttachmentTypes.video, camera: true); }, ), ], @@ -1107,7 +1127,7 @@ class MessageInputState extends State { if (_attachments.containsKey(media.id)) { setState(() => _attachments.remove(media.id)); } else { - _addAttachment(media); + _addAssetAttachment(media); } }, ), @@ -1119,15 +1139,13 @@ class MessageInputState extends State { ); } - void _addAttachment(AssetEntity medium) async { + void _addAssetAttachment(AssetEntity medium) async { final mediaFile = await medium.originFile.timeout( const Duration(seconds: 5), onTimeout: () => medium.originFile, ); - if (mediaFile == null) { - return; - } + if (mediaFile == null) return; var file = AttachmentFile( path: mediaFile.path, @@ -1166,11 +1184,12 @@ class MessageInputState extends State { } setState(() { - _attachments[medium.id] = Attachment( + final attachment = Attachment( id: medium.id, file: file, type: medium.type == AssetType.image ? 'image' : 'video', ); + _addAttachments([attachment]); }); } @@ -1251,8 +1270,8 @@ class MessageInputState extends State { } OverlayEntry? _buildMentionsOverlayEntry() { - final splits = textEditingController.text - .substring(0, textEditingController.value.selection.start) + final splits = _textEditingController.text + .substring(0, _textEditingController.value.selection.start) .split('@'); final query = splits.last.toLowerCase(); @@ -1314,10 +1333,10 @@ class MessageInputState extends State { splits[splits.length - 1] = m.user!.name; final rejoin = splits.join('@'); - textEditingController.value = TextEditingValue( + _textEditingController.value = TextEditingValue( text: rejoin + - textEditingController.text.substring( - textEditingController.selection.start), + _textEditingController.text.substring( + _textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1361,8 +1380,8 @@ class MessageInputState extends State { } OverlayEntry? _buildEmojiOverlay() { - final splits = textEditingController.text - .substring(0, textEditingController.value.selection.start) + final splits = _textEditingController.text + .substring(0, _textEditingController.value.selection.start) .split(':'); final query = splits.last.toLowerCase(); @@ -1473,10 +1492,10 @@ class MessageInputState extends State { void _chooseEmoji(List splits, Emoji emoji) { final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - textEditingController.value = TextEditingValue( + _textEditingController.value = TextEditingValue( text: rejoin + - textEditingController.text - .substring(textEditingController.selection.start), + _textEditingController.text + .substring(_textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1487,11 +1506,10 @@ class MessageInputState extends State { } void _setCommand(Command c) { - textEditingController.clear(); + _textEditingController.clear(); setState(() { _chosenCommand = c; _commandEnabled = true; - _messageIsPresent = false; }); _commandsOverlay?.remove(); _commandsOverlay = null; @@ -1682,7 +1700,7 @@ class MessageInputState extends State { } Widget _buildCommandButton() { - final s = textEditingController.text.trim(); + final s = _textEditingController.text.trim(); return IconButton( icon: StreamSvgIcon.lightning( @@ -1768,87 +1786,100 @@ class MessageInputState extends State { }); } else { showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), ), - context: context, - isScrollControlled: true, - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text( - context.translations.addAFileLabel, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - leading: const Icon(Icons.image), - title: Text(context.translations.uploadAPhotoLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.image); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.video_library), - title: Text(context.translations.uploadAVideoLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.video); - Navigator.pop(context); - }, - ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.camera_alt), - title: Text(context.translations.photoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.image, true); - Navigator.pop(context); - }, - ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.videocam), - title: Text(context.translations.videoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.video, true); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.insert_drive_file), - title: Text(context.translations.uploadAFileLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.file); - Navigator.pop(context); - }, - ), - ], - )); + ), + context: context, + isScrollControlled: true, + builder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + context.translations.addAFileLabel, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + leading: const Icon(Icons.image), + title: Text(context.translations.uploadAPhotoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.image); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.video_library), + title: Text(context.translations.uploadAVideoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.video); + Navigator.pop(context); + }, + ), + if (!kIsWeb) + ListTile( + leading: const Icon(Icons.camera_alt), + title: Text(context.translations.photoFromCameraLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.image, camera: true); + Navigator.pop(context); + }, + ), + if (!kIsWeb) + ListTile( + leading: const Icon(Icons.videocam), + title: Text(context.translations.videoFromCameraLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.video, camera: true); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.insert_drive_file), + title: Text(context.translations.uploadAFileLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.file); + Navigator.pop(context); + }, + ), + ], + ), + ); } } - /// Add an attachment to the sending message - /// Use this to add custom type attachments - void addAttachment(Attachment attachment) { - setState(() { - _attachments[attachment.id] = attachment.copyWith( - uploadState: attachment.uploadState, + /// Adds an attachment to the [_attachments] map + void _addAttachments(Iterable attachments) { + final length = _attachments.length + attachments.length; + if (length > widget.attachmentLimit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + widget.attachmentLimit, + 'Attachment Limit crossed ${widget.attachmentLimit}', + ); + } + return _showErrorAlert( + 'Attachment Limit crossed ${widget.attachmentLimit}', ); - }); + } + for (final attachment in attachments) { + _attachments[attachment.id] = attachment; + } } /// Pick a file from the device /// If [camera] is true then the camera will open - // ignore: avoid_positional_boolean_parameters - void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async { + void pickFile( + DefaultAttachmentTypes fileType, { + bool camera = false, + }) async { setState(() => _inputEnabled = false); AttachmentFile? file; @@ -1947,16 +1978,14 @@ class MessageInputState extends State { } } - _attachments[attachment.id] = attachment; - setState(() { - _attachments.update( - attachment.id, - (it) => it.copyWith( - file: file, - extraData: {...it.extraData} - ..update('file_size', ((_) => file!.size!)), - )); + _addAttachments([ + attachment.copyWith( + file: file, + extraData: {...attachment.extraData} + ..update('file_size', ((_) => file!.size!)), + ), + ]); }); } @@ -2005,7 +2034,7 @@ class MessageInputState extends State { /// Sends the current message Future sendMessage() async { - var text = textEditingController.text.trim(); + var text = _textEditingController.text.trim(); if (text.isEmpty && _attachments.isEmpty) { return; } @@ -2018,12 +2047,11 @@ class MessageInputState extends State { final attachments = [..._attachments.values]; - textEditingController.clear(); + _textEditingController.clear(); _attachments.clear(); widget.onQuotedMessageCleared?.call(); setState(() { - _messageIsPresent = false; _commandEnabled = false; }); @@ -2153,7 +2181,8 @@ class MessageInputState extends State { child: Text( context.translations.okLabel, style: _streamChatTheme.textTheme.bodyBold.copyWith( - color: _streamChatTheme.colorTheme.accentPrimary), + color: _streamChatTheme.colorTheme.accentPrimary, + ), ), ), ], @@ -2164,13 +2193,8 @@ class MessageInputState extends State { } void _parseExistingMessage(Message message) { - textEditingController.text = message.text!; - _messageIsPresent = true; - for (final attachment in message.attachments) { - _attachments[attachment.id] = attachment.copyWith( - uploadState: attachment.uploadState, - ); - } + _textEditingController.text = message.text!; + _addAttachments(message.attachments); } @override @@ -2196,54 +2220,6 @@ class MessageInputState extends State { } } -/// Represents a 2-tuple, or pair. -class Tuple2 { - /// Creates a new tuple value with the specified items. - const Tuple2(this.item1, this.item2); - - /// Create a new tuple value with the specified list [items]. - factory Tuple2.fromList(List items) { - if (items.length != 2) { - throw ArgumentError('items must have length 2'); - } - - return Tuple2(items[0] as T1, items[1] as T2); - } - - /// Returns the first item of the tuple - final T1 item1; - - /// Returns the second item of the tuple - final T2 item2; - - /// Returns a tuple with the first item set to the specified value. - Tuple2 withItem1(T1 v) => Tuple2(v, item2); - - /// Returns a tuple with the second item set to the specified value. - Tuple2 withItem2(T2 v) => Tuple2(item1, v); - - /// Creates a [List] containing the items of this [Tuple2]. - /// - /// The elements are in item order. The list is variable-length - /// if [growable] is true. - List toList({bool growable = false}) => - List.from([item1, item2], growable: growable); - - @override - String toString() => '[$item1, $item2]'; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Tuple2 && - runtimeType == other.runtimeType && - item1 == other.item1 && - item2 == other.item2; - - @override - int get hashCode => item1.hashCode ^ item2.hashCode; -} - class _PickerWidget extends StatefulWidget { const _PickerWidget({ Key? key, @@ -2281,74 +2257,74 @@ class _PickerWidgetState extends State<_PickerWidget> { return const Offstage(); } return FutureBuilder( - future: requestPermission, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } + future: requestPermission, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } - if (snapshot.data!) { - if (widget.containsFile) { - return GestureDetector( - onTap: () { - widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); - }, - child: Container( - constraints: const BoxConstraints.expand(), - color: widget.streamChatTheme.colorTheme.inputBg, - alignment: Alignment.center, + if (snapshot.data!) { + if (widget.containsFile) { + return GestureDetector( + onTap: () { + widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); + }, + child: Container( + constraints: const BoxConstraints.expand(), + color: widget.streamChatTheme.colorTheme.inputBg, + alignment: Alignment.center, + child: Text( + context.translations.addMoreFilesLabel, + style: TextStyle( + color: widget.streamChatTheme.colorTheme.accentPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + return MediaListView( + selectedIds: widget.selectedMedias, + onSelect: widget.onMediaSelected, + ); + } + + return InkWell( + onTap: () async { + PhotoManager.openSetting(); + }, + child: Container( + color: widget.streamChatTheme.colorTheme.inputBg, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SvgPicture.asset( + 'svgs/icon_picture_empty_state.svg', + package: 'stream_chat_flutter', + height: 140, + color: widget.streamChatTheme.colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: widget.streamChatTheme.textTheme.body.copyWith( + color: widget.streamChatTheme.colorTheme.textLowEmphasis), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Center( child: Text( - context.translations.addMoreFilesLabel, - style: TextStyle( + context.translations.allowGalleryAccessMessage, + style: widget.streamChatTheme.textTheme.bodyBold.copyWith( color: widget.streamChatTheme.colorTheme.accentPrimary, - fontWeight: FontWeight.bold, ), ), ), - ); - } - return MediaListView( - selectedIds: widget.selectedMedias, - onSelect: widget.onMediaSelected, - ); - } - - return InkWell( - onTap: () async { - PhotoManager.openSetting(); - }, - child: Container( - color: widget.streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SvgPicture.asset( - 'svgs/icon_picture_empty_state.svg', - package: 'stream_chat_flutter', - height: 140, - color: widget.streamChatTheme.colorTheme.disabled, - ), - Text( - context.translations.enablePhotoAndVideoAccessMessage, - style: widget.streamChatTheme.textTheme.body.copyWith( - color: - widget.streamChatTheme.colorTheme.textLowEmphasis), - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Center( - child: Text( - context.translations.allowGalleryAccessMessage, - style: widget.streamChatTheme.textTheme.bodyBold.copyWith( - color: widget.streamChatTheme.colorTheme.accentPrimary, - ), - ), - ), - ], - ), + ], ), - ); - }); + ), + ); + }, + ); } } diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 314a788a..d97ea6a1 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -340,3 +340,51 @@ Widget wrapAttachmentWidget( type: MaterialType.transparency, child: attachmentWidget, ); + +/// Represents a 2-tuple, or pair. +class Tuple2 { + /// Creates a new tuple value with the specified items. + const Tuple2(this.item1, this.item2); + + /// Create a new tuple value with the specified list [items]. + factory Tuple2.fromList(List items) { + if (items.length != 2) { + throw ArgumentError('items must have length 2'); + } + + return Tuple2(items[0] as T1, items[1] as T2); + } + + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns a tuple with the first item set to the specified value. + Tuple2 withItem1(T1 v) => Tuple2(v, item2); + + /// Returns a tuple with the second item set to the specified value. + Tuple2 withItem2(T2 v) => Tuple2(item1, v); + + /// Creates a [List] containing the items of this [Tuple2]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2], growable: growable); + + @override + String toString() => '[$item1, $item2]'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Tuple2 && + runtimeType == other.runtimeType && + item1 == other.item1 && + item2 == other.item2; + + @override + int get hashCode => item1.hashCode ^ item2.hashCode; +} From 0b33c258f599e2b4c49192022edc277c4d092c5d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 18:14:43 +0530 Subject: [PATCH 053/165] feat(ui, localization): Move attachment limit exceeded error to translations Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 3 +++ .../lib/src/localization/translations.dart | 6 ++++++ .../stream_chat_flutter/lib/src/message_input.dart | 12 ++++++++---- .../example/lib/add_new_lang.dart | 4 ++++ .../lib/src/stream_chat_localizations_en.dart | 4 ++++ .../lib/src/stream_chat_localizations_es.dart | 6 ++++++ .../lib/src/stream_chat_localizations_fr.dart | 6 ++++++ .../lib/src/stream_chat_localizations_hi.dart | 6 ++++++ .../lib/src/stream_chat_localizations_it.dart | 6 ++++++ .../test/translations_test.dart | 1 + 10 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5289ec30..464e40ea 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -10,6 +10,9 @@ - `GalleryHeader` - `GalleryFooter` - `ThreadHeader` +- Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message. +- Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded. + This will override the default error alert behaviour. 🔄 Changed diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index d6e5add0..e5dd5df3 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -304,6 +304,8 @@ abstract class Translations { /// The label for "Reply to message" String get replyToMessageLabel; + + String attachmentLimitExceedError(int limit); } /// Default implementation of Translation strings for the stream chat widgets @@ -664,4 +666,8 @@ class DefaultTranslations implements Translations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String attachmentLimitExceedError(int limit) => + 'Attachment limit exceeded, limit: $limit'; } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4f61258c..bce164f0 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -263,9 +263,12 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; + /// A limit for the no. of attachments that can be sent with a single message. final int attachmentLimit; - /// A callback for error reporting + /// A callback for when the [attachmentLimit] is exceeded. + /// + /// This will override the default error alert behaviour. final AttachmentLimitExceedListener? onAttachmentLimitExceed; @override @@ -1856,17 +1859,18 @@ class MessageInputState extends State { /// Adds an attachment to the [_attachments] map void _addAttachments(Iterable attachments) { + final limit = widget.attachmentLimit; final length = _attachments.length + attachments.length; - if (length > widget.attachmentLimit) { + if (length > limit) { final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; if (onAttachmentLimitExceed != null) { return onAttachmentLimitExceed( widget.attachmentLimit, - 'Attachment Limit crossed ${widget.attachmentLimit}', + context.translations.attachmentLimitExceedError(limit), ); } return _showErrorAlert( - 'Attachment Limit crossed ${widget.attachmentLimit}', + context.translations.attachmentLimitExceedError(limit), ); } for (final attachment in attachments) { diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 8a1d196e..8d2109f6 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -381,6 +381,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String attachmentLimitExceedError(int limit) => + 'Attachment limit exceeded, limit: $limit'; } void main() async { diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 5041d398..8c905b41 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -357,4 +357,8 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String attachmentLimitExceedError(int limit) => + 'Attachment limit exceeded, limit: $limit'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index b43e4ba8..10d9b2a7 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -362,4 +362,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Responder al Mensaje'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 9d8586d5..17efdd70 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -361,4 +361,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Répondre au Message'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 850e481b..c08f77a2 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -356,4 +356,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'संदेश का जवाब'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 8b2e1692..084878ee 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -358,4 +358,10 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; @override String get replyToMessageLabel => 'Rispondi al messaggio'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index 0e621159..f4eca409 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -177,6 +177,7 @@ void main() { expect(localizations.ofText, isNotNull); expect(localizations.fileText, isNotNull); expect(localizations.replyToMessageLabel, isNotNull); + expect(localizations.attachmentLimitExceedError(3), isNotNull); }); } From f8797086c5b353d2d2b197e9a67303e8ef68df82 Mon Sep 17 00:00:00 2001 From: Gordon Date: Wed, 11 Aug 2021 15:42:52 +0200 Subject: [PATCH 054/165] docs: fix broken link --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 37f7299d..b031796f 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -14,7 +14,7 @@ We have a dedicated package for adding localization to our UI widgets. It's call ## What is Localization? -If you deploy your app to users who speak another language, you'll need to internationalize (localize) it. That means you need to write the app in a way that makes it possible to localize values like text and layouts for each language or locale that the app supports. For more information, see the [Flutter documentation](https://flutter.dev/docs/development/accessibility-and-localization/**internationalization**). +If you deploy your app to users who speak another language, you'll need to internationalize (localize) it. That means you need to write the app in a way that makes it possible to localize values like text and layouts for each language or locale that the app supports. For more information, see the [Flutter documentation](https://flutter.dev/docs/development/accessibility-and-localization/internationalization). What this package allows you to do is to provide localized strings for the Stream chat widgets. For example, depending on the application locale, the Stream Chat widgets will display the appropriate language. The locale will be set automatically, based on system preferences, or you could set it programmatically in your app. The package supports several different languages, with more to be added. The package allows you to override any supported language or add a new language that isn't supported. From a15ae16f987d8c80daa83575543e33a69ab83752 Mon Sep 17 00:00:00 2001 From: Gordon Date: Wed, 11 Aug 2021 15:49:46 +0200 Subject: [PATCH 055/165] docs: add i18n to title --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index b031796f..3cad5696 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -1,7 +1,7 @@ --- id: adding_localization sidebar_position: 2 -title: Adding Localization +title: Adding Localization (i18n) --- Adding Localization To UI Widgets From 977a6fdc7d9dea613760d8f62b25845442c7d8f6 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 18:56:58 +0530 Subject: [PATCH 056/165] Update docusaurus/docs/Flutter/guides/understanding_filters.mdx Co-authored-by: Gordon Hayes --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index 8a6fc925..d4ecf94c 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -85,7 +85,7 @@ Filter.notIn('members', [user.id]) #### Filter.query -Matches values by performing text search with the specified value. +The 'query' filter matches values by performing text search with the specified value. ```dart Filter.query('name', 'demo') From 1c846b4da7df6174171e44e157b129e00824817a Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 18:57:04 +0530 Subject: [PATCH 057/165] Update docusaurus/docs/Flutter/guides/understanding_filters.mdx Co-authored-by: Gordon Hayes --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index d4ecf94c..a88f371c 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -93,7 +93,7 @@ Filter.query('name', 'demo') #### Filter.autoComplete -Matches values with the specified prefix. +The 'autoComplete' filter matches values with the specified prefix. ```dart Filter.autoComplete('name', 'demo') From c3444fe4915b6344cad4b6416a56f36d0e2787f1 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 18:57:10 +0530 Subject: [PATCH 058/165] Update docusaurus/docs/Flutter/guides/understanding_filters.mdx Co-authored-by: Gordon Hayes --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index a88f371c..e7c288b4 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -101,7 +101,7 @@ Filter.autoComplete('name', 'demo') #### Filter.exists -Matches values that exist/don't exist based on the specified boolean value. +The 'exists' filter matches values that exist, or don't exist, based on the specified boolean value. ```dart Filter.exists('name', true) From a928bea12504168c935d899c503dfcdd29490d54 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 19:00:54 +0530 Subject: [PATCH 059/165] added new guide --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index e7c288b4..9d169a80 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -109,7 +109,7 @@ Filter.exists('name', true) ### Group Queries -#### FilterOperator.and +#### Filter.and The 'and' operator combines multiple queries. @@ -120,7 +120,7 @@ final filter = Filter.and([ ]) ``` -#### FilterOperator.or +#### Filter.or Combines the provided filters and matches the values matched by at least one of the filters. @@ -131,7 +131,7 @@ final filter = Filter.or([ ]) ``` -#### FilterOperator.nor +#### Filter.nor Combines the provided filters and matches the values not matched by all the filters. From f097ef9913094e10d7ae44cea03c298a225b6bbd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 11:45:40 +0530 Subject: [PATCH 060/165] chore(ui): Apply review feedbacks. Signed-off-by: xsahil03x --- .../stream_chat_flutter/lib/src/extension.dart | 2 +- .../lib/src/message_input.dart | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 2780e1c8..19bf4523 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -48,7 +48,7 @@ extension PlatformFileX on PlatformFile { /// Extension on [InputDecoration] extension InputDecorationX on InputDecoration { - /// Merges this [AvatarThemeData] with the [other] + /// Merges this [InputDecoration] with the [other] InputDecoration merge(InputDecoration? other) { if (other == null) return this; return copyWith( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index f5e4158a..f556024d 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -50,9 +50,10 @@ typedef MentionTileBuilder = Widget Function( Member member, ); -/// Widget builder for action button -/// [defaultActionButton] is the default [IconButton] configuration -/// Use [defaultActionButton.copyWith] to easily customize it +/// 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, @@ -257,10 +258,16 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; - /// Builder for customizing attachment button. + /// 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 command button. + /// Builder for customizing the command button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. final ActionButtonBuilder? commandButtonBuilder; @override From 7b7a65cd70c56888de3cb1506faa7a5027108f86 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 11:50:07 +0530 Subject: [PATCH 061/165] fix(localization): Fix hindi translations. Signed-off-by: xsahil03x --- .../lib/src/stream_chat_localizations_hi.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 850e481b..7d7f54d2 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -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 => 'कोई शीर्षक नहीं'; From 955b5f1c0b0829d949f9218921f229b814287976 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 11:55:56 +0530 Subject: [PATCH 062/165] chore(localization): Update changelog. Signed-off-by: xsahil03x --- packages/stream_chat_localizations/CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 865741c4..0845fa05 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,6 +1,18 @@ ## 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. + +🔄 Changed + +* Some of the `Hindi` translations have been updated/changed for better understanding. + - 'रिप्लाई' -> 'जवाब दें' + - 'तस्वीरें' -> 'फ़ोटोज' + - 'बिता हुआ कल' -> 'कल' + - 'चैनल मौन है' -> 'चैनल म्यूट है' ## 1.0.2 From f18c326cf44340b2c1e57ddabcec0c806b4f6236 Mon Sep 17 00:00:00 2001 From: Gordon Date: Fri, 13 Aug 2021 08:46:50 +0200 Subject: [PATCH 063/165] docs: update title Co-authored-by: Deven Joshi --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 3cad5696..8ecc0cce 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -1,7 +1,7 @@ --- id: adding_localization sidebar_position: 2 -title: Adding Localization (i18n) +title: Adding Localization (l10n) / Internationalization (i18n) --- Adding Localization To UI Widgets From 03e9f2e5d14a4425de00b19542ff6c287e4b0631 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 14:49:53 +0530 Subject: [PATCH 064/165] Update packages/stream_chat_flutter/lib/src/message_input.dart Co-authored-by: Gordon --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index f556024d..4765bcc5 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -53,7 +53,7 @@ typedef MentionTileBuilder = Widget Function( /// Widget builder for action button. /// /// [defaultActionButton] is the default [IconButton] configuration, -/// Use [defaultActionButton.copyWith] to easily customize it. +/// use [defaultActionButton.copyWith] to easily customize it. typedef ActionButtonBuilder = Widget Function( BuildContext context, IconButton defaultActionButton, From 6c71350d395518e1b98ff0cbe25f0dbc8abc5651 Mon Sep 17 00:00:00 2001 From: Jia-Han Wu <60439733+jiahan-wu@users.noreply.github.com> Date: Fri, 13 Aug 2021 15:42:45 +0800 Subject: [PATCH 065/165] fix(ui): Fix a `MessageInput` bug. --- .../lib/src/message_input.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 9431cd22..a9d6c18b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1869,15 +1869,14 @@ class MessageInputState extends State { } 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) { From 4a9301317ad99e191df581ab00e48349cdbfa978 Mon Sep 17 00:00:00 2001 From: Jia-Han Wu Date: Fri, 13 Aug 2021 18:14:17 +0800 Subject: [PATCH 066/165] Update CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5289ec30..100853bb 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -28,6 +28,10 @@ Here's the full naming breakdown: * `MessageTheme` is now `MessageThemeData` * `UserListViewTheme` is now `UserListViewThemeData` +🐞 Fixed + +- Fixed `MessageInput` textField behaviour + ## 2.1.2 🐞 Fixed From 1a28a91be7a7ebbf58eef64264945b4aa47333a3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 16:19:14 +0530 Subject: [PATCH 067/165] Update packages/stream_chat_flutter/CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 100853bb..218843f2 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -30,7 +30,7 @@ Here's the full naming breakdown: 🐞 Fixed -- Fixed `MessageInput` textField behaviour +- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. ## 2.1.2 From d56a6df3de096bfeb2dd53af9b76b00ce0b31000 Mon Sep 17 00:00:00 2001 From: Gordon Date: Fri, 13 Aug 2021 14:34:51 +0200 Subject: [PATCH 068/165] docs: fix example folder link --- packages/stream_chat_flutter_core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter_core/README.md b/packages/stream_chat_flutter_core/README.md index 462498f6..e625aeeb 100644 --- a/packages/stream_chat_flutter_core/README.md +++ b/packages/stream_chat_flutter_core/README.md @@ -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) From 33262200a3347aae43cd8de03b7fed709f45d6e5 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 16 Aug 2021 14:21:12 +0530 Subject: [PATCH 069/165] fix: tests --- .../test/src/core/models/channel_state_test.dart | 1 + .../test/src/core/models/channel_test.dart | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index 8bd17d46..f151607a 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -29,6 +29,7 @@ void main() { DateTime.parse('2019-04-03T18:43:33.213374Z')); expect(channelState.channel?.createdBy, isA()); expect(channelState.channel?.frozen, true); + expect(channelState.channel?.cooldown, 0); expect(channelState.channel?.extraData['example'], 1); expect(channelState.channel?.extraData['name'], '#dev'); expect( diff --git a/packages/stream_chat/test/src/core/models/channel_test.dart b/packages/stream_chat/test/src/core/models/channel_test.dart index 31162bd6..e27f5970 100644 --- a/packages/stream_chat/test/src/core/models/channel_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_test.dart @@ -25,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', + }, ); }); @@ -39,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', + }, ); }); }); From fd13d1089f54396176a3e2d6539e8a65b0361564 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 16 Aug 2021 14:48:36 +0530 Subject: [PATCH 070/165] fix: tests --- packages/stream_chat/test/fixtures/channel_state_to_json.json | 1 + .../stream_chat/test/src/core/models/channel_state_test.dart | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/test/fixtures/channel_state_to_json.json b/packages/stream_chat/test/fixtures/channel_state_to_json.json index 4b8a0369..ebb74fb6 100644 --- a/packages/stream_chat/test/fixtures/channel_state_to_json.json +++ b/packages/stream_chat/test/fixtures/channel_state_to_json.json @@ -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 diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index f151607a..8bd17d46 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -29,7 +29,6 @@ void main() { DateTime.parse('2019-04-03T18:43:33.213374Z')); expect(channelState.channel?.createdBy, isA()); expect(channelState.channel?.frozen, true); - expect(channelState.channel?.cooldown, 0); expect(channelState.channel?.extraData['example'], 1); expect(channelState.channel?.extraData['name'], '#dev'); expect( From 3e4d39308f883f58733f1023aae254698c29a6bf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 16 Aug 2021 20:19:55 +0530 Subject: [PATCH 071/165] refactor(llc, ui): Make Streams non-nullable wherever possible. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 86 +++++++++---------- .../lib/src/channel_preview.dart | 2 +- .../lib/src/message_list_view.dart | 4 +- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index b22a57cc..3285db75 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -153,9 +153,9 @@ class Channel { true; /// Returns true if the channel is muted, as a stream. - Stream? get isMutedStream => _client.state.currentUserStream + Stream get isMutedStream => _client.state.currentUserStream .map((event) => - event!.channelMutes.any((element) => element.channel.cid == cid) == + event?.channelMutes.any((element) => element.channel.cid == cid) == true) .distinct(); @@ -168,105 +168,97 @@ class Channel { /// Channel configuration. ChannelConfig? get config { _checkInitialized(); - return state?._channelState.channel?.config; + return state!._channelState.channel?.config; } /// Channel configuration as a stream. - Stream? get configStream { + Stream get configStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.config); + return state!.channelStateStream.map((cs) => cs.channel?.config); } /// Channel user creator. User? get createdBy { _checkInitialized(); - return state?._channelState.channel?.createdBy; + return state!._channelState.channel?.createdBy; } /// Channel user creator as a stream. - Stream? get createdByStream { + Stream get createdByStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.createdBy); + return state!.channelStateStream.map((cs) => cs.channel?.createdBy); } /// Channel frozen status. - bool? get frozen { + bool get frozen { _checkInitialized(); - return state?._channelState.channel?.frozen; + return state!._channelState.channel?.frozen == true; } /// Channel frozen status as a stream. - Stream? get frozenStream { + Stream get frozenStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.frozen); + return state!.channelStateStream.map((cs) => cs.channel?.frozen == true); } /// Channel creation date. DateTime? get createdAt { _checkInitialized(); - return state?._channelState.channel?.createdAt; + return state!._channelState.channel?.createdAt; } /// Channel creation date as a stream. - Stream? get createdAtStream { + Stream get createdAtStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.createdAt); + return state!.channelStateStream.map((cs) => cs.channel?.createdAt); } /// Channel last message date. DateTime? get lastMessageAt { _checkInitialized(); - - return state?._channelState.channel?.lastMessageAt; + return state!._channelState.channel?.lastMessageAt; } /// Channel last message date as a stream. - Stream? get lastMessageAtStream { + Stream get lastMessageAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.lastMessageAt); + return state!.channelStateStream.map((cs) => cs.channel?.lastMessageAt); } /// Channel updated date. DateTime? get updatedAt { _checkInitialized(); - - return state?._channelState.channel?.updatedAt; + return state!._channelState.channel?.updatedAt; } /// Channel updated date as a stream. - Stream? get updatedAtStream { + Stream get updatedAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.updatedAt); + return state!.channelStateStream.map((cs) => cs.channel?.updatedAt); } /// Channel deletion date. DateTime? get deletedAt { _checkInitialized(); - - return state?._channelState.channel?.deletedAt; + return state!._channelState.channel?.deletedAt; } /// Channel deletion date as a stream. - Stream? get deletedAtStream { + Stream get deletedAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.deletedAt); + return state!.channelStateStream.map((cs) => cs.channel?.deletedAt); } /// Channel member count. int? get memberCount { _checkInitialized(); - - return state?._channelState.channel?.memberCount; + return state!._channelState.channel?.memberCount; } /// Channel member count as a stream. - Stream? get memberCountStream { + Stream get memberCountStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.memberCount); + return state!.channelStateStream.map((cs) => cs.channel?.memberCount); } /// Channel id. @@ -281,7 +273,7 @@ class Channel { /// Channel team. String? get team { _checkInitialized(); - return state?._channelState.channel?.team; + return state!._channelState.channel?.team; } /// Channel extra data. @@ -294,7 +286,7 @@ class Channel { } /// Channel extra data as a stream. - Stream> get extraDataStream { + Stream> get extraDataStream { _checkInitialized(); return state!.channelStateStream.map( (cs) => cs.channel?.extraData ?? _extraData, @@ -1728,9 +1720,9 @@ class ChannelClientState { (event) { final readList = List.from(_channelState.read); final userReadIndex = - read?.indexWhere((r) => r.user.id == event.user!.id); + read.indexWhere((r) => r.user.id == event.user!.id); - if (userReadIndex != null && userReadIndex != -1) { + if (userReadIndex != -1) { final userRead = readList.removeAt(userReadIndex); if (userRead.user.id == _channel._client.state.currentUser!.id) { unreadCount = 0; @@ -1751,15 +1743,15 @@ class ChannelClientState { List get messages => _channelState.messages; /// Channel message list as a stream. - Stream?> get messagesStream => channelStateStream + Stream> get messagesStream => channelStateStream .map((cs) => cs.messages) .distinct(const ListEquality().equals); /// Channel pinned message list. - List? get pinnedMessages => _channelState.pinnedMessages.toList(); + List get pinnedMessages => _channelState.pinnedMessages.toList(); /// Channel pinned message list as a stream. - Stream?> get pinnedMessagesStream => + Stream> get pinnedMessagesStream => channelStateStream.map((cs) => cs.pinnedMessages.toList()); /// Get channel last message. @@ -1768,8 +1760,8 @@ class ChannelClientState { : null; /// Get channel last message. - Stream get lastMessageStream => messagesStream - .map((event) => event?.isNotEmpty == true ? event!.last : null); + Stream get lastMessageStream => + messagesStream.map((event) => event.isNotEmpty ? event.last : null); /// Channel members list. List get members => _channelState.members @@ -1806,10 +1798,10 @@ class ChannelClientState { ); /// Channel read list. - List? get read => _channelState.read; + List get read => _channelState.read; /// Channel read list as a stream. - Stream?> get readStream => channelStateStream.map((cs) => cs.read); + Stream> get readStream => channelStateStream.map((cs) => cs.read); final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); @@ -2060,7 +2052,7 @@ class ChannelClientState { .toList(); updateChannelState(_channelState.copyWith( - pinnedMessages: pinnedMessages!.where(_pinIsValid()).toList(), + pinnedMessages: pinnedMessages.where(_pinIsValid()).toList(), messages: expiredMessages, )); } diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 333ee886..e01cd008 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -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) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index a3830966..54634257 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -915,7 +915,7 @@ class _MessageListViewState extends State { } 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); @@ -1204,7 +1204,7 @@ class _MessageListViewState extends State { builder: (_) => BetterStreamBuilder( stream: streamChannel!.channel.state!.messagesStream.map( (messages) => - messages!.firstWhere((m) => m.id == message.id)), + messages.firstWhere((m) => m.id == message.id)), initialData: message, builder: (_, data) => StreamChannel( channel: streamChannel!.channel, From b4329786e8682a680c26980a6dc4cc173dadf73d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 10:00:59 +0200 Subject: [PATCH 072/165] fix(ui): date dividers in not reversed message list view --- .../lib/src/message_list_view.dart | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index a3830966..82697640 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -505,8 +505,14 @@ class _MessageListViewState extends State { 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 +642,7 @@ class _MessageListViewState extends State { } Positioned _buildFloatingDateDivider(int itemCount) => Positioned( - top: widget.reverse ? 20 : null, - bottom: widget.reverse ? null : 20, + top: 20, left: 0, right: 0, child: BetterStreamBuilder>( @@ -647,16 +652,27 @@ class _MessageListViewState extends State { 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); + late final int? index; + if (widget.reverse) { + index = _getTopElementIndex(values); + } else { + index = _getBottomElementIndex(values); + } if (index == null || index <= 2 || index >= itemCount - 3) { return const Offstage(); @@ -685,6 +701,15 @@ class _MessageListViewState extends State { .index; } + int? _getBottomElementIndex(Iterable 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>( stream: Rx.combineLatest2( streamChannel!.channel.state!.isUpToDateStream.distinct(), From 79b2d91902b75a3235cde2507cf0c84f060a9999 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 10:01:50 +0200 Subject: [PATCH 073/165] chore(ui): update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index add2c3cf..9138d952 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -45,6 +45,7 @@ breakdown: 🐞 Fixed - Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. +- Fixed date dividers in not reversed `MessageListView`. ## 2.1.2 From 607ad40443b818a61afd2acf0c5b1f496e1cc245 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 10:32:56 +0200 Subject: [PATCH 074/165] fix(core): `ChannelListView` pagination on refresh --- packages/stream_chat_flutter_core/lib/src/channels_bloc.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 46aaccee..6687eef5 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -106,6 +106,9 @@ class ChannelsBlocState extends State final client = _streamChatCoreState!.client; final clear = paginationParams.offset == 0; + if (clear && _paginationEnded) { + _paginationEnded = false; + } if ((!clear && _paginationEnded) || _queryChannelsLoadingController.value == true) { From 6fb8c8731e2d6ba36f94d2856140f0b97828870a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Aug 2021 14:46:44 +0530 Subject: [PATCH 075/165] fix(ui): fix test Signed-off-by: xsahil03x --- packages/stream_chat_flutter/test/src/channel_preview_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 7d0c549e..d6f3a5ea 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -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); From d0f5f4af8322067d131ef5fc43dec76fdd882978 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 11:19:02 +0200 Subject: [PATCH 076/165] fix format --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 54634257..d7a49051 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1203,8 +1203,7 @@ class _MessageListViewState extends State { MaterialPageRoute( builder: (_) => BetterStreamBuilder( 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, From ad4be0e9d639ed51f9f358a88ec0a97657facb84 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 11:26:16 +0200 Subject: [PATCH 077/165] update changelog --- packages/stream_chat_flutter_core/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index ef24e401..65280674 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +🐞 Fixed +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh + ## 2.1.1 - Updated llc dependency From 7cdc2b98e51529e754cd287ddb00a123530403d2 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 11:59:45 +0200 Subject: [PATCH 078/165] Update packages/stream_chat_flutter/CHANGELOG.md Co-authored-by: Sahil Kumar --- packages/stream_chat_flutter/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 9138d952..b21acc7a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -45,7 +45,7 @@ breakdown: 🐞 Fixed - Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. -- Fixed date dividers in not reversed `MessageListView`. +- Fixed date dividers position/alignment in non reversed `MessageListView`. ## 2.1.2 From 56bd3b5b50ec6e461ab8bc5221c9bd3db67a1afe Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 12:34:38 +0200 Subject: [PATCH 079/165] fix(core): always show date divider --- .../lib/src/message_list_view.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 82697640..8d83ab9d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -667,7 +667,7 @@ class _MessageListViewState extends State { return const Offstage(); } - late final int? index; + int? index; if (widget.reverse) { index = _getTopElementIndex(values); } else { @@ -675,7 +675,14 @@ class _MessageListViewState extends State { } if (index == null || index <= 2 || index >= itemCount - 3) { - return const Offstage(); + if (index == null) { + return const Offstage(); + } + if (widget.reverse) { + index = itemCount - 4; + } else { + index = 2; + } } final message = messages[index - 2]; From b2eb51317f3abc2b18bd4655359ac788a385c85b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Aug 2021 16:17:19 +0530 Subject: [PATCH 080/165] refactor(ui): early return `Offstage` in case index is null. Signed-off-by: xsahil03x --- .../stream_chat_flutter/lib/src/message_list_view.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 8d83ab9d..cd9c4b45 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -674,10 +674,9 @@ class _MessageListViewState extends State { index = _getBottomElementIndex(values); } - if (index == null || index <= 2 || index >= itemCount - 3) { - if (index == null) { - return const Offstage(); - } + if (index == null) return const Offstage(); + + if (index <= 2 || index >= itemCount - 3) { if (widget.reverse) { index = itemCount - 4; } else { From 49fcaf40f5fa0d229aa38f6a4a92a20fb3a6a0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Tue, 17 Aug 2021 20:15:52 +0200 Subject: [PATCH 081/165] Fix sample app link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6758b189..62358d57 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This repository contains code for our [Dart](https://dart.dev/) and [Flutter](ht Stream allows developers to rapidly deploy scalable feeds and chat messaging with an industry leading 99.999% uptime SLA guarantee. ## Sample apps and demos -Our team maintains a dedicated repository for fully-fledged sample applications and demos. Consider checking out [GetStream/flutter-samples](https://github.com/GetStream/flutter-samples) to learn more or get started by looking at our latest [Stream Chat demo](https://github.com/GetStream/flutter-samples/tree/main/stream_chat_v1). +Our team maintains a dedicated repository for fully-fledged sample applications and demos. Consider checking out [GetStream/flutter-samples](https://github.com/GetStream/flutter-samples) to learn more or get started by looking at our latest [Stream Chat demo](https://github.com/GetStream/flutter-samples/tree/main/packages/stream_chat_v1). ## Free for Makers From f28b6b61a0dbbead99d3218838cdd7fa9802caa2 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 18 Aug 2021 15:45:46 +0530 Subject: [PATCH 082/165] fix: cooldown and teams, added correct textfield hint --- packages/stream_chat/lib/src/core/models/event.dart | 4 ++++ packages/stream_chat/lib/src/core/models/event.g.dart | 2 ++ .../lib/src/localization/translations.dart | 6 ++++++ packages/stream_chat_flutter/lib/src/message_input.dart | 4 ++++ .../lib/src/stream_chat_localizations_en.dart | 3 +++ .../lib/src/stream_chat_localizations_es.dart | 3 +++ .../lib/src/stream_chat_localizations_fr.dart | 3 +++ .../lib/src/stream_chat_localizations_hi.dart | 3 +++ .../lib/src/stream_chat_localizations_it.dart | 3 +++ 9 files changed, 31 insertions(+) diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 26831555..9250176d 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -184,6 +184,8 @@ class EventChannel extends ChannelModel { DateTime? deletedAt, required int memberCount, Map? 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 diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index 5247af17..93d2e75b 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -87,5 +87,7 @@ EventChannel _$EventChannelFromJson(Map json) { : DateTime.parse(json['deleted_at'] as String), memberCount: json['member_count'] as int? ?? 0, extraData: json['extra_data'] as Map? ?? {}, + cooldown: json['cooldown'] as int? ?? 0, + team: json['team'] as String?, ); } diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index d6e5add0..4c86e6e2 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -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; @@ -664,4 +667,7 @@ class DefaultTranslations implements Translations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String get slowModeOnLabel => 'Slow mode ON'; } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index afff3d52..23e64642 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -835,6 +835,10 @@ class MessageInputState extends State { if (_attachments.isNotEmpty) { return context.translations.addACommentOrSendLabel; } + if(_timeOut != 0 && _timeOut != null) { + return context.translations.slowModeOnLabel; + } + return context.translations.writeAMessageLabel; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 5041d398..56f2ccfe 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -357,4 +357,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String get slowModeOnLabel => 'Slow mode ON'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index b43e4ba8..5602a4a5 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -362,4 +362,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Responder al Mensaje'; + + @override + String get slowModeOnLabel => 'Modo lento activado'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 9d8586d5..f3ab6070 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -361,4 +361,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Répondre au Message'; + + @override + String get slowModeOnLabel => 'Mode lent activé'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 850e481b..386651cb 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -356,4 +356,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'संदेश का जवाब'; + + @override + String get slowModeOnLabel => 'स्लो मोड चालू'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 8b2e1692..cb82a690 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -358,4 +358,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; @override String get replyToMessageLabel => 'Rispondi al messaggio'; + + @override + String get slowModeOnLabel => 'Modalità lenta attiva'; } From 84fccf2c63fb9b4ba364dc7254a6938099fe41fd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Aug 2021 12:07:27 +0200 Subject: [PATCH 083/165] fix(ui): revert wrapping ChannelPreview in a DecoratedBox because of Slidable --- .../lib/src/channel_list_view.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index eadbed59..d6cd454b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -584,11 +584,16 @@ class _ChannelListViewState extends State { ), ], 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), + ), ), ), ); From 00ea765959e7aea360f0d8e637714533e10e7288 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 18 Aug 2021 16:00:17 +0530 Subject: [PATCH 084/165] fix: it translations --- .../lib/src/stream_chat_localizations_it.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index cb82a690..b3740f23 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -360,5 +360,5 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get replyToMessageLabel => 'Rispondi al messaggio'; @override - String get slowModeOnLabel => 'Modalità lenta attiva'; + String get slowModeOnLabel => 'Slowmode attiva'; } From c2177fa7a55a52055473a84f3a48034025dcfd6f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 18 Aug 2021 16:01:07 +0530 Subject: [PATCH 085/165] fmt --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 23e64642..90a4b8fa 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -835,7 +835,7 @@ class MessageInputState extends State { if (_attachments.isNotEmpty) { return context.translations.addACommentOrSendLabel; } - if(_timeOut != 0 && _timeOut != null) { + if (_timeOut != 0 && _timeOut != null) { return context.translations.slowModeOnLabel; } From 2a30ff2e73140977aa6ca3808328cb06e592bae8 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 18 Aug 2021 16:08:20 +0530 Subject: [PATCH 086/165] fixes --- packages/stream_chat_flutter_core/test/channels_bloc_test.dart | 1 + .../stream_chat_localizations/example/lib/add_new_lang.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index a237a23c..2aee9eba 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -558,6 +558,7 @@ void main() { config: ChannelConfig(), createdAt: DateTime.now(), memberCount: 1, + cooldown: 0, ), ); diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 8a1d196e..f7eb280e 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -381,6 +381,9 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String get slowModeOnLabel => 'Slow mode ON'; } void main() async { From a029adae249bae2b44abd7ae008eeef8f7ea27a4 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 18 Aug 2021 17:30:15 +0530 Subject: [PATCH 087/165] fixes --- packages/stream_chat_flutter/lib/src/message_input.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 90a4b8fa..7b6d2936 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -363,7 +363,6 @@ class MessageInputState extends State { if (_timeOut == 0) { timer.cancel(); } else { - print('Time left until cooldown is over: $_timeOut'); setState(() => _timeOut = _timeOut! - 1); } }); From 170f18093ed296002de1b32b3d4b85bfe357df62 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 18 Aug 2021 17:31:44 +0530 Subject: [PATCH 088/165] Update packages/stream_chat/lib/src/client/channel.dart --- packages/stream_chat/lib/src/client/channel.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 5dfcb4f9..3190dc1b 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -213,7 +213,7 @@ class Channel { return state?.channelStateStream.map((cs) => cs.channel?.cooldown); } - /// + /// Stores time at which cooldown was started DateTime? cooldownStartedAt; /// Channel creation date. From d44eceb29093a5098580076e7bd85fdeff712612 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 18 Aug 2021 18:06:44 +0530 Subject: [PATCH 089/165] ci(repo): add verify-semantic-changelog-update action workflow Signed-off-by: xsahil03x --- .github/workflows/pr_title.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml index eddb2ac1..d3c6122c 100644 --- a/.github/workflows/pr_title.yml +++ b/.github/workflows/pr_title.yml @@ -1,4 +1,4 @@ -name: 'PR Title is Conventional' +name: 'PR Title is Conventional and Semantic' on: pull_request_target: types: @@ -9,7 +9,7 @@ on: - develop jobs: - main: + conventional_pr_title: runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v3.4.0 @@ -25,3 +25,20 @@ jobs: requireScope: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + semantic_changelog_update: + needs: conventional_pr_title # Trigger after the [conventional_pr_title] completes + runs-on: ubuntu-latest + steps: + - uses: GetStream/verify-semantic-changelog-update@main + with: + scopes: | + { + "llc": "packages/stream_chat", + "ui": "packages/stream_chat_flutter", + "core": "packages/stream_chat_flutter_core", + "localization": "packages/stream_chat_flutter_localizations", + "persistence": "packages/stream_chat_persistence" + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4aaef416bf349306079a78f5366bf3cf8f373209 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 18 Aug 2021 18:57:48 +0530 Subject: [PATCH 090/165] Update .github/workflows/pr_title.yml --- .github/workflows/pr_title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml index d3c6122c..4e7c3b5f 100644 --- a/.github/workflows/pr_title.yml +++ b/.github/workflows/pr_title.yml @@ -1,4 +1,4 @@ -name: 'PR Title is Conventional and Semantic' +name: 'PR is Conventional and Semantic' on: pull_request_target: types: From b3542ffb78004a6e34be2b95fae6ccf9805b57b7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Aug 2021 11:34:22 +0200 Subject: [PATCH 091/165] fix(ui): open `MessageListView` at a specific message --- .../lib/src/message_list_view.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 75d45707..0feecf83 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:jiffy/jiffy.dart'; import 'package:rxdart/rxdart.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -454,7 +455,11 @@ class _MessageListViewState extends State { _inBetweenList = true; }, child: ScrollablePositionedList.separated( - key: ValueKey(initialIndex! + initialAlignment!), + key: (_upToDate || + initialIndex == null || + initialAlignment == null) + ? null + : ValueKey(initialIndex! + initialAlignment!), itemPositionsListener: _itemPositionListener, initialScrollIndex: initialIndex ?? 0, initialAlignment: initialAlignment ?? 0, @@ -1194,6 +1199,15 @@ class _MessageListViewState extends State { initialIndex = _initialIndex; initialAlignment = _initialAlignment; + WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { + if (initialIndex != null) { + _scrollController?.jumpTo( + index: initialIndex!, + alignment: initialAlignment ?? 0, + ); + } + }); + _messageNewListener = streamChannel!.channel.on(EventType.messageNew).listen((event) { if (_upToDate) { From d53f309a26ffd4b803e2eb0a4235a87f7208fca3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Aug 2021 11:35:26 +0200 Subject: [PATCH 092/165] remove unused import --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 0feecf83..ab333260 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -5,7 +5,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:jiffy/jiffy.dart'; import 'package:rxdart/rxdart.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; From 44e4cb5d37656a7760c2c1ce6a2da28caf678ae7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Aug 2021 11:39:12 +0200 Subject: [PATCH 093/165] update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b21acc7a..6d060d52 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -46,6 +46,7 @@ breakdown: - 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. ## 2.1.2 From 819abd091e418117b3f052ea77c345bdbc25dc10 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Aug 2021 11:43:25 +0200 Subject: [PATCH 094/165] fix format --- .../lib/src/stream_chat_localizations_hi.dart | 4 ++-- .../lib/src/stream_chat_localizations_ja.dart | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 11d7d486..247b90f8 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -340,10 +340,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get sendLabel => 'भेजें'; @override - String get withText => 'विद';//TODO: break? + String get withText => 'विद'; //TODO: break? @override - String get inText => 'इन';//TODO: break? + String get inText => 'इन'; //TODO: break? @override String get youText => 'आप'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index e8d9759e..5510a2d8 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -85,8 +85,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get emptyChatMessagesText => 'チャットがありませんが。。。'; @override - String threadSeparatorText(int replyCount)=> '$replyCount件の返信'; - + String threadSeparatorText(int replyCount) => '$replyCount件の返信'; @override String get connectedLabel => '接続しています'; From a93b585d61e75b5b7675fbb6a9c1a9dbbf86af91 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Aug 2021 11:44:23 +0200 Subject: [PATCH 095/165] fix analysis --- .../lib/src/stream_chat_localizations_ja.dart | 3 ++- .../lib/src/stream_chat_localizations_ko.dart | 15 +++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 5510a2d8..f61fcc4f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -331,7 +331,8 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get youText => 'あなた'; - // This is the word for 'customer' or 'user' because saying 'you' directly is too informal and rude + // This is the word for 'customer' or 'user' because saying 'you' directly + //is too informal and rude @override String galleryPaginationText({ diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index dbd9609d..5cc278f7 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -85,9 +85,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get emptyChatMessagesText => '아직 채팅이 없습니다...'; @override - String threadSeparatorText(int replyCount) { - return '$replyCount개의 답장'; - } + String threadSeparatorText(int replyCount) => '$replyCount개의 답장'; @override String get connectedLabel => '연결중'; @@ -283,14 +281,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get tryAgainLabel => '다시 시도합니다'; @override - String membersCountText(int count) { - return '$count명'; - } + String membersCountText(int count) => '$count명'; @override - String watchersCountText(int count) { - return '$count명이 온라인'; - } + String watchersCountText(int count) => '$count명이 온라인'; @override String get viewInfoLabel => '정보를 보기'; @@ -336,7 +330,8 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get youText => '당신'; - // This is the word for 'customer' or 'user' because saying 'you' directly is too informal and rude + // This is the word for 'customer' or 'user' because saying 'you' directly + // is too informal and rude @override String galleryPaginationText( From 894802502a82d03a46c579c046eaffb97725e901 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Aug 2021 12:16:55 +0200 Subject: [PATCH 096/165] update readme and example --- packages/stream_chat_localizations/README.md | 6 ++++++ .../stream_chat_localizations/example/lib/add_new_lang.dart | 3 +++ packages/stream_chat_localizations/example/lib/main.dart | 3 +++ .../example/lib/override_lang.dart | 3 +++ 4 files changed, 15 insertions(+) diff --git a/packages/stream_chat_localizations/README.md b/packages/stream_chat_localizations/README.md index c2ea8a72..131d6b13 100644 --- a/packages/stream_chat_localizations/README.md +++ b/packages/stream_chat_localizations/README.md @@ -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: fr it es + ja + ko ``` diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 2f373691..a64717d3 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -456,6 +456,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'), ], diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 818086df..76194992 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -73,6 +73,9 @@ class MyApp extends StatelessWidget { Locale('hi'), Locale('fr'), Locale('it'), + Locale('es'), + Locale('ja'), + Locale('ko'), ], // Add GlobalStreamChatLocalizations.delegates localizationsDelegates: GlobalStreamChatLocalizations.delegates, diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index 0e45019a..8eafd257 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -98,6 +98,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" From 5bda5db70599e8ef4d402264cf04e518c83a8f33 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Aug 2021 12:17:56 +0200 Subject: [PATCH 097/165] update changelog --- packages/stream_chat_localizations/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 865741c4..a1ae75b0 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,6 +1,8 @@ ## 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 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. ## 1.0.2 From b920919e0f872ea9d808cfe15156cc5d1ba0bb7a Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 19 Aug 2021 17:20:59 +0530 Subject: [PATCH 098/165] added tests --- .../test/src/api/channel_test.dart | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 7c5386c6..6afa60c2 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -1859,6 +1859,52 @@ void main() { ).called(1); }); + test('`.enableSlowMode`', () async { + final channelModel = ChannelModel( + cid: channelCid, + cooldown: 10, + ); + + when(() => client.enableSlowdown( + channelCid, + channelType, + 10, + )).thenAnswer((_) async => PartialUpdateChannelResponse() + ..channel = channelModel); + + final res = await channel.enableSlowMode(cooldownInterval: 10); + + expect(res, isNotNull); + + verify(() => client.enableSlowdown( + channelCid, + channelType, + any(), + )).called(1); + }); + + test('`.disableSlowMode`', () async { + final channelModel = ChannelModel( + cid: channelCid, + cooldown: 0, + ); + + when(() => client.disableSlowdown( + channelCid, + channelType, + )).thenAnswer((_) async => PartialUpdateChannelResponse() + ..channel = channelModel); + + final res = await channel.disableSlowMode(); + + expect(res, isNotNull); + + verify(() => client.disableSlowdown( + channelCid, + channelType, + )).called(1); + }); + test('`.banUser`', () async { const userId = 'test-user-id'; const options = {'key': 'value'}; From ddf33a19897641d874dd716e500565887e5b235a Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 19 Aug 2021 17:50:37 +0530 Subject: [PATCH 099/165] added tests --- .../test/src/api/channel_test.dart | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 6afa60c2..08ebd091 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -1860,15 +1860,17 @@ void main() { }); test('`.enableSlowMode`', () async { + const cooldown = 10; + final channelModel = ChannelModel( cid: channelCid, - cooldown: 10, + cooldown: cooldown, ); when(() => client.enableSlowdown( - channelCid, + channelId, channelType, - 10, + cooldown, )).thenAnswer((_) async => PartialUpdateChannelResponse() ..channel = channelModel); @@ -1877,32 +1879,28 @@ void main() { expect(res, isNotNull); verify(() => client.enableSlowdown( - channelCid, + channelId, channelType, - any(), + cooldown, )).called(1); }); test('`.disableSlowMode`', () async { final channelModel = ChannelModel( cid: channelCid, - cooldown: 0, ); when(() => client.disableSlowdown( - channelCid, - channelType, - )).thenAnswer((_) async => PartialUpdateChannelResponse() + channelId, + channelType, + )).thenAnswer((_) async => PartialUpdateChannelResponse() ..channel = channelModel); final res = await channel.disableSlowMode(); expect(res, isNotNull); - verify(() => client.disableSlowdown( - channelCid, - channelType, - )).called(1); + verify(() => client.disableSlowdown(channelId, channelType)).called(1); }); test('`.banUser`', () async { From 5cd59f2277431cf2c8842a0e82702531bee485b3 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 19 Aug 2021 18:03:34 +0530 Subject: [PATCH 100/165] fix: timer dispose, added widget test --- .../lib/src/message_input.dart | 5 +- .../test/src/message_input_test.dart | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 7b6d2936..e97e1364 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -318,6 +318,8 @@ class MessageInputState extends State { late DateTime? _cooldownStartedAt; int? _timeOut; + Timer? slowModeTimer; + @override void initState() { super.initState(); @@ -359,7 +361,7 @@ class MessageInputState extends State { StreamChannel.of(context).channel.cooldown!) { _timeOut = StreamChannel.of(context).channel.cooldown! - DateTime.now().difference(_cooldownStartedAt!).inSeconds; - Timer.periodic(const Duration(seconds: 1), (timer) { + slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_timeOut == 0) { timer.cancel(); } else { @@ -2265,6 +2267,7 @@ class MessageInputState extends State { _emojiOverlay?.remove(); _mentionsOverlay?.remove(); _keyboardListener?.cancel(); + slowModeTimer?.cancel(); super.dispose(); } diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input_test.dart index 679fb7f3..62fe0107 100644 --- a/packages/stream_chat_flutter/test/src/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input_test.dart @@ -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); + }, + ); } From 4cec36a7231bf39fc30523855d43f976a9e8349a Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 19 Aug 2021 18:13:18 +0530 Subject: [PATCH 101/165] fix: analysis --- packages/stream_chat_flutter/lib/src/message_input.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index e97e1364..de7c2788 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -318,7 +318,7 @@ class MessageInputState extends State { late DateTime? _cooldownStartedAt; int? _timeOut; - Timer? slowModeTimer; + Timer? _slowModeTimer; @override void initState() { @@ -361,7 +361,7 @@ class MessageInputState extends State { StreamChannel.of(context).channel.cooldown!) { _timeOut = StreamChannel.of(context).channel.cooldown! - DateTime.now().difference(_cooldownStartedAt!).inSeconds; - slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + _slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_timeOut == 0) { timer.cancel(); } else { @@ -2267,7 +2267,7 @@ class MessageInputState extends State { _emojiOverlay?.remove(); _mentionsOverlay?.remove(); _keyboardListener?.cancel(); - slowModeTimer?.cancel(); + _slowModeTimer?.cancel(); super.dispose(); } From 5ecbfa4ac8248fb10595eee4a86c7c071c1310ab Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Aug 2021 10:21:13 +0200 Subject: [PATCH 102/165] move comments --- .../lib/src/stream_chat_localizations_ja.dart | 4 ++-- .../lib/src/stream_chat_localizations_ko.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index f61fcc4f..e6946125 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -329,10 +329,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get inText => 'に'; - @override - String get youText => 'あなた'; // This is the word for 'customer' or 'user' because saying 'you' directly //is too informal and rude + @override + String get youText => 'あなた'; @override String galleryPaginationText({ diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 5cc278f7..2642b991 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -328,10 +328,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get inText => '에'; - @override - String get youText => '당신'; // This is the word for 'customer' or 'user' because saying 'you' directly // is too informal and rude + @override + String get youText => '당신'; @override String galleryPaginationText( From 26407a0f15567f9b1c2699b242181397b8d08bf4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Aug 2021 11:03:04 +0200 Subject: [PATCH 103/165] rename test folder --- packages/stream_chat/test/src/{api => client}/channel_test.dart | 0 packages/stream_chat/test/src/{api => client}/client_test.dart | 0 .../stream_chat/test/src/{api => client}/retry_queue_test.dart | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename packages/stream_chat/test/src/{api => client}/channel_test.dart (100%) rename packages/stream_chat/test/src/{api => client}/client_test.dart (100%) rename packages/stream_chat/test/src/{api => client}/retry_queue_test.dart (100%) diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart similarity index 100% rename from packages/stream_chat/test/src/api/channel_test.dart rename to packages/stream_chat/test/src/client/channel_test.dart diff --git a/packages/stream_chat/test/src/api/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart similarity index 100% rename from packages/stream_chat/test/src/api/client_test.dart rename to packages/stream_chat/test/src/client/client_test.dart diff --git a/packages/stream_chat/test/src/api/retry_queue_test.dart b/packages/stream_chat/test/src/client/retry_queue_test.dart similarity index 100% rename from packages/stream_chat/test/src/api/retry_queue_test.dart rename to packages/stream_chat/test/src/client/retry_queue_test.dart From 6cf63fe3ca899bf8606830e26b9c549f2141ef61 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 20 Aug 2021 16:10:01 +0530 Subject: [PATCH 104/165] added tests --- .../test/src/core/api/channel_api_test.dart | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index 8ffd778d..8dd6a3f4 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -605,4 +605,69 @@ 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 = { + 'ccooldown': 10, + }; + + final path = _getChannelUrl(channelId, channelType); + + final channelModel = ChannelModel( + id: channelId, + type: channelType, + extraData: set, + ); + + when(() => client.patch(path, data: { + 'set': { + 'cooldown': cooldown, + }, + })).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': { + 'cooldown': cooldown, + }, + })).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); + }); } From feac07057a45e5d70851c1b5f7ec423ec2ce4c67 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 20 Aug 2021 16:10:48 +0530 Subject: [PATCH 105/165] removed comments --- .../lib/src/message_input.dart | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index de7c2788..7c67c4d3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -536,36 +536,10 @@ class MessageInputState extends State { : _buildSendButton(context); } - /*if (_timeOut == null || _timeOut == 0) { - sendButton = widget.activeSendButton != null - ? InkWell( - onTap: sendMessage, - child: widget.activeSendButton, - ) - : _buildSendButton(context); - } else { - sendButton = _CountdownButton( - count: _timeOut!, - ); - } - - if (!_messageIsPresent && _attachments.isEmpty) { - sendButton = widget.idleSendButton ?? _buildIdleSendButton(context); - }*/ - return AnimatedSwitcher( duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, child: sendButton, ); - /*return AnimatedCrossFade( - crossFadeState: (_messageIsPresent || _attachments.isNotEmpty) - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: sendButton, - secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), - duration: _messageInputTheme.sendAnimationDuration!, - alignment: Alignment.center, - );*/ } Widget _buildExpandActionsButton(BuildContext context) { From f90dc4127d43a2562a9d353328a494fa2bbd0192 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 20 Aug 2021 16:13:27 +0530 Subject: [PATCH 106/165] refactor!(core): Refactor BetterStreamBuilder. 1. BREAKING: Renamed `BetterStreamBuilder.loadingBuilder` to `.noDataBuilder`. 2. BREAKING: Added non-null constraint on `BetterStreamBuilder`. 3. `BetterStreamBuilder.initialData` is now nullable/not-required. Signed-off-by: xsahil03x --- .../stream_chat_flutter_core/CHANGELOG.md | 6 +++ .../lib/src/better_stream_builder.dart | 39 +++++++++++-------- .../lib/src/channel_list_core.dart | 16 +++----- .../lib/src/message_list_core.dart | 6 +-- .../lib/src/message_search_list_core.dart | 14 +++---- .../lib/src/user_list_core.dart | 13 ++----- 6 files changed, 46 insertions(+), 48 deletions(-) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 65280674..ad331bc2 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,5 +1,11 @@ ## 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 diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index 871a8a1d..3c456246 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -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 extends StatefulWidget { +class BetterStreamBuilder 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? stream; + final Stream? 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 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 extends StatefulWidget { _BetterStreamBuilderState createState() => _BetterStreamBuilderState(); } -class _BetterStreamBuilderState extends State> { +class _BetterStreamBuilderState + extends State> { T? _lastEvent; - StreamSubscription? _subscription; + StreamSubscription? _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 extends State> { 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; } } } diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart index c59b2030..c5d3f69a 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -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 { @override Widget build(BuildContext context) => _buildListView(_channelsBloc); - StreamBuilder> _buildListView( + BetterStreamBuilder> _buildListView( ChannelsBlocState channelsBlocState, ) => - StreamBuilder>( + BetterStreamBuilder>( 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); } diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index d3d69dfb..b9b423ce 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -138,7 +138,7 @@ class MessageListCoreState extends State { return true; } - return BetterStreamBuilder?>( + return BetterStreamBuilder>( initialData: initialData, comparator: const ListEquality().equals, stream: messagesStream!.map( @@ -148,9 +148,9 @@ class MessageListCoreState extends State { ), ), 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); diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 4901c332..234b5c9a 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -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 { Widget build(BuildContext context) => _buildListView(_messageSearchBloc!); Widget _buildListView(MessageSearchBlocState messageSearchBloc) => - StreamBuilder>( + BetterStreamBuilder>( 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); } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index a45aa904..57d61fe6 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -176,16 +176,11 @@ class UserListCoreState extends State }, ); - StreamBuilder> _buildListView() => StreamBuilder( + BetterStreamBuilder> _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); } From b7ba8b5e854ce9d6ccbb6a77670d865200ef091f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 20 Aug 2021 16:13:46 +0530 Subject: [PATCH 107/165] fixed test --- .../test/src/core/api/channel_api_test.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index 8dd6a3f4..9110845d 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -611,7 +611,7 @@ void main() { const channelType = 'test-channel-type'; const cooldown = 10; const set = { - 'ccooldown': 10, + 'cooldown': 10, }; final path = _getChannelUrl(channelId, channelType); @@ -623,9 +623,7 @@ void main() { ); when(() => client.patch(path, data: { - 'set': { - 'cooldown': cooldown, - }, + 'set': set, })).thenAnswer((_) async => successResponse(path, data: { 'channel': channelModel.toJson(), })); @@ -636,9 +634,7 @@ void main() { expect(res, isNotNull); verify(() => client.patch(path, data: { - 'set': { - 'cooldown': cooldown, - }, + 'set': set, })).called(1); verifyNoMoreInteractions(client); }); From 08c2a2a8863fb1e90ce80dfa34db3678fe839559 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 20 Aug 2021 16:14:07 +0530 Subject: [PATCH 108/165] refactor(ui): update core dependency Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 5 ++++- .../lib/src/channel_preview.dart | 17 +++++++---------- .../lib/src/connection_status_builder.dart | 2 +- .../lib/src/unread_indicator.dart | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b21acc7a..acc93337 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -42,9 +42,12 @@ 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 `MessageInput` textField case where `input` is not enabled if the file picked from the + camera is null. - Fixed date dividers position/alignment in non reversed `MessageListView`. ## 2.1.2 diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index e01cd008..f3984ab9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -94,13 +94,13 @@ class ChannelPreview extends StatelessWidget { textStyle: channelPreviewTheme.titleStyle, ), ), - BetterStreamBuilder?>( + BetterStreamBuilder>( 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) { @@ -153,13 +153,10 @@ class ChannelPreview extends StatelessWidget { )); } - Widget _buildDate(BuildContext context) => BetterStreamBuilder( + Widget _buildDate(BuildContext context) => BetterStreamBuilder( 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?>( + child: BetterStreamBuilder>( 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(); } diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index fbe7ffae..2cba8eb4 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -38,7 +38,7 @@ class ConnectionStatusBuilder extends StatelessWidget { return BetterStreamBuilder( initialData: client.wsConnectionStatus, stream: stream, - loadingBuilder: loadingBuilder, + noDataBuilder: loadingBuilder, errorBuilder: (context, error) { if (errorBuilder != null) { return errorBuilder!(context, error); diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index 381bad39..e5f3027b 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -17,7 +17,7 @@ class UnreadIndicator extends StatelessWidget { Widget build(BuildContext context) { final client = StreamChat.of(context).client; return IgnorePointer( - child: BetterStreamBuilder( + child: BetterStreamBuilder( 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( From 54b162749d44c77b0b78aa0743668b621a1e9bcd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 16:34:24 +0530 Subject: [PATCH 109/165] refactor(ui): Replace `channel.extraDataStream` with `channel.nameStream`, `channel.imageStream` in `ChannelName` and `ChannelAvatar` widget respectively. Signed-off-by: xsahil03x --- .../lib/src/channel_avatar.dart | 13 ++--- .../lib/src/channel_name.dart | 36 ++++++------ .../test/src/channel_header_test.dart | 55 +++++++++---------- .../test/src/channel_image_test.dart | 47 ++++++---------- .../test/src/channel_name_test.dart | 12 ++-- .../test/src/channel_preview_test.dart | 12 ++-- .../test/src/thread_header_test.dart | 8 +-- 7 files changed, 79 insertions(+), 104 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index 551fd278..a2390e81 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -90,12 +90,11 @@ class ChannelAvatar extends StatelessWidget { final colorTheme = chatThemeData.colorTheme; final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; - return BetterStreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, extraData) { - final channelImage = extraData['image']; - + return StreamBuilder( + stream: channel.imageStream, + initialData: channel.image, + builder: (context, snapshot) { + final channelImage = snapshot.data; if (channelImage != null) { Widget child = ClipRRect( borderRadius: borderRadius ?? previewTheme?.borderRadius, @@ -108,7 +107,7 @@ class ChannelAvatar extends StatelessWidget { imageUrl: channelImage, errorWidget: (_, __, ___) => Center( child: Text( - extraData['name']?[0] ?? '', + channel.name?[0] ?? '', style: TextStyle( color: colorTheme.barsBg, fontWeight: FontWeight.bold, diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 4ae4f935..6fb76d82 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -27,40 +27,40 @@ class ChannelName extends StatelessWidget { final client = StreamChat.of(context); final channel = StreamChannel.of(context).channel; - return BetterStreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, data) => _buildName( - data, - channel.state?.members, + assert(channel.state != null, 'Channel ${channel.id} is not initialized'); + + return StreamBuilder( + stream: channel.nameStream, + initialData: channel.name, + builder: (context, snapshot) => _buildName( + snapshot.data, + channel.state!.members, client, ), ); } Widget _buildName( - Map extraData, - List? members, + String? name, + List members, StreamChatState client, ) => LayoutBuilder( builder: (context, constraints) { - var title = context.translations.noTitleText; - if (extraData['name'] != null) { - title = extraData['name']; - } else { + var title = name; + if (title == null && members.isNotEmpty) { final otherMembers = members - ?.where((member) => member.userId != client.currentUser!.id); - if (otherMembers?.length == 1) { - if (otherMembers!.first.user != null) { + .where((member) => member.userId != client.currentUser!.id); + if (otherMembers.length == 1) { + if (otherMembers.first.user != null) { title = otherMembers.first.user!.name; } - } else if (otherMembers?.isNotEmpty == true) { + } else if (otherMembers.isNotEmpty == true) { final maxWidth = constraints.maxWidth; final maxChars = maxWidth / (textStyle?.fontSize ?? 1); var currentChars = 0; final currentMembers = []; - otherMembers!.forEach((element) { + otherMembers.forEach((element) { final newLength = currentChars + (element.user?.name.length ?? 0); if (newLength < maxChars) { @@ -77,7 +77,7 @@ class ChannelName extends StatelessWidget { } return Text( - title, + title ?? context.translations.noTitleText, style: textStyle, overflow: textOverflow, ); diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index f7157871..95d2e751 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -26,12 +26,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); @@ -91,12 +90,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -159,12 +157,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -305,12 +302,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); @@ -373,12 +369,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index f6d110dd..be2ade55 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -20,14 +20,11 @@ void main() { when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - 'image': 'imagetest', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - 'image': 'imagetest', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); await tester.pumpWidget(MaterialApp( home: StreamChat( @@ -43,7 +40,7 @@ void main() { final image = tester.widget(find.byType(CachedNetworkImage)); - expect(image.imageUrl, 'imagetest'); + expect(image.imageUrl, 'https://bit.ly/321RmWb'); }, ); @@ -59,12 +56,10 @@ void main() { when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream).thenAnswer((i) => Stream.value(null)); + when(() => channel.image).thenReturn(null); when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ Member( userId: 'user-id', @@ -132,12 +127,9 @@ void main() { when(() => clientState.currentUser).thenReturn(currentUser); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream).thenAnswer((i) => Stream.value(null)); final members = [ Member( userId: 'user-id', @@ -198,14 +190,11 @@ void main() { when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - 'image': 'imagetest', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - 'image': 'imagetest', - }); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); await tester.pumpWidget(MaterialApp( home: StreamChat( diff --git a/packages/stream_chat_flutter/test/src/channel_name_test.dart b/packages/stream_chat_flutter/test/src/channel_name_test.dart index fc868eb0..dcfda04f 100644 --- a/packages/stream_chat_flutter/test/src/channel_name_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_name_test.dart @@ -21,17 +21,13 @@ void main() { when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); - when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); + when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); + when(() => channel.name).thenReturn('test'); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ + when(() => channelState.membersStream).thenAnswer((_) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index d6f3a5ea..47554605 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -28,12 +28,12 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test name', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test name', - }); + when(() => channel.nameStream) + .thenAnswer((i) => Stream.value('test name')); + when(() => channel.name).thenReturn('test name'); + when(() => channel.imageStream) + .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => clientState.channels).thenReturn({ channel.cid!: channel, }); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index 1bc4c865..babe4318 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -22,12 +22,8 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); + when(() => channel.name).thenReturn('test'); + when(() => channel.nameStream).thenAnswer((i) => Stream.value('test')); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); From fea90d4afdbc25151678ab2dd71266a53b067c5d Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 20 Aug 2021 16:20:56 +0530 Subject: [PATCH 110/165] added japanese and korean localizations --- .../lib/src/stream_chat_localizations_ja.dart | 3 +++ .../lib/src/stream_chat_localizations_ko.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index e6946125..21fc48fa 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -346,4 +346,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'メッセージに返信'; + + @override + String get slowModeOnLabel => 'スローモードオン'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 2642b991..a933ddb0 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -344,4 +344,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => '메시지에 회신합니다.'; + + @override + String get slowModeOnLabel => '느린 모드 켜짐'; } From 8627865b44405596c912bc278c1a304a0411bc3a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 20 Aug 2021 17:04:32 +0530 Subject: [PATCH 111/165] refactor(ui): use `BetterStreamBuilder` in `ChannelAvatar` and `ChannelName` Signed-off-by: xsahil03x --- .../lib/src/channel_avatar.dart | 80 +++++++++---------- .../lib/src/channel_name.dart | 39 +++++---- 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index a2390e81..ace5f310 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -90,56 +90,54 @@ class ChannelAvatar extends StatelessWidget { final colorTheme = chatThemeData.colorTheme; final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; - return StreamBuilder( + return BetterStreamBuilder( stream: channel.imageStream, initialData: channel.image, - builder: (context, snapshot) { - final channelImage = snapshot.data; - if (channelImage != null) { - Widget child = ClipRRect( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - child: Container( - constraints: constraints ?? previewTheme?.constraints, - decoration: BoxDecoration(color: colorTheme.accentPrimary), - child: InkWell( - onTap: onTap, - child: CachedNetworkImage( - imageUrl: channelImage, - errorWidget: (_, __, ___) => Center( - child: Text( - channel.name?[0] ?? '', - style: TextStyle( - color: colorTheme.barsBg, - fontWeight: FontWeight.bold, - ), + builder: (context, channelImage) { + Widget child = ClipRRect( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + child: Container( + constraints: constraints ?? previewTheme?.constraints, + decoration: BoxDecoration(color: colorTheme.accentPrimary), + child: InkWell( + onTap: onTap, + child: CachedNetworkImage( + imageUrl: channelImage, + errorWidget: (_, __, ___) => Center( + child: Text( + channel.name?[0] ?? '', + style: TextStyle( + color: colorTheme.barsBg, + fontWeight: FontWeight.bold, ), ), - fit: BoxFit.cover, ), + fit: BoxFit.cover, + ), + ), + ), + ); + + if (selected) { + child = ClipRRect( + key: const Key('selectedImage'), + borderRadius: BorderRadius.circular(selectionThickness) + + (borderRadius ?? + previewTheme?.borderRadius ?? + BorderRadius.zero), + child: Container( + constraints: constraints ?? previewTheme?.constraints, + color: selectionColor ?? colorTheme.accentPrimary, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, ), ), ); - - if (selected) { - child = ClipRRect( - key: const Key('selectedImage'), - borderRadius: BorderRadius.circular(selectionThickness) + - (borderRadius ?? - previewTheme?.borderRadius ?? - BorderRadius.zero), - child: Container( - constraints: constraints ?? previewTheme?.constraints, - color: selectionColor ?? colorTheme.accentPrimary, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: child, - ), - ), - ); - } - return child; } - + return child; + }, + noDataBuilder: (context) { final currentUser = streamChat.currentUser!; final otherMembers = channel.state!.members .where((it) => it.userId != currentUser.id) diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 6fb76d82..918aa076 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -29,33 +29,39 @@ class ChannelName extends StatelessWidget { assert(channel.state != null, 'Channel ${channel.id} is not initialized'); - return StreamBuilder( + return BetterStreamBuilder( stream: channel.nameStream, initialData: channel.name, - builder: (context, snapshot) => _buildName( - snapshot.data, + builder: (context, channelName) => Text( + channelName, + style: textStyle, + overflow: textOverflow, + ), + noDataBuilder: (context) => _generateName( + client.currentUser!, channel.state!.members, - client, ), ); } - Widget _buildName( - String? name, + Widget _generateName( + User currentUser, List members, - StreamChatState client, ) => LayoutBuilder( builder: (context, constraints) { - var title = name; - if (title == null && members.isNotEmpty) { - final otherMembers = members - .where((member) => member.userId != client.currentUser!.id); + var channelName = context.translations.noTitleText; + final otherMembers = members.where( + (member) => member.userId != currentUser.id, + ); + + if (otherMembers.isNotEmpty) { if (otherMembers.length == 1) { - if (otherMembers.first.user != null) { - title = otherMembers.first.user!.name; + final user = otherMembers.first.user; + if (user != null) { + channelName = user.name; } - } else if (otherMembers.isNotEmpty == true) { + } else { final maxWidth = constraints.maxWidth; final maxChars = maxWidth / (textStyle?.fontSize ?? 1); var currentChars = 0; @@ -71,13 +77,14 @@ class ChannelName extends StatelessWidget { final exceedingMembers = otherMembers.length - currentMembers.length; - title = '${currentMembers.map((e) => e.user?.name).join(', ')} ' + channelName = + '${currentMembers.map((e) => e.user?.name).join(', ')} ' '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; } } return Text( - title ?? context.translations.noTitleText, + channelName, style: textStyle, overflow: textOverflow, ); From 071b337194dedaea4999d4d2e9f9bb9de29b6d5d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Aug 2021 14:40:41 +0200 Subject: [PATCH 112/165] call streamchannel.of just once --- packages/stream_chat_flutter/lib/src/message_input.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 7c67c4d3..388df21b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -355,11 +355,12 @@ class MessageInputState extends State { } void _startSlowMode() { - if (StreamChannel.of(context).channel.cooldownStartedAt != null) { - _cooldownStartedAt = StreamChannel.of(context).channel.cooldownStartedAt; + final channel = StreamChannel.of(context).channel; + if (channel.cooldownStartedAt != null) { + _cooldownStartedAt = channel.cooldownStartedAt; if (DateTime.now().difference(_cooldownStartedAt!).inSeconds < - StreamChannel.of(context).channel.cooldown!) { - _timeOut = StreamChannel.of(context).channel.cooldown! - + channel.cooldown!) { + _timeOut = channel.cooldown! - DateTime.now().difference(_cooldownStartedAt!).inSeconds; _slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_timeOut == 0) { From ed6c87ef494e164e0808f659a802f342e039b7cd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 20 Aug 2021 19:16:23 +0530 Subject: [PATCH 113/165] refactor(ui): make initialIndex, initialAlignment non nullable Signed-off-by: xsahil03x --- .../lib/src/message_list_view.dart | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index ab333260..a8296231 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -298,8 +298,9 @@ class _MessageListViewState extends State { 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 { 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 { bool _topPaginationActive = false; bool _bottomPaginationActive = false; - int? initialIndex; - double? initialAlignment; + int initialIndex = 0; + double initialAlignment = 0; List messages = []; @@ -454,14 +456,12 @@ class _MessageListViewState extends State { _inBetweenList = true; }, child: ScrollablePositionedList.separated( - key: (_upToDate || - initialIndex == null || - initialAlignment == null) + key: _upToDate ? null - : ValueKey(initialIndex! + initialAlignment!), + : ValueKey(initialIndex + initialAlignment), itemPositionsListener: _itemPositionListener, - initialScrollIndex: initialIndex ?? 0, - initialAlignment: initialAlignment ?? 0, + initialScrollIndex: initialIndex, + initialAlignment: initialAlignment, physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: widget.reverse, @@ -1199,12 +1199,10 @@ class _MessageListViewState extends State { initialAlignment = _initialAlignment; WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { - if (initialIndex != null) { - _scrollController?.jumpTo( - index: initialIndex!, - alignment: initialAlignment ?? 0, - ); - } + _scrollController?.jumpTo( + index: initialIndex, + alignment: initialAlignment, + ); }); _messageNewListener = From 162a144d976c23b38228d00f61e0140b9738c043 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Aug 2021 15:56:18 +0200 Subject: [PATCH 114/165] fix(docs): update `Adding custom attachment guide` with updated code --- .../guides/adding_custom_attachments.mdx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx b/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx index fb1f7bbe..9fe7f6aa 100644 --- a/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx +++ b/docusaurus/docs/Flutter/guides/adding_custom_attachments.mdx @@ -146,21 +146,25 @@ Next, we build the Static Maps URL (Add your API key before using the code snipp } ``` -And then modify the MessageListView and tell it how to build a location attachment: +And then modify the `MessageListView` and tell it how to build a location attachment, using the `messageBuilder` property and copying the default message implementation overriding the `customAttachmentBuilders` property: ```dart MessageListView( - customAttachmentBuilders: { - 'location': (context, message, attachments) { - var attachmentWidget = Image.network( - _buildMapAttachment( - attachments[0].extraData['latitude'], - attachments[0].extraData['longitude'], - ), - ); + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + customAttachmentBuilders: { + 'location': (context, message, attachments) { + final attachmentWidget = Image.network( + _buildMapAttachment( + attachments[0].extraData['latitude'], + attachments[0].extraData['longitude'], + ), + ); - return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); - } + return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); + } + }, + ); }, ), ``` From 80aaef037a8180c619ae75010397d784511f7029 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Aug 2021 17:12:26 +0200 Subject: [PATCH 115/165] Update packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart --- .../lib/src/stream_chat_localizations_ko.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index a933ddb0..49015674 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -346,5 +346,5 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get replyToMessageLabel => '메시지에 회신합니다.'; @override - String get slowModeOnLabel => '느린 모드 켜짐'; + String get slowModeOnLabel => '슬로모드 켜짐'; } From c0b80b84c602699d35f5a2b4d9cb03c8cfe12fcc Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Fri, 20 Aug 2021 17:31:08 +0200 Subject: [PATCH 116/165] fix: add null checks on message text --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 2 +- packages/stream_chat_flutter/lib/src/message_widget.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 75d45707..9400a984 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -984,7 +984,7 @@ class _MessageListViewState extends State { 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; diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 19af5b2a..5bfa2b63 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1258,7 +1258,7 @@ class _MessageWidgetState extends State ); 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: [ From eb468edf826f69567c9cd7c5649e1c54bc4c03f9 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Fri, 20 Aug 2021 17:48:16 +0200 Subject: [PATCH 117/165] fix: add null checks to message text --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 2 +- packages/stream_chat_flutter/lib/src/message_widget.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 9400a984..d5fc3671 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -845,7 +845,7 @@ class _MessageListViewState extends State { ) { 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 = diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 5bfa2b63..ca1aa0bf 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1048,7 +1048,7 @@ class _MessageWidgetState extends State 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: 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, ), From 0f8418dd29a660fe92fa91207371b3db75eda796 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 23 Aug 2021 13:07:23 +0530 Subject: [PATCH 118/165] added changelog --- packages/stream_chat/CHANGELOG.md | 1 + packages/stream_chat_flutter/CHANGELOG.md | 2 ++ packages/stream_chat_localizations/CHANGELOG.md | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 52496a96..85bc73ef 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -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. - `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 diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b21acc7a..e9dfe04b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -25,6 +25,8 @@ typedef ActionButtonBuilder = Widget Function( > **_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 diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index d718ce12..8e13d154 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -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 [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 From 93ce9201a998c22315e306bd412f878ce955447b Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Mon, 23 Aug 2021 09:54:19 +0200 Subject: [PATCH 119/165] docs: update CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index acc93337..b2515712 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -49,6 +49,7 @@ breakdown: - 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 null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text. ## 2.1.2 From 6bbb80c2e9d15d5995b238116f0841ce0960129a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:45:30 +0530 Subject: [PATCH 120/165] fix(ui): add null check for message.text while parsing existing message. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_input.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index f7964c1e..d14e9a54 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2259,7 +2259,8 @@ class MessageInputState extends State { } void _parseExistingMessage(Message message) { - _textEditingController.text = message.text!; + final messageText = message.text; + if (messageText != null) textEditingController.text = messageText; _addAttachments(message.attachments); } From d67ff91e1283ed37fbde6605681710b0892f27f5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:46:29 +0530 Subject: [PATCH 121/165] feat(ui): Focus message input if initial message is provided. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_input.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index d14e9a54..967ec08e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2280,7 +2280,8 @@ class MessageInputState extends State { void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); _messageInputTheme = MessageInputTheme.of(context); - if (widget.editMessage != null && !_initialized) { + if ((widget.editMessage != null || widget.initialMessage != null) && + !_initialized) { FocusScope.of(context).requestFocus(_focusNode); _initialized = true; } From 0bdfff5bf6be54dbe9a9baa9b2c5d3e25a48fbd5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:49:03 +0530 Subject: [PATCH 122/165] fix(ui): disable camera and video button if attachment limit is crossed. Signed-off-by: xsahil03x --- .../lib/src/message_input.dart | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 967ec08e..329d9816 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1042,6 +1042,9 @@ class MessageInputState extends State { final _attachmentContainsFile = _attachments.values.any((it) => it.type == 'file'); + final attachmentLimitCrossed = + _attachments.length >= widget.attachmentLimit; + Color _getIconColor(int index) { final streamChatThemeData = _streamChatTheme; switch (index) { @@ -1061,15 +1064,21 @@ class MessageInputState extends State { : streamChatThemeData.colorTheme.textHighEmphasis .withOpacity(0.2)); case 2: - return _attachmentContainsFile && _attachments.isNotEmpty + return attachmentLimitCrossed ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); + : _attachmentContainsFile && _attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); case 3: - return _attachmentContainsFile && _attachments.isNotEmpty + return attachmentLimitCrossed ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); + : _attachmentContainsFile && _attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); default: return Colors.black; } @@ -1112,7 +1121,8 @@ class MessageInputState extends State { icon: StreamSvgIcon.camera( color: _getIconColor(2), ), - onPressed: _attachmentContainsFile && _attachments.isNotEmpty + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && _attachments.isNotEmpty) ? null : () { pickFile(DefaultAttachmentTypes.image, camera: true); @@ -1123,7 +1133,8 @@ class MessageInputState extends State { icon: StreamSvgIcon.record( color: _getIconColor(3), ), - onPressed: _attachmentContainsFile && _attachments.isNotEmpty + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && _attachments.isNotEmpty) ? null : () { pickFile(DefaultAttachmentTypes.video, camera: true); From 9588daf683c01472601b0184c6c15a87e99e0bd5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:49:52 +0530 Subject: [PATCH 123/165] chore(ui): apply review changes, minor ui improvements Signed-off-by: xsahil03x --- .../lib/src/message_input.dart | 175 ++++++++---------- 1 file changed, 80 insertions(+), 95 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 329d9816..bf304ec4 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -326,19 +326,18 @@ class MessageInputState extends State { bool _sendAsDm = false; bool _openFilePickerSection = false; int _filePickerIndex = 0; - double _filePickerSize = _kMinMediaPickerSize; - final KeyboardVisibilityController _keyboardVisibilityController = - KeyboardVisibilityController(); + + final _keyboardVisibilityController = KeyboardVisibilityController(); /// The editing controller passed to the input TextField - late final TextEditingController _textEditingController; + late final TextEditingController textEditingController; late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => widget.quotedMessage != null; - bool get _messageIsPresent => _textEditingController.text.trim().isNotEmpty; + bool get _messageIsPresent => textEditingController.text.trim().isNotEmpty; late DateTime? _cooldownStartedAt; int? _timeOut; @@ -356,19 +355,19 @@ class MessageInputState extends State { _keyboardListener = _keyboardVisibilityController.onChange.listen((visible) { if (_focusNode.hasFocus) { - _onChanged(context, _textEditingController.text); + _onChanged(context, textEditingController.text); } }); } - _textEditingController = + textEditingController = widget.textEditingController ?? TextEditingController(); if (widget.editMessage != null || widget.initialMessage != null) { _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); } - _textEditingController.addListener(() { - _onChanged(context, _textEditingController.text); + textEditingController.addListener(() { + _onChanged(context, textEditingController.text); }); _focusNode.addListener(() { @@ -660,7 +659,7 @@ class MessageInputState extends State { maxLines: null, onSubmitted: (_) => sendMessage(), keyboardType: widget.keyboardType, - controller: _textEditingController, + controller: textEditingController, focusNode: _focusNode, style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, @@ -843,15 +842,15 @@ class MessageInputState extends State { void _checkEmoji(String s, BuildContext context) { if (s.isNotEmpty && - _textEditingController.selection.baseOffset > 0 && - _textEditingController.text + textEditingController.selection.baseOffset > 0 && + textEditingController.text .substring( 0, - _textEditingController.selection.baseOffset, + textEditingController.selection.baseOffset, ) .contains(':')) { - final textToSelection = _textEditingController.text - .substring(0, _textEditingController.value.selection.start); + final textToSelection = textEditingController.text + .substring(0, textEditingController.value.selection.start); final splits = textToSelection.split(':'); final query = splits[splits.length - 2].toLowerCase(); final emoji = Emoji.byName(query); @@ -870,9 +869,9 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (s.isNotEmpty && - _textEditingController.selection.baseOffset > 0 && - _textEditingController.text - .substring(0, _textEditingController.selection.baseOffset) + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring(0, textEditingController.selection.baseOffset) .split(' ') .last .contains('@')) { @@ -895,7 +894,7 @@ class MessageInputState extends State { if (matchedCommandsList.length == 1) { _chosenCommand = matchedCommandsList[0]; - _textEditingController.clear(); + textEditingController.clear(); setState(() { _commandEnabled = true; }); @@ -911,7 +910,7 @@ class MessageInputState extends State { } OverlayEntry? _buildCommandsOverlayEntry() { - final text = _textEditingController.text.trimLeft(); + final text = textEditingController.text.trimLeft(); final commands = StreamChannel.of(context) .channel .config @@ -1086,7 +1085,7 @@ class MessageInputState extends State { return AnimatedContainer( duration: const Duration(milliseconds: 300), - height: _openFilePickerSection ? _filePickerSize : 0, + height: _openFilePickerSection ? _kMinMediaPickerSize : 0, child: Material( color: _streamChatTheme.colorTheme.inputBg, child: Column( @@ -1142,66 +1141,50 @@ class MessageInputState extends State { ), ], ), - GestureDetector( - onVerticalDragUpdate: (update) { - setState(() { - _filePickerSize = (_filePickerSize - update.delta.dy).clamp( - _kMinMediaPickerSize, - MediaQuery.of(context).size.height / 1.7, - ); - }); - }, - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), + DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), ), - child: SizedBox( - width: double.infinity, - child: Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: SizedBox( - width: 40, - height: 4, - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.inputBg, - borderRadius: BorderRadius.circular(4), - ), - ), - ), + ), + child: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.inputBg, + borderRadius: BorderRadius.circular(4), ), ), ), ), ), - if (_openFilePickerSection) - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - child: _PickerWidget( - filePickerIndex: _filePickerIndex, - streamChatTheme: _streamChatTheme, - containsFile: _attachmentContainsFile, - selectedMedias: _attachments.keys.toList(), - onAddMoreFilesClick: pickFile, - onMediaSelected: (media) { - if (_attachments.containsKey(media.id)) { - setState(() => _attachments.remove(media.id)); - } else { - _addAssetAttachment(media); - } - }, - ), + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + child: _PickerWidget( + filePickerIndex: _filePickerIndex, + streamChatTheme: _streamChatTheme, + containsFile: _attachmentContainsFile, + selectedMedias: _attachments.keys.toList(), + onAddMoreFilesClick: pickFile, + onMediaSelected: (media) { + if (_attachments.containsKey(media.id)) { + setState(() => _attachments.remove(media.id)); + } else { + _addAssetAttachment(media); + } + }, ), ), + ), ], ), ), @@ -1339,8 +1322,8 @@ class MessageInputState extends State { } OverlayEntry? _buildMentionsOverlayEntry() { - final splits = _textEditingController.text - .substring(0, _textEditingController.value.selection.start) + final splits = textEditingController.text + .substring(0, textEditingController.value.selection.start) .split('@'); final query = splits.last.toLowerCase(); @@ -1402,10 +1385,10 @@ class MessageInputState extends State { splits[splits.length - 1] = m.user!.name; final rejoin = splits.join('@'); - _textEditingController.value = TextEditingValue( + textEditingController.value = TextEditingValue( text: rejoin + - _textEditingController.text.substring( - _textEditingController.selection.start), + textEditingController.text.substring( + textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1449,8 +1432,8 @@ class MessageInputState extends State { } OverlayEntry? _buildEmojiOverlay() { - final splits = _textEditingController.text - .substring(0, _textEditingController.value.selection.start) + final splits = textEditingController.text + .substring(0, textEditingController.value.selection.start) .split(':'); final query = splits.last.toLowerCase(); @@ -1561,10 +1544,10 @@ class MessageInputState extends State { void _chooseEmoji(List splits, Emoji emoji) { final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - _textEditingController.value = TextEditingValue( + textEditingController.value = TextEditingValue( text: rejoin + - _textEditingController.text - .substring(_textEditingController.selection.start), + textEditingController.text + .substring(textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1575,7 +1558,7 @@ class MessageInputState extends State { } void _setCommand(Command c) { - _textEditingController.clear(); + textEditingController.clear(); setState(() { _chosenCommand = c; _commandEnabled = true; @@ -1769,7 +1752,7 @@ class MessageInputState extends State { } Widget _buildCommandButton(BuildContext context) { - final s = _textEditingController.text.trim(); + final s = textEditingController.text.trim(); final defaultButton = IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty @@ -1786,10 +1769,7 @@ class MessageInputState extends State { splashRadius: 24, onPressed: () async { if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); + setState(() => _openFilePickerSection = false); await Future.delayed(const Duration(milliseconds: 300)); } @@ -1835,10 +1815,7 @@ class MessageInputState extends State { _mentionsOverlay = null; if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); + setState(() => _openFilePickerSection = false); } else { showAttachmentModal(); } @@ -1930,6 +1907,14 @@ class MessageInputState extends State { } } + /// Add an attachment to the sending message + /// Use this to add custom type attachments + /// + /// Note: Only meant to be used from outside the state. + void addAttachment(Attachment attachment) { + setState(() => _addAttachments([attachment])); + } + /// Adds an attachment to the [_attachments] map void _addAttachments(Iterable attachments) { final limit = widget.attachmentLimit; @@ -2110,7 +2095,7 @@ class MessageInputState extends State { /// Sends the current message Future sendMessage() async { - var text = _textEditingController.text.trim(); + var text = textEditingController.text.trim(); if (text.isEmpty && _attachments.isEmpty) { return; } @@ -2123,7 +2108,7 @@ class MessageInputState extends State { final attachments = [..._attachments.values]; - _textEditingController.clear(); + textEditingController.clear(); _attachments.clear(); widget.onQuotedMessageCleared?.call(); From 958ef8e9df088977649e8cd1f1ade11a95eafa54 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 17:33:48 +0530 Subject: [PATCH 124/165] fix(ui): add 8 left padding to textInput if command is enabled. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index bf304ec4..d2b94a29 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -625,7 +625,7 @@ class MessageInputState extends State { final margin = (widget.sendButtonLocation == SendButtonLocation.inside ? const EdgeInsets.only(right: 8) : EdgeInsets.zero) + - (widget.actionsLocation != ActionsLocation.left + (widget.actionsLocation != ActionsLocation.left || _commandEnabled ? const EdgeInsets.only(left: 8) : EdgeInsets.zero); return Expanded( From 55853c2acab2756ab295eaaa0b4daff3bf5d57de Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 17:42:29 +0530 Subject: [PATCH 125/165] refactor(ui): remove redundant web pickers from MessageInput Signed-off-by: xsahil03x --- .../lib/src/message_input.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index d2b94a29..84d67dea 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1875,24 +1875,6 @@ class MessageInputState extends State { Navigator.pop(context); }, ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.camera_alt), - title: Text(context.translations.photoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.image, camera: true); - Navigator.pop(context); - }, - ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.videocam), - title: Text(context.translations.videoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.video, camera: true); - Navigator.pop(context); - }, - ), ListTile( leading: const Icon(Icons.insert_drive_file), title: Text(context.translations.uploadAFileLabel), From a37c92cdbf64afe88a019ee9ce6e1a50d1191bc2 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 23 Aug 2021 20:46:14 +0530 Subject: [PATCH 126/165] fix test --- .../lib/src/message_input.dart | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 84d67dea..4227c90d 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1163,28 +1163,29 @@ class MessageInputState extends State { ), ), ), - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - child: _PickerWidget( - filePickerIndex: _filePickerIndex, - streamChatTheme: _streamChatTheme, - containsFile: _attachmentContainsFile, - selectedMedias: _attachments.keys.toList(), - onAddMoreFilesClick: pickFile, - onMediaSelected: (media) { - if (_attachments.containsKey(media.id)) { - setState(() => _attachments.remove(media.id)); - } else { - _addAssetAttachment(media); - } - }, + if (_openFilePickerSection) + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + child: _PickerWidget( + filePickerIndex: _filePickerIndex, + streamChatTheme: _streamChatTheme, + containsFile: _attachmentContainsFile, + selectedMedias: _attachments.keys.toList(), + onAddMoreFilesClick: pickFile, + onMediaSelected: (media) { + if (_attachments.containsKey(media.id)) { + setState(() => _attachments.remove(media.id)); + } else { + _addAssetAttachment(media); + } + }, + ), ), ), - ), ], ), ), From 93f60bc2484302ecad69d6438f87920fbe5ae1a7 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 23 Aug 2021 20:51:26 +0530 Subject: [PATCH 127/165] added comment --- .../stream_chat_flutter/lib/src/localization/translations.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index cf76ea0b..e86893e9 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -309,6 +309,8 @@ abstract class Translations { /// The label for "Reply to message" String get replyToMessageLabel; + /// Label for "Attachment limit exceeded: + /// it's not possible to add more than $limit attachments" String attachmentLimitExceedError(int limit); } From f0976f0aab1a77615d2c2cfe49237ec3f93c060c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 23 Aug 2021 21:00:04 +0530 Subject: [PATCH 128/165] added comment --- packages/stream_chat_localizations/example/lib/add_new_lang.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 364761a2..28ad50fb 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -388,6 +388,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String attachmentLimitExceedError(int limit) => 'Attachment limit exceeded, limit: $limit'; + @override String get slowModeOnLabel => 'Slow mode ON'; } From 02066e3bfb617199b656d337e492700f0815e0a1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 24 Aug 2021 09:46:29 +0200 Subject: [PATCH 129/165] feat(localization): add localized strings --- .../lib/src/localization/translations.dart | 6 +++--- .../lib/src/stream_chat_localizations_es.dart | 7 +++---- .../lib/src/stream_chat_localizations_fr.dart | 7 +++---- .../lib/src/stream_chat_localizations_hi.dart | 7 +++---- .../lib/src/stream_chat_localizations_it.dart | 7 +++---- .../lib/src/stream_chat_localizations_ja.dart | 7 +++---- .../lib/src/stream_chat_localizations_ko.dart | 6 ++---- 7 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index e86893e9..246cd9bf 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -676,9 +676,9 @@ class DefaultTranslations implements Translations { String get replyToMessageLabel => 'Reply to Message'; @override - String attachmentLimitExceedError(int limit) => - 'Attachment limit exceeded, limit: $limit'; + String get slowModeOnLabel => 'Slow mode ON'; @override - String get slowModeOnLabel => 'Slow mode ON'; + String attachmentLimitExceedError(int limit) => """ +Attachment limit exceeded: it's not possible to add more than $limit attachments"""; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index aac8f1eb..69ee9237 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -366,10 +366,9 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Responder al Mensaje'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +No es posible añadir más de $limit archivos adjuntos + '''; @override String get slowModeOnLabel => 'Modo lento activado'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index ce8debf7..0727075a 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -365,10 +365,9 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Répondre au Message'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $limit pièces jointes + '''; @override String get slowModeOnLabel => 'Mode lent activé'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index e80979c3..ea1738de 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -360,10 +360,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'संदेश का जवाब'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +अटैचमेंट लिमिट: $limit अटैचमेंट से अधिक जोड़ना संभव नहीं है + '''; @override String get slowModeOnLabel => 'स्लो मोड चालू'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index abe2b3a8..64e96dc0 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -362,10 +362,9 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get replyToMessageLabel => 'Rispondi al messaggio'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +Attenzione: il limite massimo di $limit file è stato superato. + '''; @override String get slowModeOnLabel => 'Slowmode attiva'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 533b84fd..8b58630d 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -351,8 +351,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get slowModeOnLabel => 'スローモードオン'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません + '''; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 9a3a1315..a52a914f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -349,8 +349,6 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get slowModeOnLabel => '슬로모드 켜짐'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => + '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; } From 6878517d8b8c4a386b75def3e0ea63a24325875d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 24 Aug 2021 11:06:25 +0200 Subject: [PATCH 130/165] fix(docs): add translation docs link --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 4ad5eede..6492f17d 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -18,6 +18,10 @@ If you deploy your app to users who speak another language, you'll need to inter What this package allows you to do is to provide localized strings for the Stream chat widgets. For example, depending on the application locale, the Stream Chat widgets will display the appropriate language. The locale will be set automatically, based on system preferences, or you could set it programmatically in your app. The package supports several different languages, with more to be added. The package allows you to override any supported language or add a new language that isn't supported. +:::note +If you want to translate your messages (or even enable automatic translation), make sure to check [the dedicated documentation](https://getstream.io/chat/docs/flutter-dart/translation/?language=dart). +::: + ### Supported languages At the moment we support the following languages: From c71575bb25b49ed5198d62d8ac947084b5b5d53e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 24 Aug 2021 11:26:27 +0200 Subject: [PATCH 131/165] Update docusaurus/docs/Flutter/guides/adding_localization.mdx Co-authored-by: Gordon --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 6492f17d..c61f26c7 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -19,7 +19,7 @@ If you deploy your app to users who speak another language, you'll need to inter What this package allows you to do is to provide localized strings for the Stream chat widgets. For example, depending on the application locale, the Stream Chat widgets will display the appropriate language. The locale will be set automatically, based on system preferences, or you could set it programmatically in your app. The package supports several different languages, with more to be added. The package allows you to override any supported language or add a new language that isn't supported. :::note -If you want to translate your messages (or even enable automatic translation), make sure to check [the dedicated documentation](https://getstream.io/chat/docs/flutter-dart/translation/?language=dart). +If you want to translate messages, or enable automatic translation, please see the [Translation documentation](https://getstream.io/chat/docs/flutter-dart/translation/?language=dart). ::: ### Supported languages From 3f1510d4ff321c2dc6c4a92cc6bc573658c38075 Mon Sep 17 00:00:00 2001 From: Gordon Date: Tue, 24 Aug 2021 12:11:58 +0200 Subject: [PATCH 132/165] docs: use gh issue forms for bug reports --- .github/ISSUE_TEMPLATE/bug-report.md | 43 ----------- .github/ISSUE_TEMPLATE/bug_report.yaml | 98 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 43 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 14f9a2d8..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**What package are you using? What version?** - -**What platform is it about?** -- [ ] Android -- [ ] iOS -- [ ] Web -- [ ] Windows -- [ ] MacOS -- [ ] Linux - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Logs ** -Run `flutter analyze` and attach any output of that command below. -If there are any analysis errors, try resolving them before filing this issue. - -Paste the output of running `flutter doctor -v` here. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..64146824 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,98 @@ +name: Bug Report +description: Create a report to help us improve +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: dropdown + id: packages + attributes: + label: Which packages are you using? + description: You may select more than one. + multiple: true + options: + - stream_chat + - stream_chat_flutter + - stream_chat_flutter_core + - stream_chat_persistance + - stream_chat_localizations + validations: + required: true + - type: dropdown + id: platforms + attributes: + label: On what platforms did you experience the issue? + description: You may select more than one. + multiple: true + options: + - iOS + - Android + - Web + - Windows + - MacOS + - Linux + validations: + required: true + - type: textarea + id: version + attributes: + label: What version are you using? + description: Please specify the package names and versions + placeholder: package - version + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also, what did you expect to happen? + placeholder: Description of the bug and what was expected. + validations: + required: true + - type: textarea + id: repro-steps + attributes: + label: Steps to reproduce + description: How do you trigger this bug? Please walk us through it step by step. + value: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + ... + render: bash + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Supporting info to reproduce + description: Please add any relevant code, screenshots and info needed to reproduce this issue. + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + id: flutter-analyze + attributes: + label: Flutter analyze output + description: Paste the output of `flutter analyze` here. + placeholder: If there are any analysis errors, try resolving them before filing this issue. + render: shell + - type: textarea + id: flutter-doctor + attributes: + label: Flutter doctor output + description: Paste the output of `flutter doctor -v` here. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GetStream/stream-chat-flutter/blob/develop/CODE_OF_CONDUCT.md) + options: + - label: "I agree to follow this project's Code of Conduct" + required: true From 12112406f4759a241a684468f3f70f5ac610fe12 Mon Sep 17 00:00:00 2001 From: Gordon Date: Tue, 24 Aug 2021 12:13:53 +0200 Subject: [PATCH 133/165] docs: use gh issue forms for feature requests --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ------- .github/ISSUE_TEMPLATE/feature_request.yaml | 66 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 20 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 11fc491e..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000..20e36ad3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,66 @@ +name: Feature Request +description: Suggest an idea for this project +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to help us improve! + - type: dropdown + id: packages + attributes: + label: Please select which package this feature is related to. + description: You may select more than one. + multiple: true + options: + - stream_chat + - stream_chat_flutter + - stream_chat_flutter_core + - stream_chat_persistance + - stream_chat_localizations + validations: + required: true + - type: dropdown + id: platforms + attributes: + label: Which platforms would this feature impact? + description: You may select more than one. + multiple: true + options: + - iOS + - Android + - Web + - Windows + - MacOS + - Linux + - type: textarea + id: problem + attributes: + label: Is your feature request related to a problem? + description: A clear description of what the problem is. + placeholder: "Example: I'm always frustrated when [...]" + - type: textarea + id: solution + attributes: + label: "Describe the solution that you'd like." + description: A clear description of what you want to happen. + placeholder: "Example: When clicking this I want that." + - type: textarea + id: alternatives + attributes: + label: "Describe alternatives that you have considered" + description: "A clear description of any alternative solutions or features you've considered." + placeholder: "Example: Instead of this it should do that." + - type: textarea + id: additional + attributes: + label: "Additional context" + description: "Add any other context or screenshots about the feature request here." + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GetStream/stream-chat-flutter/blob/develop/CODE_OF_CONDUCT.md) + options: + - label: "I agree to follow this project's Code of Conduct" + required: true From 3cb0aec5040366cf67e25b915f16b03736a3ab3c Mon Sep 17 00:00:00 2001 From: Gordon Date: Tue, 24 Aug 2021 12:15:42 +0200 Subject: [PATCH 134/165] docs: fix punctuation --- .github/ISSUE_TEMPLATE/feature_request.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 20e36ad3..997171d2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -9,7 +9,7 @@ body: - type: dropdown id: packages attributes: - label: Please select which package this feature is related to. + label: Please select which package this feature is related to description: You may select more than one. multiple: true options: @@ -42,7 +42,7 @@ body: - type: textarea id: solution attributes: - label: "Describe the solution that you'd like." + label: "Describe the solution that you'd like" description: A clear description of what you want to happen. placeholder: "Example: When clicking this I want that." - type: textarea From 1a99a38469c9762dce061184bcf0dae2d383a586 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 25 Aug 2021 15:40:26 +0530 Subject: [PATCH 135/165] refactor(llc, ui): make cooldown non-nullable. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 12 ++--- .../lib/src/message_input.dart | 50 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 22738794..999dfb0b 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -202,15 +202,15 @@ class Channel { } /// Cooldown count - int? get cooldown { + int get cooldown { _checkInitialized(); - return state?._channelState.channel?.cooldown; + return state!._channelState.channel?.cooldown ?? 0; } /// Cooldown count as a stream - Stream? get cooldownStream { + Stream get cooldownStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.cooldown); + return state!.channelStateStream.map((cs) => cs.channel?.cooldown ?? 0); } /// Stores time at which cooldown was started @@ -540,9 +540,7 @@ class Channel { skipPush: skipPush, ); state!.addMessage(response.message); - if (cooldown! > 0) { - cooldownStartedAt = DateTime.now(); - } + if (cooldown > 0) cooldownStartedAt = DateTime.now(); return response; } catch (e) { if (e is StreamChatNetworkError && e.isRetriable) { diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4227c90d..0c787e0e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -338,15 +338,10 @@ class MessageInputState extends State { 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!); @@ -377,25 +372,33 @@ class MessageInputState extends State { }); } + int _timeOut = 0; + Timer? _slowModeTimer; + 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); - } - }); + final cooldownStartedAt = channel.cooldownStartedAt; + if (cooldownStartedAt != null) { + final diff = DateTime.now().difference(cooldownStartedAt).inSeconds; + if (diff < channel.cooldown) { + _timeOut = channel.cooldown - diff; + if (_timeOut > 0) { + _slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_timeOut == 0) { + timer.cancel(); + } else { + if (mounted) { + setState(() => _timeOut -= 1); + } + } + }); + } } } } + void _stopSlowMode() => _slowModeTimer?.cancel(); + @override Widget build(BuildContext context) { Widget child = DecoratedBox( @@ -545,10 +548,8 @@ class MessageInputState extends State { Widget _animateSendButton(BuildContext context) { late Widget sendButton; - if (_timeOut != null && _timeOut! > 0) { - sendButton = _CountdownButton( - count: _timeOut!, - ); + if (_timeOut > 0) { + sendButton = _CountdownButton(count: _timeOut); } else if (!_messageIsPresent && _attachments.isEmpty) { sendButton = widget.idleSendButton ?? _buildIdleSendButton(context); } else { @@ -2249,7 +2250,8 @@ class MessageInputState extends State { _emojiOverlay?.remove(); _mentionsOverlay?.remove(); _keyboardListener?.cancel(); - _slowModeTimer?.cancel(); + textEditingController.dispose(); + _stopSlowMode(); super.dispose(); } @@ -2259,6 +2261,8 @@ class MessageInputState extends State { void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); _messageInputTheme = MessageInputTheme.of(context); + if (widget.editMessage == null) _startSlowMode(); + if ((widget.editMessage != null || widget.initialMessage != null) && !_initialized) { FocusScope.of(context).requestFocus(_focusNode); From 2d820e3f2f572c8be6540ae7de30ef0198bf040c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 25 Aug 2021 15:46:20 +0530 Subject: [PATCH 136/165] chore(ui): flutter format, fix analyzer Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 0c787e0e..651a204c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -834,7 +834,7 @@ class MessageInputState extends State { if (_attachments.isNotEmpty) { return context.translations.addACommentOrSendLabel; } - if (_timeOut != 0 && _timeOut != null) { + if (_timeOut != 0) { return context.translations.slowModeOnLabel; } From ad907d27b9342cd71a9f6b8dc77c95c8a815ec88 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 25 Aug 2021 12:36:30 +0200 Subject: [PATCH 137/165] chore(llc,core,ui,localization): update changelogs --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 2 +- packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 2 +- packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- packages/stream_chat_localizations/CHANGELOG.md | 2 +- packages/stream_chat_localizations/pubspec.yaml | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 85bc73ef..4ad72405 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 2.1.2 🐞 Fixed diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 3633af13..390660e0 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '2.1.1'; +const PACKAGE_VERSION = '2.1.2'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 12247397..1a1a5fe2 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 2.1.1 +version: 2.1.2 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index afeaeffa..91ad7f1d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 2.2.0 ✅ Added diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 61146b72..dc3bde46 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 2.1.2 +version: 2.2.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -37,7 +37,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.1.1 + stream_chat_flutter_core: ^2.2.0 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index ad331bc2..a568d88e 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 2.2.0 🛑️ Breaking Changes from `2.1.1` - Renamed `BetterStreamBuilder.loadingBuilder` to `.noDataBuilder` diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 91fd4fe1..0708852a 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 2.1.1 +version: 2.2.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.1.1 + stream_chat: ^2.1.2 dev_dependencies: fake_async: ^1.2.0 diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 8e13d154..9450d79d 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 1.1.0 ✅ Added diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 4d982a60..0ed72acd 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 1.0.2 +version: 1.1.0 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^2.1.1 + stream_chat_flutter: ^2.2.0 dev_dependencies: flutter_test: From fc7be96374bc1d9a61c017984ed170e8466cc6c8 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 25 Aug 2021 13:34:08 +0200 Subject: [PATCH 138/165] chore(llc): update version --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter_core/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 4ad72405..f136ac5d 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.1.2 +## 2.2.0 🐞 Fixed diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 390660e0..a4490d5d 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '2.1.2'; +const PACKAGE_VERSION = '2.2.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 1a1a5fe2..ede419ab 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 2.1.2 +version: 2.2.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 0708852a..bd0eea6f 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.1.2 + stream_chat: ^2.2.0 dev_dependencies: fake_async: ^1.2.0 From 2e13874957f501f0e87e3328c2b7f53ffb3f3e00 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 25 Aug 2021 13:45:00 +0200 Subject: [PATCH 139/165] fix(core): fix changelog entry --- packages/stream_chat_localizations/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 9450d79d..398311b3 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -5,7 +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 [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. +* Added translations for cooldown mode. 🔄 Changed From a11de48305609687d100e9e2eaf516b3478893be Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 25 Aug 2021 13:49:03 +0200 Subject: [PATCH 140/165] chore(persistence): update changelog --- packages/stream_chat_persistence/CHANGELOG.md | 6 ++++++ packages/stream_chat_persistence/pubspec.yaml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index c348002e..85b9e486 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.2.0 + +- Updated llc dependency +- Added support for message.i18n +- Added support for user.language + ## 2.1.1 - Updated llc dependency diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 744005d8..a8e5f991 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 2.1.1 +version: 2.2.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,7 +18,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^2.1.1 + stream_chat: ^2.2.0 dev_dependencies: build_runner: ^2.0.1 From 588234d753928280ac4b3c91d3f7da292c61bf02 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 25 Aug 2021 14:32:30 +0200 Subject: [PATCH 141/165] fix(localization): update changelog --- packages/stream_chat_localizations/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 398311b3..f5757a28 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -6,6 +6,7 @@ * Added support for [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) locale. * Added support for [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) locale. * Added translations for cooldown mode. +* Added translations for attachmentLimitExceed. 🔄 Changed From b279b840a8f5ced77d041f307a4363b2c4f39203 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 25 Aug 2021 19:00:49 +0530 Subject: [PATCH 142/165] added new guide --- .../guides/customize_message_widget.mdx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docusaurus/docs/Flutter/guides/customize_message_widget.mdx diff --git a/docusaurus/docs/Flutter/guides/customize_message_widget.mdx b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx new file mode 100644 index 00000000..96e1e37c --- /dev/null +++ b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx @@ -0,0 +1,62 @@ +--- +id: customize_message_widget +sidebar_position: 11 +title: Customizing The MessageWidget +--- + +Customizing Text Messages + +### Introduction + +Every application provides a unique look and feel to their own messaging interface including and not +limited to fonts, colors, and shapes. + +This guide details how to customize the `MessageWidget` in the Stream Chat Flutter UI SDK. + +### Theming + +You can customize the `MessageWidget` using the `StreamChatTheme` class, so that you can change the +message theme at the top instead of creating your own `MessageWidget` at the lower implementation level. + +There are several things you can change in the theme including text styles and colors of various elements. + +You can also set a different theme for the user's own messages and messages received by them. + +Here is an example: + +```dart +StreamChatThemeData( + + /// Sets theme for user's messages + ownMessageTheme: MessageThemeData( + messageBackgroundColor: colorTheme.textHighEmphasis, + ), + + /// Sets theme for received messages + otherMessageTheme: MessageThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(8), + ), + ), + +) +``` + +#### Change message text style + +The `MessageWidget` has multiple `Text` widgets that you can manipulate the styles of. The three main +are the actual message text, user name, message links, and the message timestamp. + +```dart +MessageThemeData( + messageTextStyle: TextStyle(...), + createdAtStyle: TextStyle(...), + messageAuthorStyle: TextStyle(...), + messageLinksStyle: TextStyle(...), +) +``` + +#### Change avatar theme + +You can change the attributes of the avatar (if displayed) using the `avatarTheme` property. + From c7d81d9848263b30fa8ed564403f019027f4a7d1 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 25 Aug 2021 18:24:22 +0200 Subject: [PATCH 143/165] docs: add generate user tokens with firebase guide --- .../assets/authentication_demo_app.jpg | Bin 0 -> 29335 bytes .../firebase_authentication_dashboard.jpg | Bin 0 -> 54976 bytes .../assets/stream_chat_user_database.jpg | Bin 0 -> 55473 bytes ...er_token_generation_with_firebase_auth.mdx | 448 ++++++++++++++++++ 4 files changed, 448 insertions(+) create mode 100644 docusaurus/docs/Flutter/assets/authentication_demo_app.jpg create mode 100644 docusaurus/docs/Flutter/assets/firebase_authentication_dashboard.jpg create mode 100644 docusaurus/docs/Flutter/assets/stream_chat_user_database.jpg create mode 100644 docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx diff --git a/docusaurus/docs/Flutter/assets/authentication_demo_app.jpg b/docusaurus/docs/Flutter/assets/authentication_demo_app.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eed57c3374d9be8b0b35b9fc0e11e6b12a6d260c GIT binary patch literal 29335 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 literal 0 HcmV?d00001 diff --git a/docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx b/docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx new file mode 100644 index 00000000..a514da01 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx @@ -0,0 +1,448 @@ +--- +id: token_generation_with_firebase +sidebar_position: 5 +title: User Token Generation With Firebase Auth and Cloud Functions +--- + +Securely generate Stream Chat user tokens using Firebase Authentication and Cloud Functions. + +:::note +This guide assumes that you are familiar with Firebase Authentication and Cloud Functions for Flutter and using the Flutter Stream Chat SDK. +::: + +### Introduction + +In this guide, you'll explore how you can use Firebase Auth as an authentication provider and create Firebase Cloud functions to securely +generate Stream Chat user tokens. + +You will use Stream's [NodeJS client](https://getstream.io/chat/docs/node/?language=javascript) for Stream account creation and +token generation, and [Flutter Cloud Functions for Firebase](https://firebase.flutter.dev/docs/functions/overview) to invoke the cloud functions +from your Flutter app. + +Stream supports several different [backend clients](https://getstream.io/chat/sdk/#backend-clients) to integrate with your server. This guide only shows an easy way to integrate Stream Chat authentication using Firebase and Flutter. + +### Flutter Firebase + +See the [Flutter Firebase getting started](https://firebase.flutter.dev/docs/overview) docs for setup and installation instructions. + +You will also need to add the [Flutter Firebase Authentication](https://firebase.flutter.dev/docs/auth/overview), and [Flutter Firebase Cloud Functions](https://firebase.flutter.dev/docs/functions/overview) packages to your app. Depending on the platform that you target, there may be specific configurations that you need to do. + +#### Starting Code + +The following code shows a basic application with **FirebaseAuth** and **FirebaseFunctions**. + +You will extend this later to execute cloud functions. + +```dart +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'dart:async'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Auth(), + ), + ); + } +} + +class Auth extends StatefulWidget { + Auth({Key? key}) : super(key: key); + + @override + _AuthState createState() => _AuthState(); +} + +class _AuthState extends State { + late FirebaseAuth auth; + late FirebaseFunctions functions; + + @override + void initState() { + super.initState(); + auth = FirebaseAuth.instance; + functions = FirebaseFunctions.instance; + } + + final email = 'test@getstream.io'; + final password = 'password'; + + Future createAccount() async { + // Create Firebase account + await auth.createUserWithEmailAndPassword(email: email, password: password); + print('Firebase account created'); + } + + Future signIn() async { + // Sign in with Firebase + await auth.signInWithEmailAndPassword(email: email, password: password); + print('Firebase signed in'); + } + + Future signOut() async { + // Revoke Stream chat token. + final callable = functions.httpsCallable('revokeStreamUserToken'); + await callable(); + print('Stream user token revoked'); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AuthenticationState( + streamUser: auth.authStateChanges(), + ), + ElevatedButton( + onPressed: createAccount, + child: Text('Create account'), + ), + ElevatedButton( + onPressed: signIn, + child: Text('Sign in'), + ), + ElevatedButton( + onPressed: signOut, + child: Text('Sign out'), + ), + ], + ), + ); + } +} + +class AuthenticationState extends StatelessWidget { + const AuthenticationState({ + Key? key, + required this.streamUser, + }) : super(key: key); + + final Stream streamUser; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: streamUser, + builder: (context, snapshot) { + if (snapshot.hasData) { + return (snapshot.data != null) + ? Text('Authenticated') + : Text('Not Authenticated'); + } + return Text('Not Authenticated'); + }, + ); + } +} + +``` + +Running the above will give this: + +![](../assets/authentication_demo_app.jpg) + +The `Auth` widget handles all of the authentication logic. It initializes a `FirebaseAuth.instance` and uses that +in the `createAccount`, `signIn` and `signOut` methods. There is a button to envoke each of these methods. + +The `FirebaseFunctions.instance` will be used later in this guide. + +The `AuthenticationState` widget listens to `auth.authStateChanges()` to display a message +indicating if a user is authenticated. + +### Firebase Cloud Functions + +Firebase Cloud Functions allows you to extend Firebase with custom operations that an event can trigger: +- **Internal event**: For example, when creating a new Firebase account this is automatically triggered. +- **External event**: For example, directly calling a cloud function from your Flutter application. + +To set up your local environment to deploy cloud functions, please see the +[Cloud Functions getting started](https://firebase.flutter.dev/docs/overview) docs. + +After initializing your project with cloud functions, you should have a **functions** folder in your project, including a `package.json` file. + +There should be two dependencies already added, **firebase-admin** and **firebase-functions**. You will also need to add the **stream-chat** dependency. + +Navigate to the **functions** folder and run `npm install stream-chat --save-prod`. + +This will install the node module and add it as a dependency to `package.json`. + +Now open `index.js` and add the following (this is the complete example): + +```js +const StreamChat = require('stream-chat').StreamChat; +const functions = require("firebase-functions"); +const admin = require("firebase-admin"); + +admin.initializeApp(); + +const serverClient = StreamChat.getInstance(functions.config().stream.key, functions.config().stream.secret); + + +// When a user is deleted from Firebase their associated Stream account is also deleted. +exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => { + return serverClient.deleteUser(user.uid); +}); + +// Create a Stream user and return auth token. +exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + // Create user using the serverClient. + await serverClient.upsertUser({ + id: context.auth.uid, + name: context.auth.token.name, + email: context.auth.token.email, + image: context.auth.token.image, + }); + + /// Create and return user auth token. + return serverClient.createToken(context.auth.uid); + } catch (err) { + console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not create Stream user"); + } + } +}); + +// Get Stream user token. +exports.getStreamUserToken = functions.https.onCall((data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + return serverClient.createToken(context.auth.uid); + } catch (err) { + console.error(`Unable to get user token with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not get Stream user"); + } + } +}); + +// Revoke the authenticated user's Stream chat token. +exports.revokeStreamUserToken = functions.https.onCall((data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + return serverClient.revokeUserToken(context.auth.uid); + } catch (err) { + console.error(`Unable to revoke user token with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not get Stream user"); + } + } +}); + +``` + +First, you import the necessary packages and call `admin.initializeApp();` to set up Firebase cloud functions. + +Next, you initialize the **StreamChat** server client by calling `StreamChat.getInstance`. This function requires your Stream app's +**token** and **secret**. You can get this from the Stream Dashboard for your app. + +Set these values as environment data on Firebase Functions. + +```bash + firebase functions:config:set stream.key="app-key" stream.secret="app-secret" +``` + +*Replace **app-key** and **app-secret** with the values for your Stream app.* + +This creates an object of **stream** with properties **key** and **secret**. To access this environment +data use `functions.config().stream.key` and `functions.config().stream.secret`. + +See the [Firebase environment configuration](https://firebase.google.com/docs/functions/config-env) +documentation for additional information. + +To deploy these functions to Firebase, run: + +```bash +firebase deploy --only functions +``` + +### Create a Stream User and Get the User's Token + +In the `createStreamUserAndGetToken` cloud function you create an `onCall` HTTPS handler, which exposes +a cloud function that can be envoked from your Flutter app. + +```js +// Create a Stream user and return auth token. +exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => { + // Checking that the user is authenticated. + if (!context.auth) { + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' + + 'while authenticated.'); + } else { + try { + // Create user using the serverClient. + await serverClient.upsertUser({ + id: context.auth.uid, + name: context.auth.token.name, + email: context.auth.token.email, + image: context.auth.token.image, + }); + + /// Create and return user auth token. + return serverClient.createToken(context.auth.uid); + } catch (err) { + console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`); + // Throwing an HttpsError so that the client gets the error details. + throw new functions.https.HttpsError('aborted', "Could not create Stream user"); + } + } +}); +``` + +This function first does a check to see that the client that calls it is authenticated, +by ensuring that `context.auth` is not null. If it is null, then it throws an `HttpsError` with a descriptive +message. This error can be caught in your Flutter application. + +If the caller is authenticated the function proceeds to use the `serverClient` to create a new Stream Chat +user by calling the `upsertUser` method and passing in some user data. It uses the authenticated caller's **uid** as an **id**. + +After the user is created it generates a token for that user. This token is then returned to the caller. + +To call this from Flutter, you will need to use the `cloud_functions` package. + +Update the **createAccount** method in your Flutter code to the following: + +```dart +Future createAccount() async { + // Create Firebase account + await auth.createUserWithEmailAndPassword(email: email, password: password); + print('Firebase account created'); + + // Create Stream user and get token + final callable = functions.httpsCallable('createStreamUserAndGetToken'); + final results = await callable(); + print('Stream account created, token: ${results.data}'); +} +``` + +Calling this method will do the following: +1. Create a new Firebase User and authenticate that user. +2. Call the `createStreamUserAndGetToken` cloud function and get the Stream user token for the authenticated user. + +As you can see, calling a cloud function is easy and will also send all the necessary user authentication information (such as the UID) +in the request. + +Once you have the Stream user token, you can authenticate your Stream Chat user as you normally would. + +Please see our [initialization documention](https://getstream.io/chat/docs/flutter-dart/init_and_users/?language=dart) for more information. + +As you can see below, the User ID matches on both Firebase's and Stream's user database. + +##### Firebase Authentication Database + +![Firebase Auth Database with new user created](../assets/firebase_authentication_dashboard.jpg) + +##### Stream Chat User Database + +![Stream chat user database new account created](../assets/stream_chat_user_database.jpg) + + +### Get the Stream User Token + +The `getStreamUserToken` cloud function is very similar to the `createStreamUserAndGetToken` function. The only difference is +that it only creates a user token and does not create a new user account on Stream. + +Update the **signIn** method in your Flutter code to the following: + +```dart +Future signIn() async { + // Sign in with Firebase + await auth.signInWithEmailAndPassword(email: email, password: password); + print('Firebase signed in'); + + // Get Stream user token + final callable = functions.httpsCallable('getStreamUserToken'); + final results = await callable(); + print('Stream user token retrieved: ${results.data}'); +} +``` + +Calling this method will do the following: +1. Sign in using Firebase Auth. +2. Call the `getStreamUserToken` cloud function to get a Stream user token. + +:::note +The user needs to be authenticated to call this cloud function. Otherwise, the function will throw +the **failed-precondition** error that you specified. +::: + +### Revoke Stream User Token + +You may also want to revoke the Stream user token if you sign out from Firebase. + +Update the `signOut` method in your Flutter code to the following: + +```dart +Future signOut() async { + // Revoke Stream user token. + final callable = functions.httpsCallable('revokeStreamUserToken'); + await callable(); + print('Stream user token revoked'); + + // Sign out Firebase. + await auth.signOut(); + print('Firebase signed out'); +} +``` +:::note +Call the cloud function before signing out from Firebase. +::: + +### Delete Stream User + +When deleting a Firebase user account, it would make sense also to delete the +associated Stream user account. + +The cloud function looks like this: + +```js +// When a user is deleted from Firebase their associated Stream account is also deleted. +exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => { + return serverClient.deleteUser(user.uid); +}); +``` + +In this function, you are listening to delete events on Firebase auth. When an account is deleted, this function will be triggered, and you can get the +user's **uid** and call the `deleteUser` method on the `serverClient`. + +This is not an external cloud function; it can only be triggered when an +account is deleted. + +### Conclussion + +In this guide, you have seen how to securely create Stream Chat tokens using +Firebase Authentication and Cloud Functions. + +The principles shown in this guide can be applied to your preferred authentication +provider and cloud architecture of choice. \ No newline at end of file From d020f9ed8e3e8a1c4b7d37f19965d488ba6dfb6d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 26 Aug 2021 10:24:55 +0200 Subject: [PATCH 144/165] fix(localization): fix typos in italian strings --- .../lib/src/stream_chat_localizations_it.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 64e96dc0..e60d4afe 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -255,7 +255,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get yesterdayLabel => 'Ieri'; @override - String get channelIsMutedText => 'Il canale è mutato'; + String get channelIsMutedText => 'Il canale è silenziato'; @override String get noTitleText => 'Nessun titolo'; @@ -274,7 +274,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get loadingChannelsError => 'Errore durante il caricamento dei canali'; @override - String get deleteConversationLabel => 'Elemina conversazione'; + String get deleteConversationLabel => 'Elimina conversazione'; @override String get deleteConversationQuestion => From 790e8e19af8996a37bcc35941b857d0b47b218f6 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 26 Aug 2021 10:26:12 +0200 Subject: [PATCH 145/165] chore(localization): update changelog --- packages/stream_chat_localizations/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index f5757a28..d8328475 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +* Fixed typos in `Italian` translations. + ## 1.1.0 ✅ Added From db75558ca8afeb3f7c4241648eecb50335a7a50d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 26 Aug 2021 10:29:41 +0200 Subject: [PATCH 146/165] fix(repo): pr_title config --- .github/workflows/pr_title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml index 4e7c3b5f..b7d1ad9b 100644 --- a/.github/workflows/pr_title.yml +++ b/.github/workflows/pr_title.yml @@ -37,7 +37,7 @@ jobs: "llc": "packages/stream_chat", "ui": "packages/stream_chat_flutter", "core": "packages/stream_chat_flutter_core", - "localization": "packages/stream_chat_flutter_localizations", + "localization": "packages/stream_chat_localizations", "persistence": "packages/stream_chat_persistence" } env: From 40922963abcaac30904fa1fdc7235fce9308d566 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 26 Aug 2021 10:59:35 +0200 Subject: [PATCH 147/165] Update docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx --- .../Flutter/guides/user_token_generation_with_firebase_auth.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx b/docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx index a514da01..56192f53 100644 --- a/docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx +++ b/docusaurus/docs/Flutter/guides/user_token_generation_with_firebase_auth.mdx @@ -439,7 +439,7 @@ user's **uid** and call the `deleteUser` method on the `serverClient`. This is not an external cloud function; it can only be triggered when an account is deleted. -### Conclussion +### Conclusion In this guide, you have seen how to securely create Stream Chat tokens using Firebase Authentication and Cloud Functions. From 2a69d46c64260af708d208618718aa815d248f6a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 26 Aug 2021 11:33:24 +0200 Subject: [PATCH 148/165] feat(repo): add melos docs command --- melos.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/melos.yaml b/melos.yaml index b645deb0..6fee8f6f 100644 --- a/melos.yaml +++ b/melos.yaml @@ -69,6 +69,12 @@ scripts: select-package: dir-exists: coverage + docs: + run: | + npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli && + npx stream-chat-docusaurus -i -s + description: Runs the docusaurus documentation locally. + environment: sdk: '>=2.12.0 <3.0.0' flutter: '>=1.22.4 <2.0.0' \ No newline at end of file From 8afbba8e78aac96f0ac79f089bd1405cff23d656 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 27 Aug 2021 13:38:44 +0530 Subject: [PATCH 149/165] added new guide --- .../guides/customize_message_widget.mdx | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/customize_message_widget.mdx b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx index 96e1e37c..171fbdb1 100644 --- a/docusaurus/docs/Flutter/guides/customize_message_widget.mdx +++ b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx @@ -22,6 +22,12 @@ There are several things you can change in the theme including text styles and c You can also set a different theme for the user's own messages and messages received by them. +:::note +Theming allows you to change minor factors like style while using the widget directly allows you much +more customization such as replacing a certain widget with another. Some things can only be customized +through the widget and not the theme. +::: + Here is an example: ```dart @@ -60,3 +66,74 @@ MessageThemeData( You can change the attributes of the avatar (if displayed) using the `avatarTheme` property. +```dart +MessageThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(8), + ), +) +``` + +#### Changing Reaction theme + +You also customize the reactions attached to every message using the theme. + +```dart +MessageThemeData( + reactionsBackgroundColor: Colors.red, + reactionsBorderColor: Colors.redAccent, + reactionsMaskColor: Colors.pink, +), +``` + +### Changing Message Actions + +When a message is long pressed, the `MessageActionsModal` is shown. + +The `MessageWidget` allows showing or hiding some options if you so choose. + +```dart +MessageWidget( + ... + this.showUsername = true, + this.showTimestamp = true, + this.showReactions = true, + this.showDeleteMessage = true, + this.showEditMessage = true, + this.showReplyMessage = true, + this.showThreadReplyMessage = true, + this.showResendMessage = true, + this.showCopyMessage = true, + this.showFlagButton = true, + this.showPinButton = true, + this.showPinHighlight = true, +), +``` + +### Building attachments + +The `customAttachmentBuilder` property allows you to build any kind of attachment (inbuilt or custom) +in your own way. While a separate guide is written for this, it is included here because of relevance. + +```dart +MessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + customAttachmentBuilders: { + 'location': (context, message, attachments) { + final attachmentWidget = Image.network( + _buildMapAttachment( + attachments[0].extraData['latitude'], + attachments[0].extraData['longitude'], + ), + ); + + return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0)); + } + }, + ); + }, +), +``` + + From 64ba3efb26e658ad01645e11f8aa2763d9b10427 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 30 Aug 2021 12:38:57 +0530 Subject: [PATCH 150/165] added new guide --- .../guides/customize_message_widget.mdx | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/customize_message_widget.mdx b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx index 171fbdb1..af7014fb 100644 --- a/docusaurus/docs/Flutter/guides/customize_message_widget.mdx +++ b/docusaurus/docs/Flutter/guides/customize_message_widget.mdx @@ -13,6 +13,21 @@ limited to fonts, colors, and shapes. This guide details how to customize the `MessageWidget` in the Stream Chat Flutter UI SDK. +### Building Custom Messages + +This guide goes into detail about the ability to customize the `MessageWidget`. However, if you want +to customize the default `MessageWidget` in the `MessageListView` provided, you can use the `.copyWith()` method +provided inside the `messageBuilder` parameter of the `MessageListView` like this: + +```dart +MessageListView( + messageBuilder: (context, details, messageList, defaultImpl) { + // Your implementation of the message here + // E.g: return Text(details.message.text ?? ''); + }, +), +``` + ### Theming You can customize the `MessageWidget` using the `StreamChatTheme` class, so that you can change the @@ -35,8 +50,8 @@ StreamChatThemeData( /// Sets theme for user's messages ownMessageTheme: MessageThemeData( - messageBackgroundColor: colorTheme.textHighEmphasis, - ), + messageBackgroundColor: colorTheme.textHighEmphasis, + ), /// Sets theme for received messages otherMessageTheme: MessageThemeData( @@ -48,6 +63,8 @@ StreamChatThemeData( ) ``` +![](../assets/message_theming.png) + #### Change message text style The `MessageWidget` has multiple `Text` widgets that you can manipulate the styles of. The three main @@ -62,6 +79,8 @@ MessageThemeData( ) ``` +![](../assets/message_styles.png) + #### Change avatar theme You can change the attributes of the avatar (if displayed) using the `avatarTheme` property. @@ -74,6 +93,8 @@ MessageThemeData( ) ``` +![](../assets/message_rounded_avatar.png) + #### Changing Reaction theme You also customize the reactions attached to every message using the theme. @@ -86,6 +107,8 @@ MessageThemeData( ), ``` +![](../assets/message_reaction_theming.png) + ### Changing Message Actions When a message is long pressed, the `MessageActionsModal` is shown. @@ -95,21 +118,23 @@ The `MessageWidget` allows showing or hiding some options if you so choose. ```dart MessageWidget( ... - this.showUsername = true, - this.showTimestamp = true, - this.showReactions = true, - this.showDeleteMessage = true, - this.showEditMessage = true, - this.showReplyMessage = true, - this.showThreadReplyMessage = true, - this.showResendMessage = true, - this.showCopyMessage = true, - this.showFlagButton = true, - this.showPinButton = true, - this.showPinHighlight = true, + showUsername = true, + showTimestamp = true, + showReactions = true, + showDeleteMessage = true, + showEditMessage = true, + showReplyMessage = true, + showThreadReplyMessage = true, + showResendMessage = true, + showCopyMessage = true, + showFlagButton = true, + showPinButton = true, + showPinHighlight = true, ), ``` +![](../assets/message_widget_actions.png) + ### Building attachments The `customAttachmentBuilder` property allows you to build any kind of attachment (inbuilt or custom) @@ -136,4 +161,22 @@ MessageListView( ), ``` +### Widget Builders +Some parameters allow you to construct your own widget in place of some elements in the `MessageWidget`. + +These are: +* `userAvatarBuilder` : Allows user to substitute their own widget in place of the user avatar. +* `editMessageInputBuilder` : Allows user to substitute their own widget in place of the input in edit mode. +* `textBuilder` : Allows user to substitute their own widget in place of the text. +* `bottomRowBuilder` : Allows user to substitute their own widget in the bottom of the message when not deleted. +* `deletedBottomRowBuilder` : Allows user to substitute their own widget in the bottom of the message when deleted. + +```dart +MessageWidget( + ... + textBuilder: (context, message) { + // Add your own text implementation here. + }, +), +``` From 0b77c4272300a07e948f6dbb0d380214dbd81fe6 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 30 Aug 2021 12:39:52 +0530 Subject: [PATCH 151/165] added new guide --- .../Flutter/assets/mesage_reaction_theming.png | Bin 0 -> 2660 bytes .../Flutter/assets/mesage_rounded_avatar.png | Bin 0 -> 3445 bytes .../docs/Flutter/assets/message_styles.png | Bin 0 -> 7728 bytes .../docs/Flutter/assets/message_theming.png | Bin 0 -> 15746 bytes .../Flutter/assets/message_widget_actions.png | Bin 0 -> 37574 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docusaurus/docs/Flutter/assets/mesage_reaction_theming.png create mode 100644 docusaurus/docs/Flutter/assets/mesage_rounded_avatar.png create mode 100644 docusaurus/docs/Flutter/assets/message_styles.png create mode 100644 docusaurus/docs/Flutter/assets/message_theming.png create mode 100644 docusaurus/docs/Flutter/assets/message_widget_actions.png diff --git a/docusaurus/docs/Flutter/assets/mesage_reaction_theming.png b/docusaurus/docs/Flutter/assets/mesage_reaction_theming.png new file mode 100644 index 0000000000000000000000000000000000000000..8cddd6ba222d529f6e28b011529d96d5bd90791f GIT binary patch literal 2660 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 literal 0 HcmV?d00001 diff --git a/docusaurus/docs/Flutter/assets/message_theming.png b/docusaurus/docs/Flutter/assets/message_theming.png new file mode 100644 index 0000000000000000000000000000000000000000..b5d28dde0a732a4bd82fe2c85bbe6e6d003f8f3f GIT binary patch literal 15746 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 Date: Mon, 30 Aug 2021 13:24:36 +0530 Subject: [PATCH 152/165] grammar is important --- ...ion_theming.png => message_reaction_theming.png} | Bin ...ounded_avatar.png => message_rounded_avatar.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename docusaurus/docs/Flutter/assets/{mesage_reaction_theming.png => message_reaction_theming.png} (100%) rename docusaurus/docs/Flutter/assets/{mesage_rounded_avatar.png => message_rounded_avatar.png} (100%) diff --git a/docusaurus/docs/Flutter/assets/mesage_reaction_theming.png b/docusaurus/docs/Flutter/assets/message_reaction_theming.png similarity index 100% rename from docusaurus/docs/Flutter/assets/mesage_reaction_theming.png rename to docusaurus/docs/Flutter/assets/message_reaction_theming.png diff --git a/docusaurus/docs/Flutter/assets/mesage_rounded_avatar.png b/docusaurus/docs/Flutter/assets/message_rounded_avatar.png similarity index 100% rename from docusaurus/docs/Flutter/assets/mesage_rounded_avatar.png rename to docusaurus/docs/Flutter/assets/message_rounded_avatar.png From bf1ca1ba497ab936c8fb1ef02726805b4c788fbb Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Aug 2021 11:20:41 +0200 Subject: [PATCH 153/165] docs(doc): add foreground push notification --- ...ions.mdx => adding_push_notifications.mdx} | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) rename docusaurus/docs/Flutter/guides/{adding_push_notifcations.mdx => adding_push_notifications.mdx} (87%) diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifcations.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx similarity index 87% rename from docusaurus/docs/Flutter/guides/adding_push_notifcations.mdx rename to docusaurus/docs/Flutter/guides/adding_push_notifications.mdx index 36da1373..6e5cbf1d 100644 --- a/docusaurus/docs/Flutter/guides/adding_push_notifcations.mdx +++ b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx @@ -187,8 +187,33 @@ StreamChat( As you can see we generate a local notification whenever a message.new or notification.message_new event is received. +### Foreground notifications + +Sometimes you want to show a notification when the app is in the foreground. +For example, when you're typing a message in a channel and you receive a new message from someone in another channel, you want to get notified about it. + +Even in this case you can use the `flutter_local_notifications` package to show a notification. + +You need to listen for new events using `StreamChatClient.on` and handle them accordingly. + +Here we're checking if the event is a `message.new` or `notification.message_new` event, and if the message is from a different user than the current user. In that case we'll show a notification. + +```dart +client.on( + EventType.messageNew, + EventType.notificationMessageNew, +).listen((event) { + if (event.message?.user?.id == client.state.currentUser?.id) { + return; + } + showLocalNotification(event, client.state.currentUser!.id, context); +}); +``` + :::note -Using `flutter_local_notifications` is a great way to implement notifications while the is in foreground too! You can generate a local notification listening to events using the method `streamChatClient.on()` and react to the events you want. +You should also check that the channel of the message is different than the channel in foreground. +How you can do this depends on your app infrastructure and how you handle navigation. +Take a look at the [Stream Chat v1 sample app](https://github.com/GetStream/flutter-samples/blob/main/packages/stream_chat_v1/lib/home_page.dart#L11) to see how we're doing it over there. ::: ### Saving notification messages to the offline storage From d7c96143a598960258d3d683a5ff5ce67e68603a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Aug 2021 13:25:34 +0200 Subject: [PATCH 154/165] fix(llc): channel.show body --- packages/stream_chat/lib/src/core/api/channel_api.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 68f0d1c3..619e0980 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -292,6 +292,7 @@ class ChannelApi { ) async { final response = await _client.post( '${_getChannelUrl(channelId, channelType)}/show', + data: {}, ); return EmptyResponse.fromJson(response.data); } From a23b29ddc3348e55b5cd47abdcf0fc7ec88cc4f7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Aug 2021 13:26:35 +0200 Subject: [PATCH 155/165] chore(llc): update changelog --- packages/stream_chat/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f136ac5d..6ddea6ed 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Fix `channel.show` not working because of null body + ## 2.2.0 🐞 Fixed From 1ebafb036fdbf2a744c62384f718759baecdc748 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 30 Aug 2021 15:21:59 +0200 Subject: [PATCH 156/165] fix(llc): update tests --- .../test/src/core/api/channel_api_test.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index 9110845d..e427ff2e 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -550,14 +550,21 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/show'; - when(() => client.post(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: {}, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await channelApi.showChannel(channelId, channelType); expect(res, isNotNull); - verify(() => client.post(path)).called(1); + verify(() => client.post( + path, + data: {}, + )).called(1); verifyNoMoreInteractions(client); }); From de8f0d9f938982231016e0f31fe7f092c0385334 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 31 Aug 2021 09:11:27 +0200 Subject: [PATCH 157/165] Apply suggestions from code review Co-authored-by: Gordon --- .../docs/Flutter/guides/adding_push_notifications.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx index 6e5cbf1d..c90f6814 100644 --- a/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx +++ b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx @@ -189,10 +189,10 @@ As you can see we generate a local notification whenever a message.new or notifi ### Foreground notifications -Sometimes you want to show a notification when the app is in the foreground. -For example, when you're typing a message in a channel and you receive a new message from someone in another channel, you want to get notified about it. +Sometimes you may want to show a notification when the app is in the foreground. +For example, when you're in a channel and you receive a new message from someone in another channel. -Even in this case you can use the `flutter_local_notifications` package to show a notification. +For this scenario, you can also use the `flutter_local_notifications` package to show a notification. You need to listen for new events using `StreamChatClient.on` and handle them accordingly. @@ -211,8 +211,8 @@ client.on( ``` :::note -You should also check that the channel of the message is different than the channel in foreground. -How you can do this depends on your app infrastructure and how you handle navigation. +You should also check that the channel of the message is different than the channel in the foreground. +How you do this depends on your app infrastructure and how you handle navigation. Take a look at the [Stream Chat v1 sample app](https://github.com/GetStream/flutter-samples/blob/main/packages/stream_chat_v1/lib/home_page.dart#L11) to see how we're doing it over there. ::: From 8217b7469a26b3a16ed2d454750d0b21379c1f1b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 31 Aug 2021 10:47:45 +0200 Subject: [PATCH 158/165] fix(llc): unread count not updating --- packages/stream_chat/lib/src/client/channel.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 999dfb0b..aec63f50 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1934,6 +1934,8 @@ class ChannelClientState { read: newReads, pinnedMessages: updatedState.pinnedMessages, ); + + _computeInitialUnread(); } int _sortByCreatedAt(Message a, Message b) => From cfd66a451ee57d4394eee51eb04c4933220e0d12 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 31 Aug 2021 10:48:44 +0200 Subject: [PATCH 159/165] chore(llc): update changelog --- packages/stream_chat/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f136ac5d..322ae413 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Fixed unread indicator not updating correctly + ## 2.2.0 🐞 Fixed From ae18eff8e8b74066fb756e317bc7abdd34c9056c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 31 Aug 2021 13:38:51 +0200 Subject: [PATCH 160/165] refactor(llc): rename _computeInitialUnread to _computeUnread --- packages/stream_chat/lib/src/client/channel.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index aec63f50..f1b1a9e7 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1488,7 +1488,7 @@ class ChannelClientState { _listenMemberRemoved(); - _computeInitialUnread(); + _computeUnread(); _startCleaning(); @@ -1512,7 +1512,7 @@ class ChannelClientState { final _subscriptions = []; - void _computeInitialUnread() { + void _computeUnread() { final userRead = channelState.read.firstWhereOrNull( (r) => r.user.id == _channel._client.state.currentUser?.id, ); @@ -1935,7 +1935,7 @@ class ChannelClientState { pinnedMessages: updatedState.pinnedMessages, ); - _computeInitialUnread(); + _computeUnread(); } int _sortByCreatedAt(Message a, Message b) => From 142d062c538de11e50be0dc767258f5c243857c2 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 1 Sep 2021 10:50:39 +0200 Subject: [PATCH 161/165] fix(llc): use unread count when > 0 --- packages/stream_chat/lib/src/client/channel.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index f1b1a9e7..c2c40018 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1516,7 +1516,7 @@ class ChannelClientState { final userRead = channelState.read.firstWhereOrNull( (r) => r.user.id == _channel._client.state.currentUser?.id, ); - if (userRead != null) { + if (userRead != null && userRead.unreadMessages > 0) { unreadCount = userRead.unreadMessages; } } From ed2a3f17d8abc5b3b5e0bc6fe6b2df4afb33a764 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 1 Sep 2021 15:15:21 +0530 Subject: [PATCH 162/165] removed clipping from user avatar --- packages/stream_chat_flutter/CHANGELOG.md | 1 + .../lib/src/user_avatar.dart | 46 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 91ad7f1d..b7a37acb 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -46,6 +46,7 @@ breakdown: * `UserListViewTheme` is now `UserListViewThemeData` - Updated core dependency. +- `UserAvatar` no longer uses a clipper by default. 🐞 Fixed diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 6a5ef0de..47c50b95 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -68,26 +68,32 @@ class UserAvatar extends StatelessWidget { Widget avatar = FittedBox( fit: BoxFit.cover, - child: ClipRRect( - borderRadius: borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, - child: Container( - constraints: constraints ?? - streamChatTheme.ownMessageTheme.avatarTheme?.constraints, - child: hasImage - ? CachedNetworkImage( - fit: BoxFit.cover, - filterQuality: FilterQuality.high, - imageUrl: user.image!, - errorWidget: (context, __, ___) => - streamChatTheme.defaultUserImage(context, user), - placeholder: placeholder != null - ? (context, __) => placeholder(context, user) - : null, - ) - : streamChatTheme.defaultUserImage(context, user), - ), - ), + child: hasImage + ? CachedNetworkImage( + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + imageUrl: user.image!, + imageBuilder: (context, imageProvider) => Container( + constraints: constraints ?? + streamChatTheme.ownMessageTheme.avatarTheme?.constraints, + decoration: BoxDecoration( + borderRadius: borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, + image: + DecorationImage(image: imageProvider, fit: BoxFit.cover), + ), + ), + errorWidget: (context, __, ___) => + streamChatTheme.defaultUserImage(context, user), + placeholder: placeholder != null + ? (context, __) => placeholder(context, user) + : null, + ) + : ClipRRect( + borderRadius: borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, + child: streamChatTheme.defaultUserImage(context, user), + ), ); if (selected) { From 0f8ee304a28ae61ee0d17d6fa11a1c06d13591ce Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 1 Sep 2021 11:52:12 +0200 Subject: [PATCH 163/165] chore(llc,core,ui): update changelog and pubspecs --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index a1870b3d..67a80e17 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 2.2.1 🐞 Fixed diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index a4490d5d..b208e15c 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '2.2.0'; +const PACKAGE_VERSION = '2.2.1'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index ede419ab..a07606ba 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 2.2.0 +version: 2.2.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 91ad7f1d..02e2b214 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.1 + +- Updated `stream_chat_flutter_core` dependency to 2.2.1 + ## 2.2.0 ✅ Added diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index dc3bde46..907e9a7f 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 2.2.0 +version: 2.2.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -37,7 +37,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.2.0 + stream_chat_flutter_core: ^2.2.1 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index a568d88e..1378ced0 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.1 + +- Updated `stream_chat` dependency to 2.2.1 + ## 2.2.0 🛑️ Breaking Changes from `2.1.1` diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index bd0eea6f..85b57124 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 2.2.0 +version: 2.2.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.2.0 + stream_chat: ^2.2.1 dev_dependencies: fake_async: ^1.2.0 From 14be1803d334c73cc161339e250bb2c7cbd7662e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 2 Sep 2021 16:55:52 +0530 Subject: [PATCH 164/165] removed clipping from user avatar --- .../lib/src/user_avatar.dart | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 47c50b95..f9a1fd02 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -68,32 +68,35 @@ class UserAvatar extends StatelessWidget { Widget avatar = FittedBox( fit: BoxFit.cover, - child: hasImage - ? CachedNetworkImage( - fit: BoxFit.cover, - filterQuality: FilterQuality.high, - imageUrl: user.image!, - imageBuilder: (context, imageProvider) => Container( - constraints: constraints ?? - streamChatTheme.ownMessageTheme.avatarTheme?.constraints, - decoration: BoxDecoration( - borderRadius: borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, - image: - DecorationImage(image: imageProvider, fit: BoxFit.cover), + child: Container( + constraints: constraints ?? + streamChatTheme.ownMessageTheme.avatarTheme?.constraints, + child: hasImage + ? CachedNetworkImage( + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + imageUrl: user.image!, + errorWidget: (context, __, ___) => + streamChatTheme.defaultUserImage(context, user), + placeholder: placeholder != null + ? (context, __) => placeholder(context, user) + : null, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: borderRadius ?? + streamChatTheme + .ownMessageTheme.avatarTheme?.borderRadius, + image: DecorationImage( + image: imageProvider, fit: BoxFit.cover), + ), ), + ) + : ClipRRect( + borderRadius: borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, + child: streamChatTheme.defaultUserImage(context, user), ), - errorWidget: (context, __, ___) => - streamChatTheme.defaultUserImage(context, user), - placeholder: placeholder != null - ? (context, __) => placeholder(context, user) - : null, - ) - : ClipRRect( - borderRadius: borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, - child: streamChatTheme.defaultUserImage(context, user), - ), + ), ); if (selected) { From 8746976719543ed8cef05f16cccff6957278fe4f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 2 Sep 2021 17:42:57 +0530 Subject: [PATCH 165/165] removed changelog --- packages/stream_chat_flutter/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b7a37acb..91ad7f1d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -46,7 +46,6 @@ breakdown: * `UserListViewTheme` is now `UserListViewThemeData` - Updated core dependency. -- `UserAvatar` no longer uses a clipper by default. 🐞 Fixed