Skip to content

Commit 85e30ee

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 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]>
1 parent 24531db commit 85e30ee

File tree

2 files changed

+53
-1
lines changed

2 files changed

+53
-1
lines changed

lib/widgets/compose_box.dart

+31-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

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

338+
@override
339+
void didChangeAppLifecycleState(AppLifecycleState state) {
340+
switch (state) {
341+
case AppLifecycleState.hidden:
342+
case AppLifecycleState.paused:
343+
case AppLifecycleState.detached:
344+
// Transition to either [hidden] or [paused] signals that
345+
// > [the] application is not currently visible to the user, and not
346+
// > responding to user input.
347+
//
348+
// When transitioning to [detached], the compose box can't exist:
349+
// > The application defaults to this state before it initializes, and
350+
// > can be in this state (applicable on Android, iOS, and web) after
351+
// > all views have been detached.
352+
//
353+
// For all these states, we can conclude that the user is not
354+
// composing a message.
355+
final store = PerAccountStoreWidget.of(context);
356+
store.typingNotifier.stoppedComposing();
357+
case AppLifecycleState.inactive:
358+
// > At least one view of the application is visible, but none have
359+
// > input focus. The application is otherwise running normally.
360+
// For example, we expect this state when the user is selecting a file
361+
// to upload.
362+
case AppLifecycleState.resumed:
363+
}
364+
}
365+
336366
@override
337367
Widget build(BuildContext context) {
338368
ColorScheme colorScheme = Theme.of(context).colorScheme;

test/widgets/compose_box_test.dart

+22
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,28 @@ void main() {
344344
await tester.pump(store.typingNotifier.typingStoppedWaitPeriod);
345345
checkTypingRequest(TypingOp.stop, narrow);
346346
});
347+
348+
testWidgets('unfocusing app sends a "typing stopped" notice', (tester) async {
349+
await prepareComposeBox(tester, narrow: narrow);
350+
351+
await checkStartTyping(tester, narrow);
352+
353+
connection.prepare(json: {});
354+
// While this state lives on [ServicesBinding], testWidgets resets it
355+
// for us when the test ends so we don't have to:
356+
// https://github.com/flutter/flutter/blob/c78c166e3ecf963ca29ed503e710fd3c71eda5c9/packages/flutter_test/lib/src/binding.dart#L1189
357+
// On iOS and Android, a transition to [hidden] is synthesized before
358+
// transitioning into [paused].
359+
WidgetsBinding.instance.handleAppLifecycleStateChanged(
360+
AppLifecycleState.hidden);
361+
await tester.pump(Duration.zero);
362+
checkTypingRequest(TypingOp.stop, narrow);
363+
364+
WidgetsBinding.instance.handleAppLifecycleStateChanged(
365+
AppLifecycleState.paused);
366+
await tester.pump(Duration.zero);
367+
check(connection.lastRequest).isNull();
368+
});
347369
});
348370

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

0 commit comments

Comments
 (0)