Skip to content

Commit

Permalink
subscription_list: Add new SubscriptionListPage
Browse files Browse the repository at this point in the history
Fixes zulip#187.
  • Loading branch information
sirpengi committed Nov 20, 2023
1 parent 81b7a51 commit ccea15d
Show file tree
Hide file tree
Showing 3 changed files with 403 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 @@ -14,6 +14,7 @@ import 'message_list.dart';
import 'page.dart';
import 'recent_dm_conversations.dart';
import 'store.dart';
import 'subscription_list.dart';

class ZulipApp extends StatelessWidget {
const ZulipApp({super.key, this.navigatorObservers});
Expand Down Expand Up @@ -260,6 +261,11 @@ class HomePage extends StatelessWidget {
InboxPage.buildRoute(context: context)),
child: const Text("Inbox")), // TODO(i18n)
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.push(context,
SubscriptionListPage.buildRoute(context: context)),
child: const Text("Subscribed streams")),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.push(context,
RecentDmConversationsPage.buildRoute(context: context)),
Expand Down
251 changes: 251 additions & 0 deletions lib/widgets/subscription_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

import '../api/model/model.dart';
import '../model/narrow.dart';
import '../model/unreads.dart';
import 'icons.dart';
import 'message_list.dart';
import 'page.dart';
import 'store.dart';
import 'text.dart';
import 'unread_count_badge.dart';

/// Scrollable listing of subscribed streams.
class SubscriptionListPage extends StatefulWidget {
const SubscriptionListPage({super.key});

static Route<void> buildRoute({required BuildContext context}) {
return MaterialAccountWidgetRoute(context: context,
page: const SubscriptionListPage());
}

@override
State<SubscriptionListPage> createState() => _SubscriptionListPageState();
}

class _SubscriptionListPageState extends State<SubscriptionListPage> with PerAccountStoreAwareStateMixin<SubscriptionListPage> {
Map<int, Subscription>? subscriptions;
Unreads? unreadsModel;

@override
void onNewStore() {
final store = PerAccountStoreWidget.of(context);
subscriptions = store.subscriptions;

unreadsModel?.removeListener(_modelChanged);
unreadsModel = store.unreads
..addListener(_modelChanged);
}

@override
void dispose() {
unreadsModel?.removeListener(_modelChanged);
super.dispose();
}

void _modelChanged() {
setState(() {
// The actual state lives in [subscriptions] and [unreadsModel].
// This method was called because one of those just changed.
});
}

@override
Widget build(BuildContext context) {
// This is an initial version with "Pinned" and "Unpinned"
// sections following behavior in mobile. Recalculating
// groups and sorting on every `build` here: it performs well
// enough and will be replaced with a different behavior:
// TODO: Implement new grouping behavior and design, see discussion at:
// https://chat.zulip.org/#narrow/stream/101-design/topic/UI.20redesign.3A.20left.20sidebar/near/1540147
//
// TODO(intl): localize strings on page
// Strings here left unlocalized as they likely will not
// exist in the settled design.
final List<Subscription> pinned = [];
final List<Subscription> unpinned = [];
for (final subscription in subscriptions!.values) {
switch (subscription) {
case Subscription(pinToTop: true):
pinned.add(subscription);
default:
unpinned.add(subscription);
}
}
// TODO(intl): add locale-aware sorting
pinned.sortBy((subscription) => subscription.name);
unpinned.sortBy((subscription) => subscription.name);
return Scaffold(
appBar: AppBar(title: const Text("Streams")),
body: Builder(
builder: (BuildContext context) => Center(
child: CustomScrollView(
slivers: [
if (pinned.isEmpty && unpinned.isEmpty)
const _NoSubscriptionsItem(),
if (pinned.isNotEmpty) ...[
_SubscriptionListHeader(context: context, label: "Pinned"),
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: pinned),
],
if (unpinned.isNotEmpty) ...[
_SubscriptionListHeader(context: context, label: "Unpinned"),
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: unpinned),
],

// TODO(#188): add button to "All Streams" page with ability to subscribe

// This ensures last item in scrollable can settle in an unobstructed area.
const SliverSafeArea(sliver: SliverToBoxAdapter(child: SizedBox.shrink())),
]))));
}
}

class _NoSubscriptionsItem extends StatelessWidget {
const _NoSubscriptionsItem();

@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10),
child: Text("No streams found",
textAlign: TextAlign.center,
style: TextStyle(
color: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(),
fontFamily: 'Source Sans 3',
fontSize: 18,
height: (20 / 18),
).merge(weightVariableTextStyle(context)))));
}
}

class _SubscriptionListHeader extends StatelessWidget {
const _SubscriptionListHeader({
required this.context,
required this.label,
});

final BuildContext context;
final String label;

@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: ColoredBox(
color: Colors.white,
child: SizedBox(
height: 30,
child: Row(
children: [
const SizedBox(width: 16),
Expanded(child: Divider(
color: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor())),
const SizedBox(width: 8),
Text(label,
textAlign: TextAlign.center,
style: TextStyle(
color: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(),
fontFamily: 'Source Sans 3',
fontSize: 14,
letterSpacing: 1.04,
height: (16 / 14),
).merge(weightVariableTextStyle(context))),
const SizedBox(width: 8),
Expanded(child: Divider(
color: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor())),
const SizedBox(width: 16),
]))));
}
}

class _SubscriptionList extends StatelessWidget {
const _SubscriptionList({
required this.unreadsModel,
required this.subscriptions,
});

final Unreads? unreadsModel;
final List<Subscription> subscriptions;

@override
Widget build(BuildContext context) {
return SliverList.builder(
itemCount: subscriptions.length,
itemBuilder: (BuildContext context, int index) {
final subscription = subscriptions[index];
final unreadCount = unreadsModel!.countInStreamNarrow(subscription.streamId);
return SubscriptionItem(subscription: subscription, unreadCount: unreadCount);
});
}
}

@visibleForTesting
class SubscriptionItem extends StatelessWidget {
const SubscriptionItem({
super.key,
required this.subscription,
required this.unreadCount,
});

final Subscription subscription;
final int unreadCount;

@override
Widget build(BuildContext context) {
final swatch = subscription.colorSwatch();
final [double wght, double wghtBold] = (unreadCount > 0)
? [600, 900]
: [400, 600];
return Material(
color: Colors.white,
child: InkWell(
onTap: () {
Navigator.push(context,
MessageListPage.buildRoute(context: context, narrow: StreamNarrow(subscription.streamId)));
},
child: SizedBox(height: 40,
child: Row(crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(width: 16),
_SubscriptionIcon(subscription: subscription, swatch: swatch),
const SizedBox(width: 5),
Expanded(child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(subscription.name,
style: const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 18,
height: (20 / 18),
color: Color(0xFF262626),
).merge(weightVariableTextStyle(context,
wght: wght,
wghtIfPlatformRequestsBold: wghtBold)),
maxLines: 1,
overflow: TextOverflow.ellipsis))),
if (unreadCount > 0) ...[
const SizedBox(width: 12),
// TODO(#384) show @-mention indicator when it applies
UnreadCountBadge(count: unreadCount, backgroundColor: swatch),
],
const SizedBox(width: 16),
],
))));
}
}

class _SubscriptionIcon extends StatelessWidget {
const _SubscriptionIcon({
required this.subscription,
required this.swatch,
});

final Subscription subscription;
final StreamColorSwatch swatch;

@override
Widget build(BuildContext context) {
return Icon(iconDataFromStream(subscription), size: 18, color: swatch.iconOnBarBackground);
}
}
Loading

0 comments on commit ccea15d

Please sign in to comment.