Skip to content

Commit 257af55

Browse files
committed
RecentDmConversationsPage: Add
The screen's content area (so, the list of conversations, but not the app bar at the top) is built against Vlad's design: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20DM-conversation.20list/near/1594654 except that some features that appear in that design are left unimplemented for now, since we don't have data structures for them yet: - unread counts - user presence Fixes: #119
1 parent a1f931a commit 257af55

File tree

3 files changed

+153
-8
lines changed

3 files changed

+153
-8
lines changed

lib/widgets/app.dart

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '../model/narrow.dart';
44
import 'about_zulip.dart';
55
import 'login.dart';
66
import 'message_list.dart';
7+
import 'recent_dm_conversations.dart';
78
import 'store.dart';
89

910
class ZulipApp extends StatelessWidget {
@@ -152,6 +153,11 @@ class HomePage extends StatelessWidget {
152153
MessageListPage.buildRoute(context: context,
153154
narrow: const AllMessagesNarrow())),
154155
child: const Text("All messages")),
156+
const SizedBox(height: 16),
157+
ElevatedButton(
158+
onPressed: () => Navigator.push(context,
159+
RecentDmConversationsPage.buildRoute(context: context)),
160+
child: const Text("Direct messages")),
155161
if (testStreamId != null) ...[
156162
const SizedBox(height: 16),
157163
ElevatedButton(

lib/widgets/content.dart

+17-8
Original file line numberDiff line numberDiff line change
@@ -812,16 +812,20 @@ class RealmContentNetworkImage extends StatelessWidget {
812812
}
813813
}
814814

815-
/// A rounded square with size [size] showing a user's avatar.
815+
/// A square showing a user's avatar.
816+
///
817+
/// To set the size and clip the corners to be round, pass [size].
818+
/// If [size] is not passed, the caller takes responsibility
819+
/// for doing that with its own square wrapper.
816820
class Avatar extends StatelessWidget {
817821
const Avatar({
818822
super.key,
819823
required this.userId,
820-
required this.size,
824+
this.size,
821825
});
822826

823827
final int userId;
824-
final double size;
828+
final double? size;
825829

826830
@override
827831
Widget build(BuildContext context) {
@@ -832,16 +836,21 @@ class Avatar extends StatelessWidget {
832836
null => null, // TODO handle computing gravatars
833837
var avatarUrl => resolveUrl(avatarUrl, store.account),
834838
};
835-
final avatar = (resolvedUrl == null)
839+
840+
Widget current = (resolvedUrl == null)
836841
? const SizedBox.shrink()
837842
: RealmContentNetworkImage(resolvedUrl, filterQuality: FilterQuality.medium);
838843

839-
return SizedBox.square(
840-
dimension: size,
841-
child: ClipRRect(
844+
if (size != null) {
845+
current = ClipRRect(
842846
borderRadius: const BorderRadius.all(Radius.circular(4)), // TODO vary with [size]?
843847
clipBehavior: Clip.antiAlias,
844-
child: avatar));
848+
child: current);
849+
}
850+
851+
return SizedBox.square(
852+
dimension: size, // may be null
853+
child: current);
845854
}
846855
}
847856

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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(0xFF808080).withOpacity(0.2),
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: DefaultTextStyle(
90+
style: const TextStyle(
91+
fontFamily: 'Source Sans 3',
92+
fontSize: 17,
93+
height: (20 / 17),
94+
color: Color(0xFF222222),
95+
).merge(weightVariableTextStyle(context)),
96+
maxLines: 2,
97+
overflow: TextOverflow.ellipsis,
98+
child: title)),
99+
const SizedBox(width: 8),
100+
// TODO: Unread count
101+
])));
102+
}
103+
104+
@override
105+
Widget build(BuildContext context) {
106+
final sorted = model!.sorted;
107+
return Scaffold(
108+
appBar: AppBar(title: const Text('Direct messages')),
109+
body: ListView.builder(
110+
itemCount: sorted.length,
111+
itemBuilder: (context, index) => _buildItem(context, sorted[index])));
112+
}
113+
}
114+
115+
/// Clips and sizes an avatar for a list item in [RecentDmConversationsPage].
116+
class _AvatarWrapper extends StatelessWidget {
117+
const _AvatarWrapper({required this.child});
118+
119+
final Widget child;
120+
121+
@override
122+
Widget build(BuildContext context) {
123+
return SizedBox.square(
124+
dimension: 32,
125+
child: ClipRRect(
126+
borderRadius: const BorderRadius.all(Radius.circular(3)),
127+
clipBehavior: Clip.antiAlias,
128+
child: child));
129+
}
130+
}

0 commit comments

Comments
 (0)