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 9, 2023
1 parent 7d1747e commit 0b71040
Show file tree
Hide file tree
Showing 5 changed files with 494 additions and 0 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
108 changes: 108 additions & 0 deletions lib/widgets/recent_dm_conversations.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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.
});
}

@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) => RecentDmConversationsItem(narrow: sorted[index])));
}
}

class RecentDmConversationsItem extends StatelessWidget {
const RecentDmConversationsItem({super.key, required this.narrow});

final DmNarrow narrow;

@override
Widget build(BuildContext context) {
final store = PerAccountStoreWidget.of(context);
final selfUser = store.users[store.account.userId]!;

final String title;
final Widget avatar;
switch (narrow.otherRecipientIds) {
case []:
title = selfUser.fullName;
avatar = AvatarImage(userId: selfUser.userId);
case [var otherUserId]:
final otherUser = store.users[otherUserId];
title = otherUser?.fullName ?? '(unknown user)';
avatar = AvatarImage(userId: otherUserId);
default:
// TODO(i18n): List formatting, like you can do in JavaScript:
// new Intl.ListFormat('ja').format(['Chris', 'Greg', 'Alya'])
// // 'Chris、Greg、Alya'
title = narrow.otherRecipientIds.map((id) => store.users[id]?.fullName ?? '(unknown user)').join(', ');
avatar = ColoredBox(color: const Color(0x33808080),
child: Center(
child: Icon(ZulipIcons.group_dm, color: Colors.black.withOpacity(0.5))));
}

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: AvatarShape(size: 32, borderRadius: 3, child: avatar)),
const SizedBox(width: 8),
Expanded(child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
style: const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 17,
height: (20 / 17),
color: Color(0xFF222222),
).merge(weightVariableTextStyle(context)),
maxLines: 2,
overflow: TextOverflow.ellipsis,
title))),
const SizedBox(width: 8),
// TODO(#253): Unread count
])));
}
}
39 changes: 39 additions & 0 deletions test/test_navigation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:flutter/widgets.dart';

// Inspired by test code in the Flutter tree:
// https://github.com/flutter/flutter/blob/53082f65b/packages/flutter/test/widgets/observer_tester.dart
// https://github.com/flutter/flutter/blob/53082f65b/packages/flutter/test/widgets/navigator_test.dart

/// A trivial observer for testing the navigator.
class TestNavigatorObserver extends NavigatorObserver {
void Function(Route<dynamic> route, Route<dynamic>? previousRoute)? onPushed;
void Function(Route<dynamic> route, Route<dynamic>? previousRoute)? onPopped;
void Function(Route<dynamic> route, Route<dynamic>? previousRoute)? onRemoved;
void Function(Route<dynamic>? route, Route<dynamic>? previousRoute)? onReplaced;
void Function(Route<dynamic> route, Route<dynamic>? previousRoute)? onStartUserGesture;

@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
onPushed?.call(route, previousRoute);
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
onPopped?.call(route, previousRoute);
}

@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
onRemoved?.call(route, previousRoute);
}

@override
void didReplace({ Route<dynamic>? oldRoute, Route<dynamic>? newRoute }) {
onReplaced?.call(newRoute, oldRoute);
}

@override
void didStartUserGesture(Route<dynamic> route, Route<dynamic>? previousRoute) {
onStartUserGesture?.call(route, previousRoute);
}
}
12 changes: 12 additions & 0 deletions test/widgets/content_checks.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import 'package:checks/checks.dart';
import 'package:flutter/widgets.dart';

import 'package:zulip/widgets/content.dart';

extension RealmContentNetworkImageChecks on Subject<RealmContentNetworkImage> {
Subject<Uri> get src => has((i) => i.src, 'src');
// TODO others
}

extension AvatarImageChecks on Subject<AvatarImage> {
Subject<int> get userId => has((i) => i.userId, 'userId');
}

extension AvatarShapeChecks on Subject<AvatarShape> {
Subject<double> get size => has((i) => i.size, 'size');
Subject<double> get borderRadius => has((i) => i.borderRadius, 'borderRadius');
Subject<Widget> get child => has((i) => i.child, 'child');
}
Loading

0 comments on commit 0b71040

Please sign in to comment.