|  | 
|  | 1 | +import 'package:collection/collection.dart'; | 
|  | 2 | +import 'package:flutter/material.dart'; | 
|  | 3 | + | 
|  | 4 | +import '../model/narrow.dart'; | 
|  | 5 | +import '../model/recent_dm_conversations.dart'; | 
|  | 6 | +import 'content.dart'; | 
|  | 7 | +import 'icons.dart'; | 
|  | 8 | +import 'message_list.dart'; | 
|  | 9 | +import 'page.dart'; | 
|  | 10 | +import 'store.dart'; | 
|  | 11 | +import 'text.dart'; | 
|  | 12 | + | 
|  | 13 | +class RecentDmConversationsPage extends StatefulWidget { | 
|  | 14 | +  const RecentDmConversationsPage({super.key}); | 
|  | 15 | + | 
|  | 16 | +  static Route<void> buildRoute({required BuildContext context}) { | 
|  | 17 | +    return MaterialAccountPageRoute(context: context, | 
|  | 18 | +      builder: (context) => const RecentDmConversationsPage()); | 
|  | 19 | +  } | 
|  | 20 | + | 
|  | 21 | +  @override | 
|  | 22 | +  State<RecentDmConversationsPage> createState() => _RecentDmConversationsPageState(); | 
|  | 23 | +} | 
|  | 24 | + | 
|  | 25 | +class _RecentDmConversationsPageState extends State<RecentDmConversationsPage> with PerAccountStoreAwareStateMixin<RecentDmConversationsPage> { | 
|  | 26 | +  RecentDmConversationsView? model; | 
|  | 27 | + | 
|  | 28 | +  @override | 
|  | 29 | +  void onNewStore() { | 
|  | 30 | +    model?.removeListener(_modelChanged); | 
|  | 31 | +    model = PerAccountStoreWidget.of(context).recentDmConversationsView | 
|  | 32 | +      ..addListener(_modelChanged); | 
|  | 33 | +  } | 
|  | 34 | + | 
|  | 35 | +  void _modelChanged() { | 
|  | 36 | +    setState(() { | 
|  | 37 | +      // The actual state lives in [model]. | 
|  | 38 | +      // This method was called because that just changed. | 
|  | 39 | +    }); | 
|  | 40 | +  } | 
|  | 41 | + | 
|  | 42 | +  Widget _buildItem(BuildContext context, DmNarrow narrow) { | 
|  | 43 | +    final colorScheme = Theme.of(context).colorScheme; | 
|  | 44 | + | 
|  | 45 | +    final allRecipientIds = narrow.allRecipientIds; | 
|  | 46 | +    final store = PerAccountStoreWidget.of(context); | 
|  | 47 | +    final selfUser = store.users[store.account.userId]!; | 
|  | 48 | +    final recipientsSansSelf = allRecipientIds | 
|  | 49 | +      .whereNot((id) => id == selfUser.userId) | 
|  | 50 | +      .map((id) => store.users[id]!) | 
|  | 51 | +      .toList(); | 
|  | 52 | + | 
|  | 53 | +    final Widget title; | 
|  | 54 | +    final Widget avatar; | 
|  | 55 | +    switch (recipientsSansSelf.length) { | 
|  | 56 | +      case 0: { | 
|  | 57 | +        title = Text(selfUser.fullName); | 
|  | 58 | +        avatar = Avatar(userId: selfUser.userId); | 
|  | 59 | +        break; | 
|  | 60 | +      } | 
|  | 61 | +      case 1: { | 
|  | 62 | +        final otherUser = recipientsSansSelf.single; | 
|  | 63 | +        title = Text(otherUser.fullName); | 
|  | 64 | +        avatar = Avatar(userId: otherUser.userId); | 
|  | 65 | +        break; | 
|  | 66 | +      } | 
|  | 67 | +      default: { | 
|  | 68 | +        // TODO(i18n): List formatting, like you can do in JavaScript: | 
|  | 69 | +        //   new Intl.ListFormat('ja').format(['Chris', 'Greg', 'Alya']) | 
|  | 70 | +        //   // 'Chris、Greg、Alya' | 
|  | 71 | +        title = Text(recipientsSansSelf.map((r) => r.fullName).join(', ')); | 
|  | 72 | +        avatar = ColoredBox(color: const Color(0x33808080), | 
|  | 73 | +          child: Center( | 
|  | 74 | +            child: Icon(ZulipIcons.group_dm, color: Colors.black.withOpacity(0.5)))); | 
|  | 75 | +        break; | 
|  | 76 | +      } | 
|  | 77 | +    } | 
|  | 78 | + | 
|  | 79 | +    return InkWell( | 
|  | 80 | +      onTap: () { | 
|  | 81 | +        Navigator.push(context, | 
|  | 82 | +          MessageListPage.buildRoute(context: context, narrow: narrow)); | 
|  | 83 | +      }, | 
|  | 84 | +      child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 48), | 
|  | 85 | +        child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ | 
|  | 86 | +          Padding(padding: const EdgeInsets.fromLTRB(12, 8, 0, 8), | 
|  | 87 | +            child: _AvatarWrapper(child: avatar)), | 
|  | 88 | +          const SizedBox(width: 8), | 
|  | 89 | +          Expanded(child: Padding( | 
|  | 90 | +            padding: const EdgeInsets.symmetric(vertical: 4), | 
|  | 91 | +            child: DefaultTextStyle( | 
|  | 92 | +              style: const TextStyle( | 
|  | 93 | +                fontFamily: 'Source Sans 3', | 
|  | 94 | +                fontSize: 17, | 
|  | 95 | +                height: (20 / 17), | 
|  | 96 | +                color: Color(0xFF222222), | 
|  | 97 | +              ).merge(weightVariableTextStyle(context)), | 
|  | 98 | +              maxLines: 2, | 
|  | 99 | +              overflow: TextOverflow.ellipsis, | 
|  | 100 | +              child: title), | 
|  | 101 | +          )), | 
|  | 102 | +          const SizedBox(width: 8), | 
|  | 103 | +          // TODO: Unread count | 
|  | 104 | +        ]))); | 
|  | 105 | +  } | 
|  | 106 | + | 
|  | 107 | +  @override | 
|  | 108 | +  Widget build(BuildContext context) { | 
|  | 109 | +    final sorted = model!.sorted; | 
|  | 110 | +    return Scaffold( | 
|  | 111 | +      appBar: AppBar(title: const Text('Direct messages')), | 
|  | 112 | +      body: ListView.builder( | 
|  | 113 | +        itemCount: sorted.length, | 
|  | 114 | +        itemBuilder: (context, index) => _buildItem(context, sorted[index]))); | 
|  | 115 | +  } | 
|  | 116 | +} | 
|  | 117 | + | 
|  | 118 | +/// Clips and sizes an avatar for a list item in [RecentDmConversationsPage]. | 
|  | 119 | +class _AvatarWrapper extends StatelessWidget { | 
|  | 120 | +  const _AvatarWrapper({required this.child}); | 
|  | 121 | + | 
|  | 122 | +  final Widget child; | 
|  | 123 | + | 
|  | 124 | +  @override | 
|  | 125 | +  Widget build(BuildContext context) { | 
|  | 126 | +    return SizedBox.square( | 
|  | 127 | +      dimension: 32, | 
|  | 128 | +      child: ClipRRect( | 
|  | 129 | +        borderRadius: const BorderRadius.all(Radius.circular(3)), | 
|  | 130 | +        clipBehavior: Clip.antiAlias, | 
|  | 131 | +        child: child)); | 
|  | 132 | +  } | 
|  | 133 | +} | 
0 commit comments