Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscriptions page #397

Merged
merged 2 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -247,6 +248,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
13 changes: 13 additions & 0 deletions lib/widgets/icons.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

import 'package:flutter/widgets.dart';

import '../api/model/model.dart';

// ignore_for_file: constant_identifier_names

/// Identifiers for Zulip's custom icons.
Expand Down Expand Up @@ -63,3 +65,14 @@ abstract final class ZulipIcons {

// END GENERATED ICON DATA
}

IconData iconDataForStream(ZulipStream stream) {
// TODO: these icons aren't quite right yet;
// see this message and the one after it:
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1680637
return switch(stream) {
ZulipStream(isWebPublic: true) => ZulipIcons.globe,
ZulipStream(inviteOnly: true) => ZulipIcons.lock,
ZulipStream() => ZulipIcons.hash_sign,
};
}
8 changes: 1 addition & 7 deletions lib/widgets/inbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -385,13 +385,7 @@ class _StreamHeaderItem extends _HeaderItem {
});

@override get title => subscription.name;
@override get icon => switch (subscription) {
// TODO these icons aren't quite right yet; see this message and the following:
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1680637
Subscription(isWebPublic: true) => ZulipIcons.globe,
Subscription(inviteOnly: true) => ZulipIcons.lock,
Subscription() => ZulipIcons.hash_sign,
};
@override get icon => iconDataForStream(subscription);
@override get collapsedIconColor => subscription.colorSwatch().iconOnPlainBackground;
@override get uncollapsedIconColor => subscription.colorSwatch().iconOnBarBackground;
@override get uncollapsedBackgroundColor =>
Expand Down
8 changes: 2 additions & 6 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,8 @@ class MessageListAppBarTitle extends StatelessWidget {
final Narrow narrow;

Widget _buildStreamRow(ZulipStream? stream, String text) {
final icon = switch (stream) {
ZulipStream(isWebPublic: true) => ZulipIcons.globe,
ZulipStream(inviteOnly: true) => ZulipIcons.lock,
ZulipStream() => ZulipIcons.hash_sign,
null => null, // A null [Icon.icon] makes a blank space.
};
// A null [Icon.icon] makes a blank space.
final icon = (stream != null) ? iconDataForStream(stream) : null;
return Row(
mainAxisSize: MainAxisSize.min,
// TODO(design): The vertical alignment of the stream privacy icon is a bit ad hoc.
Expand Down
238 changes: 238 additions & 0 deletions lib/widgets/subscription_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
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> {
Unreads? unreadsModel;

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

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

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

@override
Widget build(BuildContext context) {
// Design referenced from:
// https://www.figma.com/file/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?type=design&node-id=171-12359&mode=design&t=4d0vykoYQ0KGpFuu-0

// 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 not worth optimizing as it 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: Implement collapsible topics

// TODO(i18n): localize strings on page
// Strings here left unlocalized as they likely will not
// exist in the settled design.
final store = PerAccountStoreWidget.of(context);

final List<Subscription> pinned = [];
final List<Subscription> unpinned = [];
for (final subscription in store.subscriptions.values) {
if (subscription.pinToTop) {
pinned.add(subscription);
} else {
unpinned.add(subscription);
}
}
// TODO(i18n): add locale-aware sorting
pinned.sortBy((subscription) => subscription.name);
unpinned.sortBy((subscription) => subscription.name);

return Scaffold(
appBar: AppBar(title: const Text("Streams")),
body: Center(
child: CustomScrollView(
slivers: [
if (pinned.isEmpty && unpinned.isEmpty)
const _NoSubscriptionsItem(),
if (pinned.isNotEmpty) ...[
const _SubscriptionListHeader(label: "Pinned"),
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: pinned),
],
if (unpinned.isNotEmpty) ...[
const _SubscriptionListHeader(label: "Unpinned"),
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: unpinned),
],

// TODO(#188): add button leading 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.label});

final String label;

@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: ColoredBox(
color: Colors.white,
child: Row(crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(width: 16),
Expanded(child: Divider(
color: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor())),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.symmetric(vertical: 7),
child: 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: 0.04 * 14,
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 hasUnreads = (unreadCount > 0);
return Material(
color: Colors.white,
child: InkWell(
onTap: () {
Navigator.push(context,
MessageListPage.buildRoute(context: context,
narrow: StreamNarrow(subscription.streamId)));
},
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
const SizedBox(width: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 11),
child: Icon(size: 18, color: swatch.iconOnPlainBackground,
iconDataForStream(subscription))),
const SizedBox(width: 5),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
// TODO(design): unclear whether bold text is applied to all subscriptions
// or only those with unreads:
// https://github.com/zulip/zulip-flutter/pull/397#pullrequestreview-1742524205
child: Text(
style: const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 18,
height: (20 / 18),
color: Color(0xFF262626),
).merge(hasUnreads
? weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900)
: weightVariableTextStyle(context)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
subscription.name))),
if (unreadCount > 0) ...[
const SizedBox(width: 12),
// TODO(#384) show @-mention indicator when it applies
UnreadCountBadge(count: unreadCount, backgroundColor: swatch, bold: true),
],
const SizedBox(width: 16),
])));
}
}
Loading