mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: standardize app bar titles with serif

+29 -21
+8 -8
docs/tasks/phase-3.md
··· 20 20 21 21 ## M9 — Notifications 22 22 23 - - [ ] Notifications screen with grouped-by-day notification list 24 - - [ ] `NotificationBloc` — events: `NotificationsRequested`, `NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead` 25 - - [ ] Fetch notifications via `listNotifications` with cursor pagination 26 - - [ ] Render all notification reasons: like, repost, follow, mention, reply, quote 27 - - [ ] Each notification row: author avatar, reason icon, summary text, optional post preview 28 - - [ ] Unread count badge on nav bar via `getUnreadCount` polling (30s interval) 29 - - [ ] Mark as read via `updateSeen` when notifications screen opens 30 - - [ ] Tap notification to navigate to relevant post or profile 23 + - [x] Notifications screen with grouped-by-day notification list 24 + - [x] `NotificationBloc` — events: `NotificationsRequested`, `NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead` 25 + - [x] Fetch notifications via `listNotifications` with cursor pagination 26 + - [x] Render all notification reasons: like, repost, follow, mention, reply, quote 27 + - [x] Each notification row: author avatar, reason icon, summary text, optional post preview 28 + - [x] Unread count badge on nav bar via `getUnreadCount` polling (30s interval) 29 + - [x] Mark as read via `updateSeen` when notifications screen opens 30 + - [x] Tap notification to navigate to relevant post or profile 31 31 32 32 ## M10 — Post & Profile Actions 33 33
+2
lib/core/router/app_shell.dart
··· 13 13 return Scaffold( 14 14 body: navigationShell, 15 15 bottomNavigationBar: NavigationBar( 16 + height: 50, 16 17 backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, 17 18 selectedIndex: navigationShell.currentIndex, 18 19 onDestinationSelected: (index) { 19 20 navigationShell.goBranch(index, initialLocation: index == navigationShell.currentIndex); 20 21 }, 22 + indicatorShape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(10)), 21 23 labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, 22 24 destinations: _destinations, 23 25 ),
+3 -3
lib/core/theme/typography.dart
··· 60 60 headlineLarge: lora(fontSize: 32, fontWeight: FontWeight.w600, color: headlineColor), 61 61 headlineMedium: lora(fontSize: 28, fontWeight: FontWeight.w600, color: headlineColor), 62 62 headlineSmall: lora(fontSize: 24, fontWeight: FontWeight.w600, color: headlineColor), 63 - titleLarge: dmSans(fontSize: 22, fontWeight: FontWeight.w600, color: bodyColor), 64 - titleMedium: dmSans(fontSize: 16, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.15), 65 - titleSmall: jetBrainsMono(fontSize: 14, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.1), 63 + titleLarge: lora(fontSize: 22, fontWeight: FontWeight.w600, color: bodyColor), 64 + titleMedium: lora(fontSize: 16, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.15), 65 + titleSmall: lora(fontSize: 14, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.1), 66 66 bodyLarge: dmSans(fontSize: 16, fontWeight: FontWeight.w400, color: bodyColor, letterSpacing: 0.5), 67 67 bodyMedium: dmSans(fontSize: 14, fontWeight: FontWeight.w400, color: bodyColor, letterSpacing: 0.25), 68 68 bodySmall: jetBrainsMono(fontSize: 12, fontWeight: FontWeight.w400, color: captionColor, letterSpacing: 0.4),
+5 -3
lib/features/feed/presentation/home_feed_screen.dart
··· 41 41 42 42 if (prefsState.status == FeedPreferencesStatus.error) { 43 43 return Scaffold( 44 - appBar: AppBar(title: const Text('Home')), 44 + appBar: AppBar(title: _title), 45 45 body: Center( 46 46 child: Column( 47 47 mainAxisAlignment: MainAxisAlignment.center, ··· 64 64 65 65 if (pinnedFeeds.isEmpty) { 66 66 return Scaffold( 67 - appBar: AppBar(title: const Text('Home')), 67 + appBar: AppBar(title: _title), 68 68 body: Center( 69 69 child: Padding( 70 70 padding: const EdgeInsets.all(24), ··· 92 92 93 93 return Scaffold( 94 94 appBar: AppBar( 95 - title: const Text('Home'), 95 + title: _title, 96 96 actions: [IconButton(icon: const Icon(Icons.rss_feed), onPressed: () => context.push('/feeds'))], 97 97 ), 98 98 body: Column( ··· 146 146 final index = feeds.indexWhere((feed) => feed.id == _selectedFeedId); 147 147 return index >= 0 ? index : 0; 148 148 } 149 + 150 + Widget get _title => Text('Home', style: Theme.of(context).textTheme.titleLarge); 149 151 150 152 Widget _buildTabBar( 151 153 BuildContext context,
+3 -1
lib/features/notifications/presentation/notifications_screen.dart
··· 48 48 context.read<UnreadCountCubit>().refresh(); 49 49 } 50 50 51 + Widget get _title => Text('Notifications', style: Theme.of(context).textTheme.titleMedium); 52 + 51 53 @override 52 54 Widget build(BuildContext context) { 53 55 return Scaffold( 54 56 appBar: AppBar( 55 - title: const Text('Notifications'), 57 + title: _title, 56 58 actions: [TextButton(onPressed: _markAllRead, child: const Text('Mark All Read'))], 57 59 ), 58 60 body: BlocBuilder<NotificationBloc, NotificationState>(
+3 -1
lib/features/settings/presentation/settings_screen.dart
··· 13 13 Widget build(BuildContext context) { 14 14 return Scaffold( 15 15 appBar: AppBar( 16 - title: const Text('Settings'), 16 + title: _title(context), 17 17 actions: [ 18 18 TextButton( 19 19 onPressed: () { ··· 116 116 ), 117 117 ); 118 118 } 119 + 120 + Widget _title(BuildContext context) => Text('Settings', style: Theme.of(context).textTheme.titleLarge); 119 121 120 122 Widget _buildThemeSelector(BuildContext context) { 121 123 final settingsCubit = context.read<SettingsCubit>();
+3 -3
test/features/notifications/bloc/notification_bloc_test.dart
··· 20 20 final sampleNotification = bsky.Notification( 21 21 uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/abc'), 22 22 cid: 'cid-123', 23 - author: ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), 23 + author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), 24 24 reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), 25 25 record: {r'$type': 'app.bsky.feed.post', 'text': 'Hello world'}, 26 26 isRead: false, ··· 77 77 final secondNotification = bsky.Notification( 78 78 uri: AtUri.parse('at://did:plc:author2/app.bsky.feed.post/def'), 79 79 cid: 'cid-456', 80 - author: ProfileView(did: 'did:plc:author2', handle: 'author2.bsky.social'), 80 + author: const ProfileView(did: 'did:plc:author2', handle: 'author2.bsky.social'), 81 81 reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.follow), 82 82 record: {}, 83 83 isRead: true, ··· 155 155 final newNotification = bsky.Notification( 156 156 uri: AtUri.parse('at://did:plc:new/app.bsky.feed.post/new'), 157 157 cid: 'cid-new', 158 - author: ProfileView(did: 'did:plc:new', handle: 'new.bsky.social'), 158 + author: const ProfileView(did: 'did:plc:new', handle: 'new.bsky.social'), 159 159 reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.repost), 160 160 record: {}, 161 161 isRead: false,
+2 -2
test/features/notifications/data/notification_repository_test.dart
··· 18 18 final sampleNotification = bsky.Notification( 19 19 uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/abc'), 20 20 cid: 'cid-123', 21 - author: ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), 21 + author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), 22 22 reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), 23 23 record: {r'$type': 'app.bsky.feed.post', 'text': 'Hello world'}, 24 24 isRead: false, ··· 97 97 final notification = bsky.Notification( 98 98 uri: AtUri.parse('at://did:plc:test/app.bsky.notification/1'), 99 99 cid: 'cid', 100 - author: ProfileView(did: 'did:plc:test', handle: 'test.bsky.social'), 100 + author: const ProfileView(did: 'did:plc:test', handle: 'test.bsky.social'), 101 101 reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.follow), 102 102 record: {}, 103 103 isRead: true,