Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion docs/docs_screenshots/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ dependencies:
sdk: flutter
record: ^6.2.0
stream_chat_flutter: ^10.0.1
stream_core_flutter: ^0.3.0
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter
ref: 5353033ea55b88e8f651c604389f0b7b0a839ebe
path: packages/stream_core_flutter

dev_dependencies:
alchemist: ^0.14.0
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot in the docs tests is broken

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ Widget _buildVoiceRecordingComposerScaffold({
children: [
Expanded(child: Container()),
Builder(
builder: (context) {
return Material(
builder: (context) => StreamSnackbarPopup(
child: Material(
child: DecoratedBox(
decoration: BoxDecoration(
color: context.streamColorScheme.backgroundElevation1,
Expand All @@ -132,8 +132,8 @@ Widget _buildVoiceRecordingComposerScaffold({
),
),
),
);
},
),
),
),
],
),
Expand Down
7 changes: 6 additions & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ command:
stream_chat_persistence: ^10.0.1
streaming_shared_preferences: ^2.0.0
svg_icon_widget: ^0.0.1
stream_core_flutter: ^0.3.0
# Pinned to PR #118 (feat: add StreamSnackbar) until released.
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter
ref: 5353033ea55b88e8f651c604389f0b7b0a839ebe
path: packages/stream_core_flutter
synchronized: ^3.4.0
thumblr: ^0.0.4
url_launcher: ^6.3.2
Expand Down
11 changes: 11 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## Upcoming

✅ Added

- `StreamMessageComposer` now surfaces the hold-to-record hint through `StreamSnackbar` anchored above the composer, and `StreamChat` provides an app-wide `StreamSnackbarScope` fallback.

⚠️ Deprecated

- `StreamAudioRecorderController.showInfo` is now deprecated. Show your own snackbar via `StreamSnackbarMessenger.of(context).show(StreamSnackbar(...))` instead.
- `RecordStateIdle.message` is now deprecated; the composer no longer reads it.

## 10.0.1

🐞 Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class StreamAudioRecorderController extends ValueNotifier<AudioRecorderState> {
/// Shows an info message to the user for the given [duration].
///
/// This is useful for showing messages like "Hold to record" or "Recording".
@Deprecated('Use StreamSnackbar via StreamSnackbarMessenger instead.')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we still use this state, but just use this state to show a self-controllled snackbar? This way we're immediately breaking potential custom implementations.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but it will be a tech debt for us, but i get your point

void showInfo(
String message, {
Duration duration = const Duration(seconds: 3),
Expand All @@ -198,6 +199,18 @@ class StreamAudioRecorderController extends ValueNotifier<AudioRecorderState> {
}
}

/// Cancels any pending info-message timer and clears the message on
/// [RecordStateIdle]. Counterpart to [showInfo].
@Deprecated('Use StreamSnackbar via StreamSnackbarMessenger instead.')
void hideInfo() {
// Cancel the info timer.
_infoTimer?.cancel();
_infoTimer = null;

// Clear the info message if it is currently being shown.
if (value case RecordStateIdle()) value = const RecordStateIdle();
}

Future<String> _getOutputFilePath(AudioEncoder encoder) async {
// Ignored on web platform.
if (CurrentPlatform.isWeb) return '';
Expand Down Expand Up @@ -232,6 +245,8 @@ class StreamAudioRecorderController extends ValueNotifier<AudioRecorderState> {
void dispose() {
_durationTimer?.cancel();
_durationTimer = null;
_infoTimer?.cancel();
_infoTimer = null;
_recorderAmplitudeSubscription?.cancel();
_recorder.dispose();
super.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,13 @@ sealed class AudioRecorderState {

/// {@template recordStateIdle}
/// The audio recorder is currently idle and not recording any audio track.
///
/// Optionally, provide a [message] to display when the recorder is idle.
///
/// For example, when the user has not long pressed the record button long
/// enough to start recording.
/// {@endtemplate}
final class RecordStateIdle extends AudioRecorderState {
/// {@macro recordStateIdle}
const RecordStateIdle({this.message}) : super._();

/// The optional message to display when the recorder is idle.
@Deprecated('Use StreamSnackbar via StreamSnackbarMessenger instead.')
final String? message;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ class _StreamChatMessageInputState extends State<StreamChatMessageInput> {
void initState() {
super.initState();
_initController();
widget.audioRecorderController?.addListener(_onAudioRecorderChanged);
// Update the snackbar based on the initial state of the audio recorder controller
// after the first frame is rendered, to ensure that the BuildContext is fully available for showing the snackbar.
WidgetsBinding.instance.addPostFrameCallback((_) => _onAudioRecorderChanged());
}

@override
Expand All @@ -118,10 +122,16 @@ class _StreamChatMessageInputState extends State<StreamChatMessageInput> {
if (oldWidget.controller == null) _controller.dispose();
_initController();
}
if (widget.audioRecorderController != oldWidget.audioRecorderController) {
oldWidget.audioRecorderController?.removeListener(_onAudioRecorderChanged);
widget.audioRecorderController?.addListener(_onAudioRecorderChanged);
_onAudioRecorderChanged(); // Update the snackbar based on the new controller's state immediately.
}
Comment thread
xsahil03x marked this conversation as resolved.
}

@override
void dispose() {
widget.audioRecorderController?.removeListener(_onAudioRecorderChanged);
if (widget.controller == null) _controller.dispose();
super.dispose();
}
Expand All @@ -130,6 +140,27 @@ class _StreamChatMessageInputState extends State<StreamChatMessageInput> {
_controller = widget.controller ?? StreamMessageComposerController();
}

// Listens to changes in the audio recorder controller and shows/hides a snackbar
// with the current message, if any.
void _onAudioRecorderChanged() {
if (!mounted) return;

final state = widget.audioRecorderController?.value;
final message = state is RecordStateIdle ? state.message : null;
final messenger = StreamSnackbarMessenger.maybeOf(context);

if (message == null || message.isEmpty) return messenger?.removeCurrent();
final controller = messenger?.show(StreamSnackbar(message: Text(message)), replace: true);
if (controller == null) return;

// Notify the recorder controller when the snackbar is closed, so it can clear
// the message and avoid showing stale messages if the user triggers the recorder again.
controller.closed.then((_) {
if (!mounted) return;
return widget.audioRecorderController?.hideInfo();
});
Comment thread
xsahil03x marked this conversation as resolved.
}

@override
Widget build(BuildContext context) {
final audioRecorderController = widget.audioRecorderController;
Expand All @@ -149,30 +180,18 @@ class _StreamChatMessageInputState extends State<StreamChatMessageInput> {
const targetAlignment = AlignmentDirectional.topEnd;
const followerAlignment = AlignmentDirectional.bottomEnd;

final idleMessage = state is RecordStateIdle ? state.message : null;
final showIdleTooltip = idleMessage != null && idleMessage.isNotEmpty;

return PortalTarget(
visible: showIdleTooltip,
anchor: Aligned(
target: Alignment.topCenter,
follower: Alignment.bottomCenter,
offset: Offset(0, -streamSpacing.md),
target: targetAlignment.resolve(textDirection),
follower: followerAlignment.resolve(textDirection),
offset: Offset(-streamSpacing.md, -streamSpacing.md).directional(textDirection),
),
portalFollower: showIdleTooltip ? HoldToRecordInfoTooltip(message: idleMessage) : const SizedBox.shrink(),
child: PortalTarget(
anchor: Aligned(
target: targetAlignment.resolve(textDirection),
follower: followerAlignment.resolve(textDirection),
offset: Offset(-streamSpacing.md, -streamSpacing.md).directional(textDirection),
),
visible: state is RecordStateRecording,
portalFollower: SwipeToLockButton(isLocked: state is RecordStateRecordingLocked),
child: _StreamChatMessageInputContent(
widget: widget,
inputController: _controller,
audioRecorderState: state,
),
visible: state is RecordStateRecording,
portalFollower: SwipeToLockButton(isLocked: state is RecordStateRecordingLocked),
child: _StreamChatMessageInputContent(
widget: widget,
inputController: _controller,
audioRecorderState: state,
),
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class StreamMessageComposer extends StatelessWidget {
TextCapitalization textCapitalization = TextCapitalization.sentences,
bool autofocus = false,
bool autoCorrect = true,
}) : props = MessageComposerProps(
}) : props = .new(
onMessageSent: onMessageSent,
preMessageSending: preMessageSending,
messageComposerController: messageComposerController,
Expand Down Expand Up @@ -147,8 +147,8 @@ class StreamMessageComposer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final builder = context.chatComponentBuilder<MessageComposerProps>();
if (builder != null) return builder(context, props);
return DefaultStreamMessageComposer(props: props);
final composer = builder?.call(context, props) ?? DefaultStreamMessageComposer(props: props);
return StreamSnackbarPopup(child: composer);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/stream_chat_flutter/lib/src/stream_chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class StreamChatState extends State<StreamChat> {
onBackgroundEventReceived: widget.onBackgroundEventReceived,
backgroundKeepAlive: widget.backgroundKeepAlive,
connectivityStream: widget.connectivityStream,
child: widget.child ?? const Empty(),
child: StreamSnackbarScope(child: widget.child ?? const Empty()),
);

final theme = widget.themeData ?? StreamChatThemeData();
Expand Down
14 changes: 14 additions & 0 deletions packages/stream_chat_flutter/lib/stream_chat_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ export 'package:stream_core_flutter/stream_core_flutter.dart'
StreamSheetTheme,
StreamSheetThemeData,
StreamSheetTransition,
StreamSnackbar,
StreamSnackbarAction,
StreamSnackbarClosedReason,
StreamSnackbarController,
StreamSnackbarHost,
StreamSnackbarMessenger,
StreamSnackbarPopup,
StreamSnackbarPopupPlacement,
StreamSnackbarProps,
StreamSnackbarScope,
StreamSnackbarStyle,
StreamSnackbarTheme,
StreamSnackbarThemeData,
StreamSnackbarVariant,
StreamSwitch,
StreamStepper,
StreamStepperProps,
Expand Down
6 changes: 5 additions & 1 deletion packages/stream_chat_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ dependencies:
share_plus: ">=12.0.2 <14.0.0"
shimmer: ^3.0.0
stream_chat_flutter_core: ^10.0.1
stream_core_flutter: ^0.3.0
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter
ref: 5353033ea55b88e8f651c604389f0b7b0a839ebe
path: packages/stream_core_flutter
svg_icon_widget: ^0.0.1
synchronized: ^3.4.0
theme_extensions_builder_annotation: ^7.1.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,36 @@ void main() {
});
});

group('hideInfo', () {
test('clears the info message immediately', () {
controller.showInfo('Test Message');
expect((controller.value as RecordStateIdle).message, 'Test Message');

controller.hideInfo();

expect((controller.value as RecordStateIdle).message, isNull);
});

test('cancels the pending auto-clear timer', () async {
controller.showInfo('Test Message', duration: const Duration(milliseconds: 100));
expect((controller.value as RecordStateIdle).message, 'Test Message');

controller.hideInfo();
controller.showInfo('Other Message', duration: const Duration(seconds: 5));
expect((controller.value as RecordStateIdle).message, 'Other Message');

// Wait past the original (now-canceled) timer; the new message must
// still be present.
await Future.delayed(const Duration(milliseconds: 150));
expect((controller.value as RecordStateIdle).message, 'Other Message');
});

test('is a no-op when no info message is being shown', () {
controller.hideInfo();
expect((controller.value as RecordStateIdle).message, isNull);
});
});

group('amplitude changes', () {
setUp(() {
when(() => mockRecorder.hasPermission(request: false)).thenAnswer((_) async => true);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,14 @@ void main() {
),
);

// Expect an empty box
expect(find.byType(SizedBox), findsOneWidget);
// Expect an empty box rendered by the attachment list itself.
expect(
find.descendant(
of: find.byType(StreamMessageComposerAttachmentList),
matching: find.byType(SizedBox),
),
findsOneWidget,
);
},
);
});
Expand Down Expand Up @@ -184,8 +190,14 @@ void main() {
),
);

// Expect an empty box
expect(find.byType(SizedBox), findsOneWidget);
// Expect an empty box rendered by the media attachments widget itself.
expect(
find.descendant(
of: find.byType(MessageInputMediaAttachments),
matching: find.byType(SizedBox),
),
findsOneWidget,
);
},
);

Expand Down
Loading
Loading