Skip to content

Commit 0ae829f

Browse files
committed
compose_box: Send "typing stopped" notices on app lifecycle updates
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 Signed-off-by: Zixuan James Li <[email protected]>
1 parent f7533ad commit 0ae829f

File tree

2 files changed

+41
-1
lines changed

2 files changed

+41
-1
lines changed

lib/widgets/compose_box.dart

+19-1
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,13 @@ class _ContentInput extends StatefulWidget {
288288
State<_ContentInput> createState() => _ContentInputState();
289289
}
290290

291-
class _ContentInputState extends State<_ContentInput> {
291+
class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserver {
292292
@override
293293
void initState() {
294294
super.initState();
295295
widget.controller.addListener(_contentChanged);
296296
widget.focusNode.addListener(_focusChanged);
297+
WidgetsBinding.instance.addObserver(this);
297298
}
298299

299300
@override
@@ -313,6 +314,7 @@ class _ContentInputState extends State<_ContentInput> {
313314
void dispose() {
314315
widget.controller.removeListener(_contentChanged);
315316
widget.focusNode.removeListener(_focusChanged);
317+
WidgetsBinding.instance.removeObserver(this);
316318
super.dispose();
317319
}
318320

@@ -337,6 +339,22 @@ class _ContentInputState extends State<_ContentInput> {
337339
store.typingNotifier.stoppedComposing();
338340
}
339341

342+
@override
343+
void didChangeAppLifecycleState(AppLifecycleState state) {
344+
switch (state) {
345+
// For AppLifecycleState changes, only signal the end of typing when
346+
// > [the] application is not currently visible to the user, and not
347+
// > responding to user input.
348+
case AppLifecycleState.hidden:
349+
case AppLifecycleState.paused:
350+
final store = PerAccountStoreWidget.of(context);
351+
store.typingNotifier.stoppedComposing();
352+
case AppLifecycleState.detached:
353+
case AppLifecycleState.inactive:
354+
case AppLifecycleState.resumed:
355+
}
356+
}
357+
340358
@override
341359
Widget build(BuildContext context) {
342360
ColorScheme colorScheme = Theme.of(context).colorScheme;

test/widgets/compose_box_test.dart

+22
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,28 @@ void main() {
361361
await tester.pump(store.typingNotifier.typingStoppedWaitPeriod);
362362
checkTypingRequest(TypingOp.stop, narrow);
363363
});
364+
365+
testWidgets('unfocusing app sends "typing stopped" notice', (tester) async {
366+
await prepareComposeBox(tester, narrow: narrow);
367+
368+
await checkStartTyping(tester, narrow);
369+
370+
connection.prepare(json: {});
371+
// While this state lives on [ServicesBinding], testWidgets resets it
372+
// for us when the test ends so we don't have to:
373+
// https://github.com/flutter/flutter/blob/c78c166e3ecf963ca29ed503e710fd3c71eda5c9/packages/flutter_test/lib/src/binding.dart#L1189
374+
// On iOS and Android, a transition to [hidden] is synthesized before
375+
// transitioning into [paused].
376+
WidgetsBinding.instance.handleAppLifecycleStateChanged(
377+
AppLifecycleState.hidden);
378+
await tester.pump(Duration.zero);
379+
checkTypingRequest(TypingOp.stop, narrow);
380+
381+
WidgetsBinding.instance.handleAppLifecycleStateChanged(
382+
AppLifecycleState.paused);
383+
await tester.pump(Duration.zero);
384+
check(connection.lastRequest).isNull();
385+
});
364386
});
365387

366388
group('message-send request response', () {

0 commit comments

Comments
 (0)