Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -136,10 +137,30 @@ class _DefaultStreamPollAttachmentState extends State<DefaultStreamPollAttachmen
final channel = StreamChannel.of(context).channel;

Future<void> 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<void> onAddComment() async {
Expand Down
100 changes: 100 additions & 0 deletions packages/stream_chat_flutter/lib/src/localization/translations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,25 @@ class StreamAudioRecorderController extends ValueNotifier<AudioRecorderState>
final AudioRecorder _recorder;

/// Starts a new recording session.
Future<void> 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<void> 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,14 @@ 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1471,13 +1471,15 @@ class DefaultStreamMessageComposerState extends State<DefaultStreamMessageCompos
required Message message,
required Channel channel,
}) async {
try {
// A message is considered fresh if it doesn't have a remoteCreatedAt.
final isFreshMessage = message.remoteCreatedAt == null;
// A message is considered fresh if it doesn't have a remoteCreatedAt.
final isFreshMessage = message.remoteCreatedAt == null;

// Note: edited messages which are bounced back with an error needs to be
// sent as new messages as the backend doesn't store them.
final isUpdate = !isFreshMessage && !message.isBouncedWithError;

// Note: edited messages which are bounced back with an error needs to be
// sent as new messages as the backend doesn't store them.
final resp = await switch (!isFreshMessage && !message.isBouncedWithError) {
try {
final resp = await switch (isUpdate) {
true => channel.updateMessage(message),
false => channel.sendMessage(message),
};
Expand All @@ -1488,6 +1490,17 @@ class DefaultStreamMessageComposerState extends State<DefaultStreamMessageCompos
return widget.props.onError?.call(e, stk);
}

if (mounted) {
final translations = context.translations;
StreamSnackbarMessenger.maybeOf(context)?.show(
StreamSnackbar(
message: Text(isUpdate ? translations.editMessageError : translations.sendMessageError),
variant: .error,
),
replace: true,
);
}

rethrow;
}
}
Expand Down
Loading
Loading