diff --git a/lib/widgets/subscription_list.dart b/lib/widgets/subscription_list.dart index d2faf03b2c..5c61e89076 100644 --- a/lib/widgets/subscription_list.dart +++ b/lib/widgets/subscription_list.dart @@ -175,10 +175,30 @@ class _SubscriptionList extends StatelessWidget { @override Widget build(BuildContext context) { + + final sortedSubscriptions = List.from(subscriptions) + ..sort((a, b) { + if (a.pinToTop != b.pinToTop) { + return a.pinToTop ? -1 : 1; + } + + if (a.isMuted != b.isMuted) { + return a.isMuted ? 1 : -1; + } + + final isEmojiA = _startsWithEmoji(a.name); + final isEmojiB = _startsWithEmoji(b.name); + if (isEmojiA != isEmojiB) { + return isEmojiA ? -1 : 1; + } + + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + return SliverList.builder( itemCount: subscriptions.length, itemBuilder: (BuildContext context, int index) { - final subscription = subscriptions[index]; + final subscription = sortedSubscriptions[index]; final unreadCount = unreadsModel!.countInChannel(subscription.streamId); final showMutedUnreadBadge = unreadCount == 0 && unreadsModel!.countInChannelNarrow(subscription.streamId) > 0; @@ -189,6 +209,23 @@ class _SubscriptionList extends StatelessWidget { } } +bool _startsWithEmoji(String name) { + final firstChar = name.characters.first; + final int firstCharCode = firstChar.runes.first; + + return (firstCharCode >= 0x1F600 && firstCharCode <= 0x1F64F) || // Emoticons + (firstCharCode >= 0x1F300 && firstCharCode <= 0x1F5FF) || // Misc Symbols and Pictographs + (firstCharCode >= 0x1F680 && firstCharCode <= 0x1F6FF) || // Transport and Map + (firstCharCode >= 0x1F700 && firstCharCode <= 0x1F77F) || // Alchemical Symbols + (firstCharCode >= 0x2600 && firstCharCode <= 0x26FF) || // Misc Symbols + (firstCharCode >= 0x2700 && firstCharCode <= 0x27BF) || // Dingbats + (firstCharCode >= 0xFE00 && firstCharCode <= 0xFE0F) || // Variation Selectors + (firstCharCode >= 0x1F900 && firstCharCode <= 0x1F9FF) || // Supplemental Symbols and Pictographs + (firstCharCode >= 0x1FA70 && firstCharCode <= 0x1FAFF) || // Symbols and Pictographs Extended-A + (firstCharCode >= 0x1F1E6 && firstCharCode <= 0x1F1FF) || // Flags + (firstCharCode >= 0x1F000 && firstCharCode <= 0x1FFFF); // Supplementary Multilingual Plane +} + @visibleForTesting class SubscriptionItem extends StatelessWidget { const SubscriptionItem({ diff --git a/test/widgets/subscription_list_test.dart b/test/widgets/subscription_list_test.dart index 9439bac865..1a7f324292 100644 --- a/test/widgets/subscription_list_test.dart +++ b/test/widgets/subscription_list_test.dart @@ -150,6 +150,32 @@ void main() { ]); check(listedStreamIds(tester)).deepEquals([2, 1, 3, 4, 6, 5]); }); + + testWidgets('channels with emoji in name are listed above non-emoji names', (tester) async { + await setupStreamListPage(tester, subscriptions: [ + eg.subscription(eg.stream(streamId: 1, name: '😊 Happy Stream')), + eg.subscription(eg.stream(streamId: 2, name: 'Alpha Stream')), + eg.subscription(eg.stream(streamId: 3, name: '🚀 Rocket Stream')), + eg.subscription(eg.stream(streamId: 4, name: 'Beta Stream')), + ]); + + check(listedStreamIds(tester)).deepEquals([1, 3, 2, 4]); + }); + + testWidgets('channels with emoji in name, pinned, unpinned, muted, and unmuted are sorted correctly', (tester) async { + await setupStreamListPage(tester, subscriptions: [ + eg.subscription(eg.stream(streamId: 1, name: '😊 Happy Stream'), pinToTop: true, isMuted: false), + eg.subscription(eg.stream(streamId: 2, name: '🚀 Rocket Stream'), pinToTop: true, isMuted: true), + eg.subscription(eg.stream(streamId: 3, name: 'Alpha Stream'), pinToTop: true, isMuted: false), + eg.subscription(eg.stream(streamId: 4, name: 'Beta Stream'), pinToTop: true, isMuted: true), + eg.subscription(eg.stream(streamId: 5, name: '🌟 Star Stream'), pinToTop: false, isMuted: false), + eg.subscription(eg.stream(streamId: 6, name: '🔥 Fire Stream'), pinToTop: false, isMuted: true), + eg.subscription(eg.stream(streamId: 7, name: 'Gamma Stream'), pinToTop: false, isMuted: false), + eg.subscription(eg.stream(streamId: 8, name: 'Delta Stream'), pinToTop: false, isMuted: true), + ]); + + check(listedStreamIds(tester)).deepEquals([1,3,2,4,5,7,6,8]); + }); }); testWidgets('unread badge shows with unreads', (tester) async {