Skip to content

Commit

Permalink
RecentDmConversationsPage: Add
Browse files Browse the repository at this point in the history
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
  • Loading branch information
chrisbobbe committed Aug 2, 2023
1 parent a1f931a commit 257af55
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 8 deletions.
6 changes: 6 additions & 0 deletions lib/widgets/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '../model/narrow.dart';
import 'about_zulip.dart';
import 'login.dart';
import 'message_list.dart';
import 'recent_dm_conversations.dart';
import 'store.dart';

class ZulipApp extends StatelessWidget {
Expand Down Expand Up @@ -152,6 +153,11 @@ class HomePage extends StatelessWidget {
MessageListPage.buildRoute(context: context,
narrow: const AllMessagesNarrow())),
child: const Text("All messages")),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.push(context,
RecentDmConversationsPage.buildRoute(context: context)),
child: const Text("Direct messages")),
if (testStreamId != null) ...[
const SizedBox(height: 16),
ElevatedButton(
Expand Down
25 changes: 17 additions & 8 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -812,16 +812,20 @@ class RealmContentNetworkImage extends StatelessWidget {
}
}

/// A rounded square with size [size] showing a user's avatar.
/// A square showing a user's avatar.
///
/// To set the size and clip the corners to be round, pass [size].
/// If [size] is not passed, the caller takes responsibility
/// for doing that with its own square wrapper.
class Avatar extends StatelessWidget {
const Avatar({
super.key,
required this.userId,
required this.size,
this.size,
});

final int userId;
final double size;
final double? size;

@override
Widget build(BuildContext context) {
Expand All @@ -832,16 +836,21 @@ class Avatar extends StatelessWidget {
null => null, // TODO handle computing gravatars
var avatarUrl => resolveUrl(avatarUrl, store.account),
};
final avatar = (resolvedUrl == null)

Widget current = (resolvedUrl == null)
? const SizedBox.shrink()
: RealmContentNetworkImage(resolvedUrl, filterQuality: FilterQuality.medium);

return SizedBox.square(
dimension: size,
child: ClipRRect(
if (size != null) {
current = ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)), // TODO vary with [size]?
clipBehavior: Clip.antiAlias,
child: avatar));
child: current);
}

return SizedBox.square(
dimension: size, // may be null
child: current);
}
}

Expand Down
130 changes: 130 additions & 0 deletions lib/widgets/recent_dm_conversations.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

import '../model/narrow.dart';
import '../model/recent_dm_conversations.dart';
import 'content.dart';
import 'icons.dart';
import 'message_list.dart';
import 'page.dart';
import 'store.dart';
import 'text.dart';

class RecentDmConversationsPage extends StatefulWidget {
const RecentDmConversationsPage({super.key});

static Route<void> buildRoute({required BuildContext context}) {
return MaterialAccountPageRoute(context: context,
builder: (context) => const RecentDmConversationsPage());
}

@override
State<RecentDmConversationsPage> createState() => _RecentDmConversationsPageState();
}

class _RecentDmConversationsPageState extends State<RecentDmConversationsPage> with PerAccountStoreAwareStateMixin<RecentDmConversationsPage> {
RecentDmConversationsView? model;

@override
void onNewStore() {
model?.removeListener(_modelChanged);
model = PerAccountStoreWidget.of(context).recentDmConversationsView
..addListener(_modelChanged);
}

void _modelChanged() {
setState(() {
// The actual state lives in [model].
// This method was called because that just changed.
});
}

Widget _buildItem(BuildContext context, DmNarrow narrow) {
final colorScheme = Theme.of(context).colorScheme;

final allRecipientIds = narrow.allRecipientIds;
final store = PerAccountStoreWidget.of(context);
final selfUser = store.users[store.account.userId]!;
final recipientsSansSelf = allRecipientIds
.whereNot((id) => id == selfUser.userId)
.map((id) => store.users[id]!)
.toList();

final Widget title;
final Widget avatar;
switch (recipientsSansSelf.length) {
case 0: {
title = Text(selfUser.fullName);
avatar = Avatar(userId: selfUser.userId);
break;
}
case 1: {
final otherUser = recipientsSansSelf.single;
title = Text(otherUser.fullName);
avatar = Avatar(userId: otherUser.userId);
break;
}
default: {
// TODO(i18n): List formatting, like you can do in JavaScript:
// new Intl.ListFormat('ja').format(['Chris', 'Greg', 'Alya'])
// // 'Chris、Greg、Alya'
title = Text(recipientsSansSelf.map((r) => r.fullName).join(', '));
avatar = ColoredBox(color: const Color(0xFF808080).withOpacity(0.2),
child: Center(
child: Icon(ZulipIcons.group_dm, color: Colors.black.withOpacity(0.5))));
break;
}
}

return InkWell(
onTap: () {
Navigator.push(context,
MessageListPage.buildRoute(context: context, narrow: narrow));
},
child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 48),
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Padding(padding: const EdgeInsets.fromLTRB(12, 8, 0, 8),
child: _AvatarWrapper(child: avatar)),
const SizedBox(width: 8),
Expanded(child: DefaultTextStyle(
style: const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 17,
height: (20 / 17),
color: Color(0xFF222222),
).merge(weightVariableTextStyle(context)),
maxLines: 2,
overflow: TextOverflow.ellipsis,
child: title)),
const SizedBox(width: 8),
// TODO: Unread count
])));
}

@override
Widget build(BuildContext context) {
final sorted = model!.sorted;
return Scaffold(
appBar: AppBar(title: const Text('Direct messages')),
body: ListView.builder(
itemCount: sorted.length,
itemBuilder: (context, index) => _buildItem(context, sorted[index])));
}
}

/// Clips and sizes an avatar for a list item in [RecentDmConversationsPage].
class _AvatarWrapper extends StatelessWidget {
const _AvatarWrapper({required this.child});

final Widget child;

@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 32,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(3)),
clipBehavior: Clip.antiAlias,
child: child));
}
}

0 comments on commit 257af55

Please sign in to comment.