Skip to content

Commit

Permalink
streamlist: Add new StreamListPage
Browse files Browse the repository at this point in the history
  • Loading branch information
sirpengi committed Nov 15, 2023
1 parent 959a0be commit 364d440
Show file tree
Hide file tree
Showing 2 changed files with 235 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 @@ -13,6 +13,7 @@ import 'message_list.dart';
import 'page.dart';
import 'recent_dm_conversations.dart';
import 'store.dart';
import 'stream_list.dart';

class ZulipApp extends StatelessWidget {
const ZulipApp({super.key, this.navigatorObservers});
Expand Down Expand Up @@ -249,6 +250,11 @@ class HomePage extends StatelessWidget {
narrow: const AllMessagesNarrow())),
child: const Text("All messages")),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.push(context,
StreamListPage.buildRoute(context: context)),
child: const Text("Subscribed streams")),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.push(context,
RecentDmConversationsPage.buildRoute(context: context)),
Expand Down
229 changes: 229 additions & 0 deletions lib/widgets/stream_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import 'dart:ui';

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';

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

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

@override
State<StreamListPage> createState() => _StreamListPageState();
}

class _StreamListPageState extends State<StreamListPage> with PerAccountStoreAwareStateMixin<StreamListPage> {
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) {
final List<Subscription> pinned = [];
final List<Subscription> active = [];
for (final subscription in subscriptions!.values) {
switch (subscription) {
case Subscription(pinToTop: true):
pinned.add(subscription);
default:
active.add(subscription);
}
}
// TODO(intl): add locale-aware sorting
pinned.sortBy((subscription) => subscriptions![subscription.streamId]?.name ?? '');
active.sortBy((subscription) => subscriptions![subscription.streamId]?.name ?? '');
return Scaffold(
appBar: AppBar(title: const Text("Streams")),
body: Builder(
builder: (BuildContext context) => Center(
child: CustomScrollView(
slivers: [
if (pinned.isNotEmpty) ...[
_SubscriptionListHeader(context: context, label: "Pinned"),
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: pinned, padBottom: active.isEmpty),
],
if (active.isNotEmpty) ...[
_SubscriptionListHeader(context: context, label: "Active"),
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: active, padBottom: true),
],
]))));
}
}

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: 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,
required this.padBottom,
});

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

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

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

final Subscription subscription;
final int unreadCount;

@override
Widget build(BuildContext context) {
final streamColor = colorForStream(subscription);
return InkWell(
onTap: () {
Navigator.push(context,
MessageListPage.buildRoute(context: context, narrow: StreamNarrow(subscription.streamId)));
},
child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 40),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_StreamListIcon(subscription: subscription, color: streamColor),
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,
color: Color(0xFF262626),
fontWeight: FontWeight.w600,
).merge(weightVariableTextStyle(context,
wght: (unreadCount > 0) ? 600 : 400,
wghtIfPlatformRequestsBold: 600)),
overflow: TextOverflow.ellipsis))),
const SizedBox(width: 12),
unreadCount > 0
? DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: const Color.fromRGBO(102, 102, 153, 0.15),
),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(4, 0, 4, 1),
child: Text(unreadCount.toString(),
style: const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 16,
height: (18 / 16),
fontFeatures: [FontFeature.enable('smcp')], // small caps
color: Color(0xFF222222),
).merge(weightVariableTextStyle(context)))))
: const SizedBox(),
],
)
)));
}
}

class _StreamListIcon extends StatelessWidget {
const _StreamListIcon({
required this.subscription,
required this.color,
});

final Subscription subscription;
final Color color;

@override
Widget build(BuildContext context) {
final icon = switch (subscription) {
Subscription(isWebPublic: true) => ZulipIcons.globe,
Subscription(inviteOnly: true) => ZulipIcons.lock,
Subscription() => ZulipIcons.hash_sign,
};
return Icon(icon, size: 18, color: color);
}
}

0 comments on commit 364d440

Please sign in to comment.