forked from zulip/zulip-flutter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
subscription_list: Add new SubscriptionListPage
Fixes zulip#187.
- Loading branch information
Showing
3 changed files
with
403 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.