Skip to content

Commit 4de84ae

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: zulip#119
1 parent 71af057 commit 4de84ae

File tree

3 files changed

+128
-1
lines changed

3 files changed

+128
-1
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

+3-1
Original file line numberDiff line numberDiff line change
@@ -818,10 +818,12 @@ class Avatar extends StatelessWidget {
818818
super.key,
819819
required this.userId,
820820
required this.size,
821+
this.borderRadius = 4, // TODO vary default value with [size]?
821822
});
822823

823824
final int userId;
824825
final double size;
826+
final double borderRadius;
825827

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

0 commit comments

Comments
 (0)