@@ -2,6 +2,7 @@ import 'dart:math';
2
2
3
3
import 'package:collection/collection.dart' ;
4
4
import 'package:flutter/material.dart' ;
5
+ import 'package:flutter_color_models/flutter_color_models.dart' ;
5
6
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
6
7
import 'package:intl/intl.dart' ;
7
8
@@ -11,6 +12,7 @@ import '../api/route/messages.dart';
11
12
import '../model/message_list.dart' ;
12
13
import '../model/narrow.dart' ;
13
14
import '../model/store.dart' ;
15
+ import '../model/typing_status.dart' ;
14
16
import 'action_sheet.dart' ;
15
17
import 'compose_box.dart' ;
16
18
import 'content.dart' ;
@@ -326,17 +328,19 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
326
328
final valueKey = key as ValueKey <int >;
327
329
final index = model! .findItemWithMessageId (valueKey.value);
328
330
if (index == - 1 ) return null ;
329
- return length - 1 - (index - 2 );
331
+ return length - 1 - (index - 3 );
330
332
},
331
- childCount: length + 2 ,
333
+ childCount: length + 3 ,
332
334
(context, i) {
333
335
// To reinforce that the end of the feed has been reached:
334
336
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Mark-as-read/near/1680603
335
337
if (i == 0 ) return const SizedBox (height: 36 );
336
338
337
339
if (i == 1 ) return MarkAsReadWidget (narrow: widget.narrow);
338
340
339
- final data = model! .items[length - 1 - (i - 2 )];
341
+ if (i == 2 ) return TypingStatusWidget (narrow: widget.narrow);
342
+
343
+ final data = model! .items[length - 1 - (i - 3 )];
340
344
return _buildItem (data, i);
341
345
}));
342
346
@@ -510,6 +514,68 @@ class MarkAsReadWidget extends StatelessWidget {
510
514
}
511
515
}
512
516
517
+ class _TypingStatusState extends State <TypingStatusWidget > with PerAccountStoreAwareStateMixin <TypingStatusWidget > {
518
+ TypingStatus ? model;
519
+
520
+ @override
521
+ void onNewStore () {
522
+ model? .removeListener (_modelChanged);
523
+ model = PerAccountStoreWidget .of (context).typingStatus
524
+ ..addListener (_modelChanged);
525
+ }
526
+
527
+ @override
528
+ void dispose () {
529
+ model? .removeListener (_modelChanged);
530
+ super .dispose ();
531
+ }
532
+
533
+ void _modelChanged () {
534
+ setState (() {
535
+ // The actual state lives in [model].
536
+ // This method was called because that just changed.
537
+ });
538
+ }
539
+
540
+ @override
541
+ Widget build (BuildContext context) {
542
+ final store = PerAccountStoreWidget .of (context);
543
+ final narrow = widget.narrow;
544
+ const placeholder = SizedBox (height: 8 );
545
+ if (narrow is ! SendableNarrow ) return placeholder;
546
+
547
+ final typistNames = model! .getTypistIdsInNarrow (narrow)
548
+ .where ((id) => id != store.selfUserId)
549
+ .map ((id) => store.users[id]! .fullName)
550
+ .toList ();
551
+ if (typistNames.isEmpty) return placeholder;
552
+
553
+ final localization = ZulipLocalizations .of (context);
554
+ final String text = switch (typistNames.length) {
555
+ 1 => localization.onePersonTyping (typistNames[0 ]),
556
+ 2 => localization.twoPeopleTyping (typistNames[0 ], typistNames[1 ]),
557
+ _ => localization.manyPeopleTyping,
558
+ };
559
+
560
+ return Padding (
561
+ padding: const EdgeInsets .only (left: 16 , top: 2 ),
562
+ child: Text (text,
563
+ textAlign: TextAlign .left,
564
+ style: const TextStyle (
565
+ color: HslColor (0 , 0 , 53 ), fontStyle: FontStyle .italic)),
566
+ );
567
+ }
568
+ }
569
+
570
+ class TypingStatusWidget extends StatefulWidget {
571
+ const TypingStatusWidget ({super .key, required this .narrow});
572
+
573
+ final Narrow narrow;
574
+
575
+ @override
576
+ State <StatefulWidget > createState () => _TypingStatusState ();
577
+ }
578
+
513
579
class RecipientHeader extends StatelessWidget {
514
580
const RecipientHeader ({super .key, required this .message, required this .narrow});
515
581
0 commit comments