@@ -2,6 +2,7 @@ import 'dart:math';
22
33import 'package:collection/collection.dart' ;
44import 'package:flutter/material.dart' ;
5+ import 'package:flutter_color_models/flutter_color_models.dart' ;
56import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
67import 'package:intl/intl.dart' ;
78
@@ -11,6 +12,7 @@ import '../api/route/messages.dart';
1112import '../model/message_list.dart' ;
1213import '../model/narrow.dart' ;
1314import '../model/store.dart' ;
15+ import '../model/typing_status.dart' ;
1416import 'action_sheet.dart' ;
1517import 'compose_box.dart' ;
1618import 'content.dart' ;
@@ -326,17 +328,19 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
326328 final valueKey = key as ValueKey <int >;
327329 final index = model! .findItemWithMessageId (valueKey.value);
328330 if (index == - 1 ) return null ;
329- return length - 1 - (index - 2 );
331+ return length - 1 - (index - 3 );
330332 },
331- childCount: length + 2 ,
333+ childCount: length + 3 ,
332334 (context, i) {
333335 // To reinforce that the end of the feed has been reached:
334336 // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Mark-as-read/near/1680603
335337 if (i == 0 ) return const SizedBox (height: 36 );
336338
337339 if (i == 1 ) return MarkAsReadWidget (narrow: widget.narrow);
338340
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 )];
340344 return _buildItem (data, i);
341345 }));
342346
@@ -510,6 +514,68 @@ class MarkAsReadWidget extends StatelessWidget {
510514 }
511515}
512516
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+
513579class RecipientHeader extends StatelessWidget {
514580 const RecipientHeader ({super .key, required this .message, required this .narrow});
515581
0 commit comments