Skip to content

Commit

Permalink
compose_box: Send "typing stopped" notices on app lifecycle updates
Browse files Browse the repository at this point in the history
This is a bonus feature that covers some cases that FocusNode doesn't
cover.  We send a "typing stopped" notice when the app loses focus or
becomes invisible.

An example of WidgetsBindingObserver can be found here:
  https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver-class.html

The cited text comes from the AppLifecycleState documentation:
  https://github.com/flutter/engine/blob/a65f1d59edc618ae81e2e8ed78d59fb729291afa/lib/ui/platform_dispatcher.dart#L1856-L1991

The link is not included in code because the code themselves are
references to their documentation already.

Signed-off-by: Zixuan James Li <[email protected]>
  • Loading branch information
PIG208 committed Nov 5, 2024
1 parent 3144e7f commit cc37dae
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 1 deletion.
32 changes: 31 additions & 1 deletion lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,13 @@ class _ContentInput extends StatefulWidget {
State<_ContentInput> createState() => _ContentInputState();
}

class _ContentInputState extends State<_ContentInput> {
class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
widget.controller.addListener(_contentChanged);
widget.focusNode.addListener(_focusChanged);
WidgetsBinding.instance.addObserver(this);
}

@override
Expand All @@ -313,6 +314,7 @@ class _ContentInputState extends State<_ContentInput> {
void dispose() {
widget.controller.removeListener(_contentChanged);
widget.focusNode.removeListener(_focusChanged);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

Expand All @@ -333,6 +335,34 @@ class _ContentInputState extends State<_ContentInput> {
store.typingNotifier.stoppedComposing();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
// Transition to either [hidden] or [paused] signals that
// > [the] application is not currently visible to the user, and not
// > responding to user input.
//
// When transitioning to [detached], the compose box can't exist:
// > The application defaults to this state before it initializes, and
// > can be in this state (applicable on Android, iOS, and web) after
// > all views have been detached.
//
// For all these states, we can conclude that the user is not
// composing a message.
final store = PerAccountStoreWidget.of(context);
store.typingNotifier.stoppedComposing();
case AppLifecycleState.inactive:
// > At least one view of the application is visible, but none have
// > input focus. The application is otherwise running normally.
// For example, we expect this state when the user is selecting a file
// to upload.
case AppLifecycleState.resumed:
}
}

@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
Expand Down
22 changes: 22 additions & 0 deletions test/widgets/compose_box_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,28 @@ void main() {
await tester.pump(store.typingNotifier.typingStoppedWaitPeriod);
checkTypingRequest(TypingOp.stop, narrow);
});

testWidgets('unfocusing app sends a "typing stopped" notice', (tester) async {
await prepareComposeBox(tester, narrow: narrow);

await checkStartTyping(tester, narrow);

connection.prepare(json: {});
// While this state lives on [ServicesBinding], testWidgets resets it
// for us when the test ends so we don't have to:
// https://github.com/flutter/flutter/blob/c78c166e3ecf963ca29ed503e710fd3c71eda5c9/packages/flutter_test/lib/src/binding.dart#L1189
// On iOS and Android, a transition to [hidden] is synthesized before
// transitioning into [paused].
WidgetsBinding.instance.handleAppLifecycleStateChanged(
AppLifecycleState.hidden);
await tester.pump(Duration.zero);
checkTypingRequest(TypingOp.stop, narrow);

WidgetsBinding.instance.handleAppLifecycleStateChanged(
AppLifecycleState.paused);
await tester.pump(Duration.zero);
check(connection.lastRequest).isNull();
});
});

group('message-send request response', () {
Expand Down

0 comments on commit cc37dae

Please sign in to comment.