Skip to content

Commit 05a7975

Browse files
committed
wip compose_box: Integrate ComposeBox.
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 54bcbfe commit 05a7975

File tree

5 files changed

+172
-5
lines changed

5 files changed

+172
-5
lines changed

lib/widgets/compose_box.dart

+57-5
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ class _StreamContentInput extends StatefulWidget {
334334
class _StreamContentInputState extends State<_StreamContentInput> {
335335
late String _topicTextNormalized;
336336

337+
TopicNarrow get typingNarrow =>
338+
TopicNarrow(widget.narrow.streamId, _topicTextNormalized);
339+
337340
void _topicChanged() {
338341
setState(() {
339342
_topicTextNormalized = widget.topicController.textNormalized;
@@ -345,6 +348,8 @@ class _StreamContentInputState extends State<_StreamContentInput> {
345348
super.initState();
346349
_topicTextNormalized = widget.topicController.textNormalized;
347350
widget.topicController.addListener(_topicChanged);
351+
widget.controller.addListener(_contentChanged);
352+
widget.focusNode.addListener(_focusChanged);
348353
}
349354

350355
@override
@@ -359,9 +364,24 @@ class _StreamContentInputState extends State<_StreamContentInput> {
359364
@override
360365
void dispose() {
361366
widget.topicController.removeListener(_topicChanged);
367+
widget.controller.removeListener(_contentChanged);
368+
widget.focusNode.removeListener(_focusChanged);
362369
super.dispose();
363370
}
364371

372+
void _contentChanged() {
373+
final store = PerAccountStoreWidget.of(context);
374+
store.typingNotifier.handleTypingStatusUpdate(store,
375+
widget.controller.text.isNotEmpty ? typingNarrow : null);
376+
}
377+
378+
void _focusChanged() {
379+
if (widget.focusNode.hasFocus) return;
380+
381+
final store = PerAccountStoreWidget.of(context);
382+
store.typingNotifier.handleTypingStatusUpdate(store, null);
383+
}
384+
365385
@override
366386
Widget build(BuildContext context) {
367387
final store = PerAccountStoreWidget.of(context);
@@ -408,7 +428,7 @@ class _TopicInput extends StatelessWidget {
408428
}
409429
}
410430

411-
class _FixedDestinationContentInput extends StatelessWidget {
431+
class _FixedDestinationContentInput extends StatefulWidget {
412432
const _FixedDestinationContentInput({
413433
required this.narrow,
414434
required this.controller,
@@ -419,9 +439,41 @@ class _FixedDestinationContentInput extends StatelessWidget {
419439
final ComposeContentController controller;
420440
final FocusNode focusNode;
421441

442+
@override
443+
State<_FixedDestinationContentInput> createState() => _FixedDestinationContentInputState();
444+
}
445+
446+
class _FixedDestinationContentInputState extends State<_FixedDestinationContentInput> {
447+
@override
448+
void initState() {
449+
super.initState();
450+
widget.controller.addListener(_contentChanged);
451+
widget.focusNode.addListener(_focusChanged);
452+
}
453+
454+
@override
455+
void dispose() {
456+
widget.controller.removeListener(_contentChanged);
457+
widget.focusNode.removeListener(_focusChanged);
458+
super.dispose();
459+
}
460+
461+
void _contentChanged() {
462+
final store = PerAccountStoreWidget.of(context);
463+
store.typingNotifier.handleTypingStatusUpdate(store,
464+
widget.controller.text.isNotEmpty ? widget.narrow : null);
465+
}
466+
467+
void _focusChanged() {
468+
if (widget.focusNode.hasFocus) return;
469+
470+
final store = PerAccountStoreWidget.of(context);
471+
store.typingNotifier.handleTypingStatusUpdate(store, null);
472+
}
473+
422474
String _hintText(BuildContext context) {
423475
final zulipLocalizations = ZulipLocalizations.of(context);
424-
switch (narrow) {
476+
switch (widget.narrow) {
425477
case TopicNarrow(:final streamId, :final topic):
426478
final store = PerAccountStoreWidget.of(context);
427479
final streamName = store.streams[streamId]?.name
@@ -445,9 +497,9 @@ class _FixedDestinationContentInput extends StatelessWidget {
445497
@override
446498
Widget build(BuildContext context) {
447499
return _ContentInput(
448-
narrow: narrow,
449-
controller: controller,
450-
focusNode: focusNode,
500+
narrow: widget.narrow,
501+
controller: widget.controller,
502+
focusNode: widget.focusNode,
451503
hintText: _hintText(context));
452504
}
453505
}

test/widgets/action_sheet_test.dart

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:zulip/model/internal_link.dart';
1414
import 'package:zulip/model/localizations.dart';
1515
import 'package:zulip/model/narrow.dart';
1616
import 'package:zulip/model/store.dart';
17+
import 'package:zulip/model/typing_status.dart';
1718
import 'package:zulip/widgets/compose_box.dart';
1819
import 'package:zulip/widgets/content.dart';
1920
import 'package:zulip/widgets/icons.dart';
@@ -259,6 +260,8 @@ void main() {
259260
await tester.ensureVisible(find.byIcon(Icons.format_quote_outlined, skipOffstage: false));
260261
final quoteAndReplyButton = findQuoteAndReplyButton(tester);
261262
check(quoteAndReplyButton).isNotNull();
263+
DebugTypingNotifier.debugEnable = false;
264+
addTearDown(DebugTypingNotifier.debugReset);
262265
await tester.tap(find.byWidget(quoteAndReplyButton!));
263266
await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
264267
}

test/widgets/autocomplete_test.dart

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:zulip/model/compose.dart';
88
import 'package:zulip/model/localizations.dart';
99
import 'package:zulip/model/narrow.dart';
1010
import 'package:zulip/model/store.dart';
11+
import 'package:zulip/model/typing_status.dart';
1112
import 'package:zulip/widgets/message_list.dart';
1213

1314
import '../api/fake_api.dart';
@@ -129,6 +130,9 @@ void main() {
129130
final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]);
130131
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
131132

133+
DebugTypingNotifier.debugEnable = false;
134+
addTearDown(DebugTypingNotifier.debugReset);
135+
132136
// Options are filtered correctly for query
133137
// TODO(#226): Remove this extra edit when this bug is fixed.
134138
await tester.enterText(composeInputFinder, 'hello @user ');
@@ -180,6 +184,9 @@ void main() {
180184
final topicInputFinder = await setupToTopicInput(tester, topics: [topic1, topic2, topic3]);
181185
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
182186

187+
DebugTypingNotifier.debugEnable = false;
188+
addTearDown(DebugTypingNotifier.debugReset);
189+
183190
// Options are filtered correctly for query
184191
// TODO(#226): Remove this extra edit when this bug is fixed.
185192
await tester.enterText(topicInputFinder, 'Topic');

test/widgets/compose_box_test.dart

+102
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import 'package:zulip/api/route/messages.dart';
1313
import 'package:zulip/model/localizations.dart';
1414
import 'package:zulip/model/narrow.dart';
1515
import 'package:zulip/model/store.dart';
16+
import 'package:zulip/model/typing_status.dart';
1617
import 'package:zulip/widgets/compose_box.dart';
1718

1819
import '../api/fake_api.dart';
1920
import '../example_data.dart' as eg;
2021
import '../flutter_checks.dart';
2122
import '../model/binding.dart';
2223
import '../model/test_store.dart';
24+
import '../model/typing_status_test.dart';
2325
import '../stdlib_checks.dart';
2426
import 'dialog_checks.dart';
2527
import 'test_app.dart';
@@ -155,6 +157,97 @@ void main() {
155157
});
156158
});
157159

160+
group('ComposeBox typing notification', () {
161+
void checkTypingRequest(TypingOp op, SendableNarrow narrow) =>
162+
checkSetTypingStatusRequests(connection, [(op, narrow)]);
163+
164+
final testCases = [
165+
const TopicNarrow(123, 'some topic'),
166+
DmNarrow.withUsers([eg.otherUser.userId], selfUserId: eg.selfUser.userId),
167+
];
168+
169+
final contentInputFinder = find.byWidgetPredicate(
170+
(widget) => widget is TextField && widget.controller is ComposeContentController);
171+
final topicInputFinder = find.byWidgetPredicate(
172+
(widget) => widget is TextField && widget.controller is ComposeTopicController);
173+
174+
for (final narrow in testCases) {
175+
testWidgets('smoke $narrow', (tester) async {
176+
await prepareComposeBox(tester, narrow: narrow);
177+
178+
connection.prepare(json: {});
179+
await tester.enterText(contentInputFinder, 'hello world');
180+
checkTypingRequest(TypingOp.start, narrow);
181+
182+
connection.prepare(json: {});
183+
tester.pumpAndSettle(store.typingNotifier.typingStoppedWaitPeriod);
184+
checkTypingRequest(TypingOp.stop, narrow);
185+
});
186+
187+
testWidgets('$narrow: clearing text', (tester) async {
188+
await prepareComposeBox(tester, narrow: narrow);
189+
190+
connection.prepare(json: {});
191+
await tester.enterText(contentInputFinder, 'hello world');
192+
checkTypingRequest(TypingOp.start, narrow);
193+
194+
connection.prepare(json: {});
195+
await tester.enterText(contentInputFinder, '');
196+
checkTypingRequest(TypingOp.stop, narrow);
197+
});
198+
199+
testWidgets('$narrow: unfocusing content input stops previous typing notification', (tester) async {
200+
// TODO
201+
// await prepareComposeBox(tester, narrow: narrow);
202+
203+
// connection.prepare(json: {});
204+
// await tester.enterText(contentInputFinder, 'hello world');
205+
// checkTypingRequest(TypingOp.start, narrow);
206+
207+
// connection.prepare(json: {});
208+
// await tester.press(find.widgetWithIcon(IconButton, Icons.attach_file));
209+
// await tester.pump();
210+
// checkTypingRequest(TypingOp.stop, narrow);
211+
});
212+
}
213+
214+
testWidgets('smoke ChannelNarrow', (tester) async {
215+
const narrow = ChannelNarrow(123);
216+
await prepareComposeBox(tester, narrow: narrow);
217+
218+
await tester.enterText(topicInputFinder, 'test topic');
219+
// Clean an irrelevant topic request.
220+
check(connection.takePreviousRequests()).single
221+
..method.equals('GET')
222+
..url.path.equals('/api/v1/users/me/123/topics');
223+
224+
connection.prepare(json: {});
225+
final typingNarrow = TopicNarrow(narrow.streamId, 'test topic');
226+
await tester.enterText(contentInputFinder, 'hello world');
227+
checkTypingRequest(TypingOp.start, typingNarrow);
228+
229+
connection.prepare(json: {});
230+
tester.pumpAndSettle(store.typingNotifier.typingStoppedWaitPeriod);
231+
checkTypingRequest(TypingOp.stop, typingNarrow);
232+
});
233+
234+
testWidgets('unfocusing content input stops previous typing notification', (tester) async {
235+
// TODO
236+
// const narrow = ChannelNarrow(123);
237+
// await prepareComposeBox(tester, narrow: narrow);
238+
239+
// await tester.enterText(topicInputFinder, 'topic');
240+
// // Clean an irrelevant topic request.
241+
// check(connection.takePreviousRequests()).single
242+
// ..method.equals('GET')
243+
// ..url.path.equals('/api/v1/users/me/123/topics');
244+
245+
// connection.prepare(json: {});
246+
// await tester.enterText(contentInputFinder, 'hello world');
247+
// checkTypingRequest(TypingOp.start, TopicNarrow(narrow.streamId, 'topic'));
248+
});
249+
});
250+
158251
group('ComposeBox textCapitalization', () {
159252
void checkComposeBoxTextFields(WidgetTester tester, {
160253
required GlobalKey<ComposeBoxController> controllerKey,
@@ -198,6 +291,9 @@ void main() {
198291
Future<void> setupAndTapSend(WidgetTester tester, {
199292
required void Function(int messageId) prepareResponse,
200293
}) async {
294+
DebugTypingNotifier.debugEnable = false;
295+
addTearDown(DebugTypingNotifier.debugReset);
296+
201297
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
202298
await prepareComposeBox(tester, narrow: const TopicNarrow(123, 'some topic'));
203299

@@ -263,6 +359,9 @@ void main() {
263359

264360
group('attach from media library', () {
265361
testWidgets('success', (tester) async {
362+
DebugTypingNotifier.debugEnable = false;
363+
addTearDown(DebugTypingNotifier.debugReset);
364+
266365
final controllerKey = await prepareComposeBox(tester, narrow: ChannelNarrow(eg.stream().streamId));
267366
final composeBoxController = controllerKey.currentState!;
268367

@@ -319,6 +418,9 @@ void main() {
319418

320419
group('attach from camera', () {
321420
testWidgets('success', (tester) async {
421+
DebugTypingNotifier.debugEnable = false;
422+
addTearDown(DebugTypingNotifier.debugReset);
423+
322424
final controllerKey = await prepareComposeBox(tester, narrow: ChannelNarrow(eg.stream().streamId));
323425
final composeBoxController = controllerKey.currentState!;
324426

test/widgets/message_list_test.dart

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:zulip/api/route/messages.dart';
1414
import 'package:zulip/model/localizations.dart';
1515
import 'package:zulip/model/narrow.dart';
1616
import 'package:zulip/model/store.dart';
17+
import 'package:zulip/model/typing_status.dart';
1718
import 'package:zulip/widgets/autocomplete.dart';
1819
import 'package:zulip/widgets/content.dart';
1920
import 'package:zulip/widgets/emoji_reaction.dart';
@@ -51,12 +52,14 @@ void main() {
5152
List<Subscription>? subscriptions,
5253
UnreadMessagesSnapshot? unreadMsgs,
5354
}) async {
55+
addTearDown(DebugTypingNotifier.debugReset);
5456
addTearDown(testBinding.reset);
5557
streams ??= subscriptions ??= [eg.subscription(eg.stream(streamId: eg.defaultStreamMessageStreamId))];
5658
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
5759
streams: streams, subscriptions: subscriptions, unreadMsgs: unreadMsgs));
5860
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
5961
connection = store.connection as FakeApiConnection;
62+
DebugTypingNotifier.debugEnable = false;
6063

6164
// prepare message list data
6265
await store.addUser(eg.selfUser);

0 commit comments

Comments
 (0)