diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index f1bab37cfe..bc73dd6c09 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -8,6 +8,7 @@ - Added a `size` (`StreamLoadingSpinnerSize`) parameter to `StreamScrollViewLoadingWidget`. - Added `onReactionTap` to `StreamMessageItem` and `StreamMessageListView`, reporting the tapped message's `BuildContext` and a `ReactionTapDetails` with the tapped `message` and `reaction` (the reaction is `null` for a clustered or overflow chip that maps to no single reaction). - Added an `unreadIndicator` parameter to `StreamBackButton` that overlays a widget (typically a `StreamUnreadIndicator`) on the button's top-end corner. Pass `StreamUnreadIndicator(excludeCid: cid)` to show the total unread count of other channels, or `StreamUnreadIndicator.channels(cid: cid)` for a single channel's count. +- Added snackbar feedback for user actions: message copy, pin/unpin, delete, flag, mark unread, and mute/unmute user; ending a poll; audio-recording permission denial; and message send/edit failures (shown only when no `onError` handler is provided). ⚠️ Deprecated diff --git a/packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart index 30e22ccee5..9ad254856b 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/poll/stream_poll_comments_sheet.dart'; import 'package:stream_chat_flutter/src/poll/stream_poll_options_sheet.dart'; import 'package:stream_chat_flutter/src/poll/stream_poll_results_sheet.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_core_flutter/chat.dart'; @@ -136,10 +137,24 @@ class _DefaultStreamPollAttachmentState extends State onEndVote() async { + final translations = context.translations; + final messenger = StreamSnackbarMessenger.maybeOf(context); + final confirm = await showPollEndVoteDialog(context: context); if (confirm == null || !confirm) return; - channel.closePoll(poll).ignore(); + try { + await channel.closePoll(poll); + messenger?.show( + StreamSnackbar(message: Text(translations.endVoteSuccessMessage), variant: .success), + replace: true, + ); + } on Exception catch (_) { + messenger?.show( + StreamSnackbar(message: Text(translations.endVoteErrorMessage), variant: .error), + replace: true, + ); + } } Future onAddComment() async { diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index d495e6564f..148bf308e3 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -286,6 +286,43 @@ abstract class Translations { /// The text of an error shown when marking a message as unread fails String get markUnreadError; + /// The text shown when a message is successfully marked as unread + String get messageMarkedAsUnreadText; + + /// The text of an error shown when deleting a message fails + String get deleteMessageError; + + /// The text shown when a message is successfully pinned or unpinned, where + /// [pinned] is the message's resulting pinned state (`true` after a pin). + String togglePinUnpinMessageSuccessText({required bool pinned}); + + /// The text of an error shown when pinning or unpinning a message fails, where + /// [pinned] is the pinned state that was being applied (`true` for a pin). + String togglePinUnpinMessageErrorText({required bool pinned}); + + /// The text of an error shown when flagging a message fails + String get flagMessageError; + + /// The text shown when a message is copied to the clipboard + String get messageCopiedToClipboardText; + + /// The text of an error shown when sending a message fails + String get sendMessageError; + + /// The text of an error shown when editing a message fails + String get editMessageError; + + /// The text shown when a [user] is successfully muted or unmuted + /// based on [isMuted] + String toggleMuteUnmuteUserSuccessText({ + required String user, + required bool isMuted, + }); + + /// The text of an error shown when muting or unmuting a user fails + /// based on [isMuted] + String toggleMuteUnmuteUserErrorText({required bool isMuted}); + /// The text for showing delete/retry-delete based on [isDeleteFailed] String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}); @@ -594,6 +631,12 @@ abstract class Translations { /// The label for "End Poll". String get endVoteLabel; + /// The text shown when a poll is successfully ended + String get endVoteSuccessMessage; + + /// The text of an error shown when ending a poll fails + String get endVoteErrorMessage; + /// The label for "Poll Results". String get pollResultsLabel; @@ -638,6 +681,9 @@ abstract class Translations { /// The label for "Hold to record" String get holdToRecordLabel; + /// The message shown when audio recording permission is denied + String get audioRecordingPermissionMessage; + /// The label for "Send Anyway" String get sendAnywayLabel; @@ -1303,6 +1349,51 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments 'Error marking message unread. Cannot mark unread messages older than the' ' newest 100 channel messages.'; + @override + String get messageMarkedAsUnreadText => 'Message marked as unread'; + + @override + String get deleteMessageError => 'Error deleting message'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Message pinned'; + return 'Message unpinned'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Error pinning message'; + return 'Error removing message pin'; + } + + @override + String get flagMessageError => 'Error adding flag'; + + @override + String get messageCopiedToClipboardText => 'Message copied to clipboard'; + + @override + String get sendMessageError => 'Send message request failed'; + + @override + String get editMessageError => 'Edit message request failed'; + + @override + String toggleMuteUnmuteUserSuccessText({ + required String user, + required bool isMuted, + }) { + if (isMuted) return '$user has been unmuted'; + return '$user has been muted'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) return 'Error unmuting a user, please try again'; + return 'Error muting a user, please try again'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Create a new poll'; @@ -1457,6 +1548,12 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments @override String get endVoteLabel => 'End Poll'; + @override + String get endVoteSuccessMessage => 'Poll ended'; + + @override + String get endVoteErrorMessage => 'Failed to end the poll'; + @override String get pollResultsLabel => 'Poll Results'; @@ -1510,6 +1607,9 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments @override String get holdToRecordLabel => 'Hold to record. Release to save.'; + @override + String get audioRecordingPermissionMessage => 'Please allow Audio permissions in settings.'; + @override String get sendAnywayLabel => 'Send Anyway'; diff --git a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart index 8fc3d1c88d..13dd575beb 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart @@ -58,15 +58,25 @@ class StreamAudioRecorderController extends ValueNotifier final AudioRecorder _recorder; /// Starts a new recording session. - Future startRecord() async { + /// + /// When audio permission is denied, [permissionDeniedMessage] (if provided) is + /// surfaced through [RecordStateIdle.message] so the message input can prompt + /// the user to grant access. + Future startRecord({String? permissionDeniedMessage}) async { // Only start the recorder if it is currently idle. if (value case RecordStateIdle()) { // Return if the recorder does not have permission to record audio. final hasPermission = await _recorder.hasPermission(request: false); if (!hasPermission) { - /// Request permission to record audio. - /// User has to start the recording session again to record audio. - await _recorder.hasPermission(request: true); + // Request permission to record audio. The user has to start the + // recording session again to record audio. + final granted = await _recorder.hasPermission(request: true); + if (!granted && permissionDeniedMessage != null) { + // Cancel any pending info timer so it can't clear the denial message. + _infoTimer?.cancel(); + _infoTimer = null; + value = RecordStateIdle(message: permissionDeniedMessage); + } return; } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_chat_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_chat_message_input.dart index 60ab8ccdb5..22dd25195f 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_chat_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_chat_message_input.dart @@ -298,8 +298,12 @@ class _StreamChatMessageInputContent extends StatelessWidget { // Return if the recording is already started. if (audioRecorderController.isRecording) return; + // Capture the message before the async gap to avoid using a + // potentially unmounted BuildContext after awaiting. + final permissionDeniedMessage = context.translations.audioRecordingPermissionMessage; + await widget.feedback.onRecordStart(context); - return audioRecorderController.startRecord(); + return audioRecorderController.startRecord(permissionDeniedMessage: permissionDeniedMessage); }, onLongPressEnd: (_) async { // Return if the recording not yet started or already locked. diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 2b58cd8ba2..b83baa7e90 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -1471,13 +1471,15 @@ class DefaultStreamMessageComposerState extends State channel.updateMessage(message), false => channel.sendMessage(message), }; @@ -1488,6 +1490,16 @@ class DefaultStreamMessageComposerState extends State _onActionTap( BuildContext context, Channel channel, MessageAction action, - ) async => switch (action) { - SelectReaction() => _selectReaction(context, action.message, channel, action.reaction), - CopyMessage() => _copyMessage(action.message, channel), - DeleteMessage() => _maybeDeleteMessage(context, action.message, channel), - HardDeleteMessage() => channel.deleteMessage(action.message, hard: true), - EditMessage() => props.onEditMessageTap?.call(action.message), - FlagMessage() => _maybeFlagMessage(context, action.message, channel), - MarkUnread() => channel.markUnread(action.message.id), - MuteUser() => channel.client.muteUser(action.user.id), - UnmuteUser() => channel.client.unmuteUser(action.user.id), - BlockUser() => channel.client.blockUser(action.user.id), - UnblockUser() => channel.client.unblockUser(action.user.id), - PinMessage() => channel.pinMessage(action.message), - UnpinMessage() => channel.unpinMessage(action.message), - ResendMessage() => channel.retryMessage(action.message), - QuotedReply() => props.onReplyTap?.call(action.message), - ThreadReply() => props.onThreadTap?.call(action.message, null), - }; + ) async { + final translations = context.translations; + // Resolved up-front — before the confirmation dialogs and before the action + // can remove the message item (e.g. a delete) — so the snackbar still shows. + final messenger = StreamSnackbarMessenger.maybeOf(context); + + return switch (action) { + SelectReaction() => _selectReaction(context, action.message, channel, action.reaction).ignore(), + CopyMessage() => _copyMessage(action.message, messenger, translations.messageCopiedToClipboardText), + DeleteMessage() => _maybeDeleteMessage(context, action.message, channel, messenger), + HardDeleteMessage() => _runWithFeedback( + messenger, + () => channel.deleteMessage(action.message, hard: true), + successMessage: translations.messageDeletedLabel, + errorMessage: translations.deleteMessageError, + ), + EditMessage() => props.onEditMessageTap?.call(action.message), + FlagMessage() => _maybeFlagMessage(context, action.message, channel, messenger), + MarkUnread() => _runWithFeedback( + messenger, + () => channel.markUnread(action.message.id), + successMessage: translations.messageMarkedAsUnreadText, + errorMessage: translations.markUnreadError, + ), + MuteUser() => _runWithFeedback( + messenger, + () => channel.client.muteUser(action.user.id), + successMessage: translations.toggleMuteUnmuteUserSuccessText(user: action.user.name, isMuted: false), + errorMessage: translations.toggleMuteUnmuteUserErrorText(isMuted: false), + ), + UnmuteUser() => _runWithFeedback( + messenger, + () => channel.client.unmuteUser(action.user.id), + successMessage: translations.toggleMuteUnmuteUserSuccessText(user: action.user.name, isMuted: true), + errorMessage: translations.toggleMuteUnmuteUserErrorText(isMuted: true), + ), + BlockUser() => channel.client.blockUser(action.user.id).ignore(), + UnblockUser() => channel.client.unblockUser(action.user.id).ignore(), + PinMessage() => _runWithFeedback( + messenger, + () => channel.pinMessage(action.message), + successMessage: translations.togglePinUnpinMessageSuccessText(pinned: true), + errorMessage: translations.togglePinUnpinMessageErrorText(pinned: true), + ), + UnpinMessage() => _runWithFeedback( + messenger, + () => channel.unpinMessage(action.message), + successMessage: translations.togglePinUnpinMessageSuccessText(pinned: false), + errorMessage: translations.togglePinUnpinMessageErrorText(pinned: false), + ), + ResendMessage() => channel.retryMessage(action.message).ignore(), + QuotedReply() => props.onReplyTap?.call(action.message), + ThreadReply() => props.onThreadTap?.call(action.message, null), + }; + } - // Copies the message text (with mentions replaced) to the clipboard. + // Runs [action] and shows a success or error snackbar on [messenger] with the + // given messages. When [messenger] is null the action still runs; only the + // snackbar feedback is skipped. + Future _runWithFeedback( + StreamSnackbarMessenger? messenger, + Future Function() action, { + required String errorMessage, + String? successMessage, + }) async { + try { + await action(); + if (successMessage == null) return; + messenger?.show( + StreamSnackbar(message: Text(successMessage), variant: .success), + replace: true, + ); + } on Exception catch (_) { + messenger?.show( + StreamSnackbar(message: Text(errorMessage), variant: .error), + replace: true, + ); + } + } + + // Copies the message text (with mentions replaced) to the clipboard and + // confirms with a snackbar on [messenger]. Future _copyMessage( Message message, - Channel channel, + StreamSnackbarMessenger? messenger, + String confirmationMessage, ) async { final presentableMessage = message.replaceMentions(linkify: false); final messageText = presentableMessage.text; if (messageText == null || messageText.isEmpty) return; - return Clipboard.setData(ClipboardData(text: messageText)); + await Clipboard.setData(ClipboardData(text: messageText)); + messenger?.show( + StreamSnackbar(message: Text(confirmationMessage)), + replace: true, + ); } - // Shows a confirmation dialog before deleting the message. - Future _maybeDeleteMessage( + // Shows a confirmation dialog before deleting the message, then a + // success/error snackbar on [messenger] once confirmed. + Future _maybeDeleteMessage( BuildContext context, Message message, Channel channel, + StreamSnackbarMessenger? messenger, ) async { + final translations = context.translations; final confirmDelete = await showStreamDialog( context: context, builder: (context) => StreamMessageActionConfirmationModal( @@ -930,17 +1005,25 @@ class DefaultStreamMessageItem extends StatelessWidget { ), ); - if (confirmDelete != true) return null; + if (confirmDelete != true) return; - return channel.deleteMessage(message); + return _runWithFeedback( + messenger, + () => channel.deleteMessage(message), + successMessage: translations.messageDeletedLabel, + errorMessage: translations.deleteMessageError, + ); } - // Shows a confirmation dialog before flagging the message. - Future _maybeFlagMessage( + // Shows a confirmation dialog before flagging the message, then a + // success/error snackbar on [messenger] once confirmed. + Future _maybeFlagMessage( BuildContext context, Message message, Channel channel, + StreamSnackbarMessenger? messenger, ) async { + final translations = context.translations; final confirmFlag = await showStreamDialog( context: context, builder: (context) => StreamMessageActionConfirmationModal( @@ -952,10 +1035,14 @@ class DefaultStreamMessageItem extends StatelessWidget { ), ); - if (confirmFlag != true) return null; + if (confirmFlag != true) return; - final messageId = message.id; - return channel.client.flagMessage(messageId); + return _runWithFeedback( + messenger, + () => channel.client.flagMessage(message.id), + successMessage: translations.flagMessageSuccessfulLabel, + errorMessage: translations.flagMessageError, + ); } // Toggles a reaction: removes it if already present, otherwise sends it. diff --git a/packages/stream_chat_flutter/test/src/message_widget/stream_message_item_test.dart b/packages/stream_chat_flutter/test/src/message_widget/stream_message_item_test.dart new file mode 100644 index 0000000000..d5d276647a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_widget/stream_message_item_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + // Drives the real flow: long-press a message to open the actions modal, tap + // "Pin", and assert the resulting success/error snackbar. StreamChat sits + // above the Navigator (as in a real app) so the snackbar host is reachable + // once the modal route pops. + group('StreamMessageItem action snackbars', () { + final currentUser = OwnUser(id: 'current-user'); + final message = Message( + id: 'test-message', + text: 'Pin me', + createdAt: DateTime(2026), + user: User(id: 'other-user'), + state: MessageState.sent, + ); + + late MockClient client; + late MockClientState clientState; + late MockChannel channel; + late MockChannelState channelState; + + setUp(() { + client = MockClient(); + clientState = MockClientState(); + channel = MockChannel( + ownCapabilities: const [ + ChannelCapability.sendMessage, + ChannelCapability.pinMessage, + ChannelCapability.deleteAnyMessage, + ], + ); + channelState = MockChannelState(); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(currentUser); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(currentUser)); + when(() => channel.client).thenReturn(client); + when(() => channel.state).thenReturn(channelState); + }); + + Widget buildHarness() => MaterialApp( + builder: (context, child) => StreamChat(client: client, child: child), + home: StreamChannel( + channel: channel, + child: Scaffold(body: StreamMessageItem(message: message)), + ), + ); + + Future openActionsAndTap(WidgetTester tester, String actionLabel) async { + final gesture = await tester.startGesture( + tester.getCenter(find.byType(StreamMessageItem).first), + ); + await tester.pump(const Duration(milliseconds: 700)); + await gesture.up(); + await tester.pumpAndSettle(); + + await tester.tap(find.text(actionLabel)); + await tester.pumpAndSettle(); + } + + testWidgets('shows a success snackbar when pinning succeeds', (tester) async { + when(() => channel.pinMessage(message)).thenAnswer((_) async => UpdateMessageResponse()); + + await tester.pumpWidget(buildHarness()); + await tester.pumpAndSettle(); + + await openActionsAndTap(tester, 'Pin to Conversation'); + + verify(() => channel.pinMessage(message)).called(1); + expect(find.text('Message pinned'), findsOneWidget); + }); + + testWidgets('shows an error snackbar when pinning fails', (tester) async { + when(() => channel.pinMessage(message)).thenThrow(Exception('failed to pin')); + + await tester.pumpWidget(buildHarness()); + await tester.pumpAndSettle(); + + await openActionsAndTap(tester, 'Pin to Conversation'); + + verify(() => channel.pinMessage(message)).called(1); + expect(find.text('Error pinning message'), findsOneWidget); + }); + + testWidgets('shows a success snackbar when a confirmed delete succeeds', (tester) async { + when(() => channel.deleteMessage(message)).thenAnswer((_) async => EmptyResponse()); + + await tester.pumpWidget(buildHarness()); + await tester.pumpAndSettle(); + + // Opens the actions modal and taps "Delete Message", surfacing the + // confirmation dialog; then confirm via "Delete". + await openActionsAndTap(tester, 'Delete Message'); + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + verify(() => channel.deleteMessage(message)).called(1); + expect(find.text('Message deleted'), findsOneWidget); + }); + + testWidgets('shows an error snackbar when a confirmed delete fails', (tester) async { + when(() => channel.deleteMessage(message)).thenThrow(Exception('failed to delete')); + + await tester.pumpWidget(buildHarness()); + await tester.pumpAndSettle(); + + await openActionsAndTap(tester, 'Delete Message'); + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + expect(find.text('Error deleting message'), findsOneWidget); + }); + + testWidgets('confirms with a snackbar when copying a message', (tester) async { + // Stub the clipboard platform channel so Clipboard.setData resolves. + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (call) async => null, + ); + addTearDown(() { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + null, + ); + }); + + await tester.pumpWidget(buildHarness()); + await tester.pumpAndSettle(); + + await openActionsAndTap(tester, 'Copy Message'); + + expect(find.text('Message copied to clipboard'), findsOneWidget); + }); + }); +} diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index d583d86b58..11ebbee47c 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -3,6 +3,7 @@ ✅ Added - Added connection-error translations (`connectionErrorTitle`/`Description`, `slowConnectionErrorTitle`/`Description`, `genericErrorTitle`/`Description`) for all supported locales. +- Added action-feedback snackbar translations (message copy, pin/unpin, delete, flag, mark unread, mute/unmute user, end poll, audio-recording permission, and message send/edit failures) for all supported locales. ## 10.2.0 diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart index 063cd39e74..3bab8a791e 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart @@ -515,6 +515,48 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { ' missatges no llegits més antics que els 100 missatges més recents del' ' canal.'; + @override + String get messageMarkedAsUnreadText => 'Missatge marcat com a no llegit'; + + @override + String get deleteMessageError => 'Error en eliminar el missatge'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Missatge fixat'; + return 'Missatge desfixat'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Error en fixar el missatge'; + return 'Error en desfixar el missatge'; + } + + @override + String get flagMessageError => 'Error en reportar el missatge'; + + @override + String get messageCopiedToClipboardText => 'Missatge copiat al porta-retalls'; + + @override + String get sendMessageError => "No s'ha pogut enviar el missatge"; + + @override + String get editMessageError => "No s'ha pogut editar el missatge"; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return "S'ha activat el so de $user"; + return "S'ha silenciat $user"; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) return "Error en activar el so de l'usuari, torna-ho a provar"; + return "Error en silenciar l'usuari, torna-ho a provar"; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Crear una enquesta nova'; @@ -651,6 +693,12 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get endVoteLabel => 'Finalitzar votació'; + @override + String get endVoteSuccessMessage => 'Votació finalitzada'; + + @override + String get endVoteErrorMessage => "No s'ha pogut finalitzar la votació"; + @override String get pollResultsLabel => 'Resultats de la votació'; @@ -704,6 +752,9 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get holdToRecordLabel => 'Mantén premut per gravar, deixa anar per enviar'; + @override + String get audioRecordingPermissionMessage => "Permet els permisos d'àudio a la configuració."; + @override String get sendAnywayLabel => 'Enviar igualment'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart index d0be1bc668..36afb47ac2 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart @@ -513,6 +513,50 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { ' ungelesenen Nachrichten markieren als die neuesten 100' ' Kanalnachrichten.'; + @override + String get messageMarkedAsUnreadText => 'Nachricht als ungelesen markiert'; + + @override + String get deleteMessageError => 'Fehler beim Löschen der Nachricht'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Nachricht angeheftet'; + return 'Anheftung der Nachricht aufgehoben'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Fehler beim Anheften der Nachricht'; + return 'Fehler beim Aufheben der Anheftung'; + } + + @override + String get flagMessageError => 'Fehler beim Hinzufügen der Meldung'; + + @override + String get messageCopiedToClipboardText => 'Nachricht in die Zwischenablage kopiert'; + + @override + String get sendMessageError => 'Senden der Nachricht fehlgeschlagen'; + + @override + String get editMessageError => 'Bearbeiten der Nachricht fehlgeschlagen'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return 'Stummschaltung von $user wurde aufgehoben'; + return '$user wurde stummgeschaltet'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return 'Fehler beim Aufheben der Stummschaltung, bitte versuche es erneut'; + } + return 'Fehler beim Stummschalten, bitte versuche es erneut'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Erstellen einer neuen Umfrage'; @@ -649,6 +693,12 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get endVoteLabel => 'Abstimmung beenden'; + @override + String get endVoteSuccessMessage => 'Abstimmung beendet'; + + @override + String get endVoteErrorMessage => 'Abstimmung konnte nicht beendet werden'; + @override String get pollResultsLabel => 'Umfrage-Ergebnisse'; @@ -702,6 +752,9 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get holdToRecordLabel => 'Zum Aufnehmen halten, zum Senden loslassen'; + @override + String get audioRecordingPermissionMessage => 'Bitte erlaube die Audioberechtigung in den Einstellungen.'; + @override String get sendAnywayLabel => 'Trotzdem senden'; 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 09caf5330a..59a7589798 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 @@ -513,6 +513,48 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { 'Error marking message unread. Cannot mark unread messages older' ' than the newest 100 channel messages.'; + @override + String get messageMarkedAsUnreadText => 'Message marked as unread'; + + @override + String get deleteMessageError => 'Error deleting message'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Message pinned'; + return 'Message unpinned'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Error pinning message'; + return 'Error removing message pin'; + } + + @override + String get flagMessageError => 'Error adding flag'; + + @override + String get messageCopiedToClipboardText => 'Message copied to clipboard'; + + @override + String get sendMessageError => 'Send message request failed'; + + @override + String get editMessageError => 'Edit message request failed'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return '$user has been unmuted'; + return '$user has been muted'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) return 'Error unmuting a user, please try again'; + return 'Error muting a user, please try again'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Create a new poll'; @@ -649,6 +691,12 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get endVoteLabel => 'End Poll'; + @override + String get endVoteSuccessMessage => 'Poll ended'; + + @override + String get endVoteErrorMessage => 'Failed to end the poll'; + @override String get pollResultsLabel => 'Poll Results'; @@ -702,6 +750,9 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get holdToRecordLabel => 'Hold to record. Release to save.'; + @override + String get audioRecordingPermissionMessage => 'Please allow Audio permissions in settings.'; + @override String get sendAnywayLabel => 'Send Anyway'; 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 2f90d3f4ed..980164b0e7 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 @@ -517,6 +517,50 @@ No es posible añadir más de $limit archivos adjuntos 'Error al marcar el mensaje como no leído. No se pueden marcar mensajes' ' no leídos más antiguos que los últimos 100 mensajes del canal.'; + @override + String get messageMarkedAsUnreadText => 'Mensaje marcado como no leído'; + + @override + String get deleteMessageError => 'Error al eliminar el mensaje'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Mensaje fijado'; + return 'Mensaje desfijado'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Error al fijar el mensaje'; + return 'Error al desfijar el mensaje'; + } + + @override + String get flagMessageError => 'Error al reportar el mensaje'; + + @override + String get messageCopiedToClipboardText => 'Mensaje copiado al portapapeles'; + + @override + String get sendMessageError => 'No se pudo enviar el mensaje'; + + @override + String get editMessageError => 'No se pudo editar el mensaje'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return 'Se ha dejado de silenciar a $user'; + return 'Se ha silenciado a $user'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return 'Error al dejar de silenciar al usuario, inténtalo de nuevo'; + } + return 'Error al silenciar al usuario, inténtalo de nuevo'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Crear un nuevo sondeo'; @@ -653,6 +697,12 @@ No es posible añadir más de $limit archivos adjuntos @override String get endVoteLabel => 'Finalizar votación'; + @override + String get endVoteSuccessMessage => 'Votación finalizada'; + + @override + String get endVoteErrorMessage => 'No se pudo finalizar la votación'; + @override String get pollResultsLabel => 'Resultados de la encuesta'; @@ -706,6 +756,9 @@ No es posible añadir más de $limit archivos adjuntos @override String get holdToRecordLabel => 'Mantén pulsado para grabar, suelta para enviar'; + @override + String get audioRecordingPermissionMessage => 'Permite los permisos de audio en la configuración.'; + @override String get sendAnywayLabel => 'Enviar de todos modos'; 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 77d17595f4..9183215e64 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 @@ -519,6 +519,50 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ ' des messages non lus plus anciens que les 100 derniers messages' ' du canal.'; + @override + String get messageMarkedAsUnreadText => 'Message marqué comme non lu'; + + @override + String get deleteMessageError => 'Erreur lors de la suppression du message'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Message épinglé'; + return 'Message désépinglé'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return "Erreur lors de l'épinglage du message"; + return 'Erreur lors du désépinglage du message'; + } + + @override + String get flagMessageError => 'Erreur lors du signalement du message'; + + @override + String get messageCopiedToClipboardText => 'Message copié dans le presse-papiers'; + + @override + String get sendMessageError => "Échec de l'envoi du message"; + + @override + String get editMessageError => 'Échec de la modification du message'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return 'Le son de $user a été réactivé'; + return 'Le son de $user a été coupé'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return 'Erreur lors de la réactivation du son, veuillez réessayer'; + } + return 'Erreur lors de la coupure du son, veuillez réessayer'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Créer un sondage'; @@ -655,6 +699,12 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get endVoteLabel => 'Terminer le vote'; + @override + String get endVoteSuccessMessage => 'Vote terminé'; + + @override + String get endVoteErrorMessage => 'Impossible de terminer le vote'; + @override String get pollResultsLabel => 'Résultats du sondage'; @@ -708,6 +758,9 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get holdToRecordLabel => 'Maintenez pour enregistrer, relâchez pour envoyer'; + @override + String get audioRecordingPermissionMessage => 'Veuillez autoriser les permissions audio dans les paramètres.'; + @override String get sendAnywayLabel => 'Envoyer quand même'; 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 eaaea55d36..eb7e4bd22e 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 @@ -515,6 +515,50 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { 'संदेश को अपठित मार्क करने में त्रुटि। सबसे नए 100 चैनल संदेश से पहले के' ' सभी अपठित संदेशों को अपठित मार्क नहीं किया जा सकता है।'; + @override + String get messageMarkedAsUnreadText => 'संदेश को अपठित के रूप में चिह्नित किया गया'; + + @override + String get deleteMessageError => 'संदेश हटाने में त्रुटि'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'संदेश पिन किया गया'; + return 'संदेश अनपिन किया गया'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'संदेश पिन करने में त्रुटि'; + return 'संदेश पिन हटाने में त्रुटि'; + } + + @override + String get flagMessageError => 'फ्लैग जोड़ने में त्रुटि'; + + @override + String get messageCopiedToClipboardText => 'संदेश क्लिपबोर्ड पर कॉपी किया गया'; + + @override + String get sendMessageError => 'संदेश भेजने में विफल'; + + @override + String get editMessageError => 'संदेश संपादित करने में विफल'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return '$user को अनम्यूट कर दिया गया है'; + return '$user को म्यूट कर दिया गया है'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return 'उपयोगकर्ता को अनम्यूट करने में त्रुटि, कृपया पुनः प्रयास करें'; + } + return 'उपयोगकर्ता को म्यूट करने में त्रुटि, कृपया पुनः प्रयास करें'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'एक नया पोल बनाएँ'; @@ -592,6 +636,12 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get endVoteLabel => 'वोट समाप्त करें'; + @override + String get endVoteSuccessMessage => 'वोट समाप्त हो गया'; + + @override + String get endVoteErrorMessage => 'वोट समाप्त करने में विफल'; + @override String get enterANewOptionLabel => 'एक नया विकल्प दर्ज करें'; @@ -704,6 +754,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get holdToRecordLabel => 'रिकॉर्ड करने के लिए दबाए रखें, भेजने के लिए छोड़ें'; + @override + String get audioRecordingPermissionMessage => 'कृपया सेटिंग्स में ऑडियो अनुमति दें।'; + @override String get sendAnywayLabel => 'फिर भी भेजें'; 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 1510cbab32..bce246213c 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 @@ -522,6 +522,50 @@ Attenzione: il limite massimo di $limit file è stato superato. ' marcare messaggi non letti più vecchi dei più recenti 100 messaggi' ' del canale.'; + @override + String get messageMarkedAsUnreadText => 'Messaggio contrassegnato come non letto'; + + @override + String get deleteMessageError => "Errore durante l'eliminazione del messaggio"; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Messaggio messo in evidenza'; + return 'Messaggio rimosso dagli elementi in evidenza'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Errore durante la messa in evidenza del messaggio'; + return 'Errore durante la rimozione del messaggio dagli elementi in evidenza'; + } + + @override + String get flagMessageError => 'Errore durante la segnalazione del messaggio'; + + @override + String get messageCopiedToClipboardText => 'Messaggio copiato negli appunti'; + + @override + String get sendMessageError => 'Invio del messaggio non riuscito'; + + @override + String get editMessageError => 'Modifica del messaggio non riuscita'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return "L'audio di $user è stato attivato"; + return "L'audio di $user è stato disattivato"; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return "Errore durante l'attivazione dell'audio, riprova"; + } + return "Errore durante la disattivazione dell'audio, riprova"; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Crea un nuovo sondaggio'; @@ -658,6 +702,12 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get endVoteLabel => 'Termina votazione'; + @override + String get endVoteSuccessMessage => 'Votazione terminata'; + + @override + String get endVoteErrorMessage => 'Impossibile terminare la votazione'; + @override String get pollResultsLabel => 'Risultati del sondaggio'; @@ -711,6 +761,9 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get holdToRecordLabel => 'Tieni premuto per registrare, rilascia per inviare'; + @override + String get audioRecordingPermissionMessage => 'Consenti le autorizzazioni audio nelle impostazioni.'; + @override String get sendAnywayLabel => 'Invia comunque'; 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 3e443c6f05..8e990e74f4 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 @@ -501,6 +501,50 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get markUnreadError => 'メッセージを未読にする際にエラーが発生しました。最新の100件のチャンネルメッセージより古い未読メッセージはマークできません。'; + @override + String get messageMarkedAsUnreadText => 'メッセージを未読としてマークしました'; + + @override + String get deleteMessageError => 'メッセージの削除中にエラーが発生しました'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'メッセージをピンしました'; + return 'メッセージのピンを外しました'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'メッセージのピンに失敗しました'; + return 'メッセージのピン解除に失敗しました'; + } + + @override + String get flagMessageError => 'フラグの追加中にエラーが発生しました'; + + @override + String get messageCopiedToClipboardText => 'メッセージをクリップボードにコピーしました'; + + @override + String get sendMessageError => 'メッセージの送信に失敗しました'; + + @override + String get editMessageError => 'メッセージの編集に失敗しました'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return '$userのミュートを解除しました'; + return '$userをミュートしました'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return 'ユーザーのミュート解除に失敗しました。もう一度お試しください'; + } + return 'ユーザーのミュートに失敗しました。もう一度お試しください'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return '新しい投票を作成する'; @@ -633,6 +677,12 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get endVoteLabel => '投票を終了'; + @override + String get endVoteSuccessMessage => '投票が終了しました'; + + @override + String get endVoteErrorMessage => '投票を終了できませんでした'; + @override String get pollResultsLabel => '投票結果'; @@ -685,6 +735,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get holdToRecordLabel => '長押しで録音、離すと送信'; + @override + String get audioRecordingPermissionMessage => '設定でオーディオの権限を許可してください。'; + @override String get sendAnywayLabel => 'それでも送信'; 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 f22a77c352..056663db07 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 @@ -504,6 +504,50 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { '메시지를 읽지 않음으로 표시하는 중 오류가 발생했습니다. 가장 최근 100개의 채널 메시지보다 오래된 읽지 않은 메시지는' ' 표시할 수 없습니다.'; + @override + String get messageMarkedAsUnreadText => '메시지를 읽지 않음으로 표시했습니다'; + + @override + String get deleteMessageError => '메시지를 삭제하는 중 오류가 발생했습니다'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return '메시지를 고정했습니다'; + return '메시지 고정을 해제했습니다'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return '메시지를 고정하는 중 오류가 발생했습니다'; + return '메시지 고정을 해제하는 중 오류가 발생했습니다'; + } + + @override + String get flagMessageError => '플래그를 추가하는 중 오류가 발생했습니다'; + + @override + String get messageCopiedToClipboardText => '메시지를 클립보드에 복사했습니다'; + + @override + String get sendMessageError => '메시지 전송에 실패했습니다'; + + @override + String get editMessageError => '메시지 편집에 실패했습니다'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return '$user님의 음소거를 해제했습니다'; + return '$user님을 음소거했습니다'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return '사용자 음소거 해제 중 오류가 발생했습니다. 다시 시도해 주세요'; + } + return '사용자 음소거 중 오류가 발생했습니다. 다시 시도해 주세요'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return '새 투표 만들기'; @@ -636,6 +680,12 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get endVoteLabel => '투표 종료'; + @override + String get endVoteSuccessMessage => '투표가 종료되었습니다'; + + @override + String get endVoteErrorMessage => '투표를 종료하지 못했습니다'; + @override String get pollResultsLabel => '투표 결과'; @@ -688,6 +738,9 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get holdToRecordLabel => '길게 눌러서 녹음, 놓아서 전송'; + @override + String get audioRecordingPermissionMessage => '설정에서 오디오 권한을 허용해 주세요.'; + @override String get sendAnywayLabel => '그래도 보내기'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart index 180160c55d..367c0194c4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart @@ -501,6 +501,48 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { 'Feil ved merking av melding som ulest. Kan ikke merke meldinger som' ' uleste som er eldre enn de 100 nyeste kanalmeldingene.'; + @override + String get messageMarkedAsUnreadText => 'Melding merket som ulest'; + + @override + String get deleteMessageError => 'Feil ved sletting av melding'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Melding festet'; + return 'Melding løsnet'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Feil ved festing av melding'; + return 'Feil ved løsning av melding'; + } + + @override + String get flagMessageError => 'Feil ved rapportering av melding'; + + @override + String get messageCopiedToClipboardText => 'Melding kopiert til utklippstavlen'; + + @override + String get sendMessageError => 'Kunne ikke sende meldingen'; + + @override + String get editMessageError => 'Kunne ikke redigere meldingen'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return 'Lyden til $user er slått på igjen'; + return '$user er dempet'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) return 'Feil ved oppheving av demping, prøv igjen'; + return 'Feil ved demping av bruker, prøv igjen'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Opprett en ny avstemning'; @@ -634,6 +676,12 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get endVoteLabel => 'Avslutt avstemning'; + @override + String get endVoteSuccessMessage => 'Avstemning avsluttet'; + + @override + String get endVoteErrorMessage => 'Kunne ikke avslutte avstemningen'; + @override String get pollResultsLabel => 'Resultater for avstemningen'; @@ -687,6 +735,9 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get holdToRecordLabel => 'Hold for å ta opp, slipp for å sende'; + @override + String get audioRecordingPermissionMessage => 'Tillat lydtillatelser i innstillingene.'; + @override String get sendAnywayLabel => 'Send likevel'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 5909875c1f..68b748906f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -516,6 +516,50 @@ Não é possível adicionar mais de $limit arquivos de uma vez 'Erro ao marcar a mensagem como não lida. Não é possível marcar mensagens' ' não lidas mais antigas do que as 100 mensagens mais recentes do canal.'; + @override + String get messageMarkedAsUnreadText => 'Mensagem marcada como não lida'; + + @override + String get deleteMessageError => 'Erro ao excluir a mensagem'; + + @override + String togglePinUnpinMessageSuccessText({required bool pinned}) { + if (pinned) return 'Mensagem fixada'; + return 'Mensagem desafixada'; + } + + @override + String togglePinUnpinMessageErrorText({required bool pinned}) { + if (pinned) return 'Erro ao fixar a mensagem'; + return 'Erro ao desafixar a mensagem'; + } + + @override + String get flagMessageError => 'Erro ao denunciar a mensagem'; + + @override + String get messageCopiedToClipboardText => 'Mensagem copiada para a área de transferência'; + + @override + String get sendMessageError => 'Falha ao enviar a mensagem'; + + @override + String get editMessageError => 'Falha ao editar a mensagem'; + + @override + String toggleMuteUnmuteUserSuccessText({required String user, required bool isMuted}) { + if (isMuted) return 'O som de $user foi ativado'; + return '$user foi silenciado'; + } + + @override + String toggleMuteUnmuteUserErrorText({required bool isMuted}) { + if (isMuted) { + return 'Erro ao ativar o som do usuário, tente novamente'; + } + return 'Erro ao silenciar o usuário, tente novamente'; + } + @override String createPollLabel({bool isNew = false}) { if (isNew) return 'Criar uma nova sondagem'; @@ -652,6 +696,12 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get endVoteLabel => 'Encerrar votação'; + @override + String get endVoteSuccessMessage => 'Votação encerrada'; + + @override + String get endVoteErrorMessage => 'Não foi possível encerrar a votação'; + @override String get pollResultsLabel => 'Resultados da votação'; @@ -705,6 +755,9 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get holdToRecordLabel => 'Mantenha pressionado para gravar, solte para enviar'; + @override + String get audioRecordingPermissionMessage => 'Permita as permissões de áudio nas configurações.'; + @override String get sendAnywayLabel => 'Enviar mesmo assim'; diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index a763436c90..8448293406 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -100,6 +100,10 @@ void main() { expect(localizations.togglePinUnpinText(pinned: true), isNotNull); // un-pinned expect(localizations.togglePinUnpinText(pinned: false), isNotNull); + expect(localizations.togglePinUnpinMessageSuccessText(pinned: true), isNotNull); + expect(localizations.togglePinUnpinMessageSuccessText(pinned: false), isNotNull); + expect(localizations.togglePinUnpinMessageErrorText(pinned: true), isNotNull); + expect(localizations.togglePinUnpinMessageErrorText(pinned: false), isNotNull); // delete-failed expect( localizations.toggleDeleteRetryDeleteMessageText(isDeleteFailed: true), @@ -222,6 +226,16 @@ void main() { expect(localizations.toggleMuteUnmuteUserQuestion(isMuted: true), isNotNull); expect(localizations.toggleMuteUnmuteUserText(isMuted: true), isNotNull); expect(localizations.toggleMuteUnmuteUserText(isMuted: false), isNotNull); + expect( + localizations.toggleMuteUnmuteUserSuccessText(user: 'test-user', isMuted: true), + isNotNull, + ); + expect( + localizations.toggleMuteUnmuteUserSuccessText(user: 'test-user', isMuted: false), + isNotNull, + ); + expect(localizations.toggleMuteUnmuteUserErrorText(isMuted: true), isNotNull); + expect(localizations.toggleMuteUnmuteUserErrorText(isMuted: false), isNotNull); expect(localizations.toggleBlockUnblockUserText(isBlocked: true), isNotNull); expect(localizations.toggleBlockUnblockUserText(isBlocked: false), isNotNull); expect(localizations.viewLibrary, isNotNull); @@ -232,6 +246,12 @@ void main() { expect(localizations.unreadMessagesSeparatorText(), isNotNull); expect(localizations.markUnreadError, isNotNull); expect(localizations.markAsUnreadLabel, isNotNull); + expect(localizations.messageMarkedAsUnreadText, isNotNull); + expect(localizations.deleteMessageError, isNotNull); + expect(localizations.flagMessageError, isNotNull); + expect(localizations.messageCopiedToClipboardText, isNotNull); + expect(localizations.sendMessageError, isNotNull); + expect(localizations.editMessageError, isNotNull); // Create poll expect(localizations.createPollLabel(), isNotNull); // Create a new poll @@ -282,6 +302,8 @@ void main() { expect(localizations.createLabel, isNotNull); expect(localizations.endLabel, isNotNull); expect(localizations.endVoteLabel, isNotNull); + expect(localizations.endVoteSuccessMessage, isNotNull); + expect(localizations.endVoteErrorMessage, isNotNull); expect(localizations.enterANewOptionLabel, isNotNull); expect(localizations.enterYourCommentLabel, isNotNull); expect(localizations.loadingPollVotesError, isNotNull); @@ -328,6 +350,7 @@ void main() { expect(localizations.loadingLabel, isNotNull); expect(localizations.slideToCancelLabel, isNotNull); expect(localizations.holdToRecordLabel, isNotNull); + expect(localizations.audioRecordingPermissionMessage, isNotNull); expect(localizations.sendAnywayLabel, isNotNull); expect(localizations.moderatedMessageBlockedText, isNotNull); expect(localizations.moderationReviewModalTitle, isNotNull); diff --git a/sample_app/lib/pages/chat_info_screen.dart b/sample_app/lib/pages/chat_info_screen.dart index 77906922ed..f77b4fd770 100644 --- a/sample_app/lib/pages/chat_info_screen.dart +++ b/sample_app/lib/pages/chat_info_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:sample_app/pages/channel_file_display_screen.dart'; import 'package:sample_app/pages/channel_media_display_screen.dart'; import 'package:sample_app/pages/pinned_messages_screen.dart'; +import 'package:sample_app/utils/action_feedback.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Detail screen for a 1:1 chat correspondence. @@ -182,10 +183,21 @@ class _ActionsSection extends StatelessWidget { trailing: StreamSwitch( value: isMuted, onChanged: (_) { + final messenger = StreamSnackbarMessenger.maybeOf(context); if (isMuted) { - channel.unmute(); + runWithFeedback( + messenger, + channel.unmute, + successMessage: 'Channel unmuted', + errorMessage: 'Failed to update channel mute status', + ).ignore(); } else { - channel.mute(); + runWithFeedback( + messenger, + channel.mute, + successMessage: 'Channel muted', + errorMessage: 'Failed to update channel mute status', + ).ignore(); } }, ), @@ -216,6 +228,7 @@ class _ActionsSection extends StatelessWidget { Future _confirmDelete(BuildContext context) async { final navigator = Navigator.of(context); final channel = StreamChannel.of(context).channel; + final messenger = StreamSnackbarMessenger.maybeOf(context); final confirmed = await _showConfirmationDialog( context: context, @@ -225,11 +238,22 @@ class _ActionsSection extends StatelessWidget { ); if (confirmed != true) return; - await channel.delete(); - // Pop every screen until we land on the channel list — going back to - // the channel page would crash trying to read state from the now - // deleted channel. - navigator.popUntil((route) => route.isFirst); + try { + await channel.delete(); + messenger?.show( + StreamSnackbar(message: const Text('Channel deleted'), variant: .success), + replace: true, + ); + // Pop every screen until we land on the channel list — going back to + // the channel page would crash trying to read state from the now + // deleted channel. + navigator.popUntil((route) => route.isFirst); + } catch (_) { + messenger?.show( + StreamSnackbar(message: const Text('Failed to delete channel'), variant: .error), + replace: true, + ); + } } } diff --git a/sample_app/lib/pages/group_info_screen.dart b/sample_app/lib/pages/group_info_screen.dart index 3f26366c6f..8a54b35c02 100644 --- a/sample_app/lib/pages/group_info_screen.dart +++ b/sample_app/lib/pages/group_info_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:sample_app/pages/channel_file_display_screen.dart'; import 'package:sample_app/pages/channel_media_display_screen.dart'; import 'package:sample_app/pages/pinned_messages_screen.dart'; +import 'package:sample_app/utils/action_feedback.dart'; import 'package:sample_app/widgets/add_members_sheet.dart'; import 'package:sample_app/widgets/all_members_sheet.dart'; import 'package:sample_app/widgets/edit_group_sheet.dart'; @@ -295,10 +296,21 @@ class _ActionsSection extends StatelessWidget { trailing: StreamSwitch( value: isMuted, onChanged: (_) { + final messenger = StreamSnackbarMessenger.maybeOf(context); if (isMuted) { - channel.unmute(); + runWithFeedback( + messenger, + channel.unmute, + successMessage: 'Channel unmuted', + errorMessage: 'Failed to update channel mute status', + ).ignore(); } else { - channel.mute(); + runWithFeedback( + messenger, + channel.mute, + successMessage: 'Channel muted', + errorMessage: 'Failed to update channel mute status', + ).ignore(); } }, ), @@ -328,6 +340,7 @@ class _ActionsSection extends StatelessWidget { final currentUserId = StreamChat.of(context).currentUser?.id; if (currentUserId == null) return; + final messenger = StreamSnackbarMessenger.maybeOf(context); final confirmed = await _showConfirmationDialog( context: context, title: 'Leave group', @@ -336,15 +349,27 @@ class _ActionsSection extends StatelessWidget { ); if (confirmed != true) return; - await channel.removeMembers([currentUserId]); - // Pop every screen until we land on the channel list — going back to - // the channel page would crash since we're no longer a member. - navigator.popUntil((route) => route.isFirst); + try { + await channel.removeMembers([currentUserId]); + messenger?.show( + StreamSnackbar(message: const Text('Left channel'), variant: .success), + replace: true, + ); + // Pop every screen until we land on the channel list — going back to + // the channel page would crash since we're no longer a member. + navigator.popUntil((route) => route.isFirst); + } catch (_) { + messenger?.show( + StreamSnackbar(message: const Text('Failed to leave channel'), variant: .error), + replace: true, + ); + } } Future _confirmDelete(BuildContext context) async { final navigator = Navigator.of(context); final channel = StreamChannel.of(context).channel; + final messenger = StreamSnackbarMessenger.maybeOf(context); final confirmed = await _showConfirmationDialog( context: context, @@ -354,11 +379,22 @@ class _ActionsSection extends StatelessWidget { ); if (confirmed != true) return; - await channel.delete(); - // Pop every screen until we land on the channel list — going back to - // the channel page would crash trying to read state from the now - // deleted channel. - navigator.popUntil((route) => route.isFirst); + try { + await channel.delete(); + messenger?.show( + StreamSnackbar(message: const Text('Channel deleted'), variant: .success), + replace: true, + ); + // Pop every screen until we land on the channel list — going back to + // the channel page would crash trying to read state from the now + // deleted channel. + navigator.popUntil((route) => route.isFirst); + } catch (_) { + messenger?.show( + StreamSnackbar(message: const Text('Failed to delete channel'), variant: .error), + replace: true, + ); + } } } diff --git a/sample_app/lib/utils/action_feedback.dart b/sample_app/lib/utils/action_feedback.dart new file mode 100644 index 0000000000..244eb3ff58 --- /dev/null +++ b/sample_app/lib/utils/action_feedback.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +// Runs [action] and shows a success or error snackbar on [messenger] with the +// given messages. When [messenger] is null the action still runs; only the +// snackbar feedback is skipped. +Future runWithFeedback( + StreamSnackbarMessenger? messenger, + Future Function() action, { + required String errorMessage, + String? successMessage, +}) async { + try { + await action(); + if (successMessage == null) return; + messenger?.show( + StreamSnackbar(message: Text(successMessage), variant: .success), + replace: true, + ); + } on Exception catch (_) { + messenger?.show( + StreamSnackbar(message: Text(errorMessage), variant: .error), + replace: true, + ); + } +} diff --git a/sample_app/lib/widgets/channel_list.dart b/sample_app/lib/widgets/channel_list.dart index bb78718161..b2775698dc 100644 --- a/sample_app/lib/widgets/channel_list.dart +++ b/sample_app/lib/widgets/channel_list.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; import 'package:sample_app/routes/routes.dart'; +import 'package:sample_app/utils/action_feedback.dart'; import 'package:sample_app/widgets/channel_detail_sheet.dart'; import 'package:sample_app/widgets/search_text_field.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -137,8 +138,21 @@ class _ChannelListDefault extends StatelessWidget { foregroundColor: colorScheme.textOnAccent, backgroundColor: colorScheme.accentPrimary, onPressed: (_) { - if (isMuted) return channel.unmute().ignore(); - return channel.mute().ignore(); + final messenger = StreamSnackbarMessenger.maybeOf(context); + if (isMuted) { + return runWithFeedback( + messenger, + channel.unmute, + successMessage: 'Channel unmuted', + errorMessage: 'Failed to update channel mute status', + ).ignore(); + } + return runWithFeedback( + messenger, + channel.mute, + successMessage: 'Channel muted', + errorMessage: 'Failed to update channel mute status', + ).ignore(); }, child: Icon(isMuted ? icons.audio : icons.mute, size: 20), ), @@ -186,15 +200,41 @@ Future _onChannelDetailAction( ChannelDetailAction action, ) async { final client = StreamChat.of(context).client; + final messenger = StreamSnackbarMessenger.maybeOf(context); return switch (action) { ViewChannelInfo(:final user) => _pushChannelInfo(context, channel, user), - PinChannel() => channel.pin(), - UnpinChannel() => channel.unpin(), - MuteChannelMember(:final user) => client.muteUser(user.id), - UnmuteChannelMember(:final user) => client.unmuteUser(user.id), - BlockChannelMember(:final user) => client.blockUser(user.id), - LeaveChannel() => _maybeLeaveChannel(context, channel), - DeleteChannel() => _maybeDeleteChannel(context, channel), + PinChannel() => runWithFeedback( + messenger, + () => channel.pin(), + successMessage: 'Channel pinned', + errorMessage: 'Failed to update channel pinned status', + ), + UnpinChannel() => runWithFeedback( + messenger, + () => channel.unpin(), + successMessage: 'Channel unpinned', + errorMessage: 'Failed to update channel pinned status', + ), + MuteChannelMember(:final user) => runWithFeedback( + messenger, + () => client.muteUser(user.id), + successMessage: '${user.name} has been muted', + errorMessage: 'Error muting a user, please try again', + ), + UnmuteChannelMember(:final user) => runWithFeedback( + messenger, + () => client.unmuteUser(user.id), + successMessage: '${user.name} has been unmuted', + errorMessage: 'Error unmuting a user, please try again', + ), + BlockChannelMember(:final user) => runWithFeedback( + messenger, + () => client.blockUser(user.id), + successMessage: 'User blocked', + errorMessage: 'Failed to block user', + ), + LeaveChannel() => _maybeLeaveChannel(context, channel, messenger), + DeleteChannel() => _maybeDeleteChannel(context, channel, messenger), }; } @@ -222,7 +262,11 @@ Future _pushChannelInfo(BuildContext context, Channel channel, User? user) // Shows a confirmation dialog before removing the current user from the // channel. Leave is only surfaced for group channels in the detail sheet, // so the copy is group-specific here. -Future _maybeLeaveChannel(BuildContext context, Channel channel) async { +Future _maybeLeaveChannel( + BuildContext context, + Channel channel, + StreamSnackbarMessenger? messenger, +) async { final currentUserId = StreamChat.of(context).currentUser?.id; if (currentUserId == null) return; @@ -234,13 +278,22 @@ Future _maybeLeaveChannel(BuildContext context, Channel channel) async { ); if (confirmed != true) return; - await channel.removeMembers([currentUserId]); + return runWithFeedback( + messenger, + () => channel.removeMembers([currentUserId]), + successMessage: 'Left channel', + errorMessage: 'Failed to leave channel', + ); } // Shows a confirmation dialog before deleting the channel. On success, pops // the channel page if currently visible (e.g. when invoked from inside a // channel route). -Future _maybeDeleteChannel(BuildContext context, Channel channel) async { +Future _maybeDeleteChannel( + BuildContext context, + Channel channel, + StreamSnackbarMessenger? messenger, +) async { final router = GoRouter.of(context); final subject = channel.isOneToOne ? 'conversation' : 'group'; @@ -252,8 +305,20 @@ Future _maybeDeleteChannel(BuildContext context, Channel channel) async { ); if (confirmed != true) return; - await channel.delete(); - if (router.canPop()) router.pop(); + + try { + await channel.delete(); + messenger?.show( + StreamSnackbar(message: const Text('Channel deleted'), variant: .success), + replace: true, + ); + if (router.canPop()) router.pop(); + } catch (_) { + messenger?.show( + StreamSnackbar(message: const Text('Failed to delete channel'), variant: .error), + replace: true, + ); + } } class _ChannelListSearch extends StatelessWidget {