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.
- Loading branch information
Showing
2 changed files
with
235 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,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); | ||
} | ||
} |