Skip to content

Commit 63fefd8

Browse files
author
chimnayajith
committed

12 files changed

+174
-0
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,5 +792,9 @@
792792
"scrollToBottomTooltip": "Scroll to bottom",
793793
"@scrollToBottomTooltip": {
794794
"description": "Tooltip for button to scroll to bottom."
795+
},
796+
"emptyInboxMessage": "There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.",
797+
"@emptyInboxMessage": {
798+
"description": "Message shown when inbox is empty. [combined feed] will be replaced with a clickable link to Combined Feed."
795799
}
796800
}

lib/generated/l10n/zulip_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,12 @@ abstract class ZulipLocalizations {
11661166
/// In en, this message translates to:
11671167
/// **'Scroll to bottom'**
11681168
String get scrollToBottomTooltip;
1169+
1170+
/// Message shown when inbox is empty. [channels] will be replaced with a clickable link to Combined Feed.
1171+
///
1172+
/// In en, this message translates to:
1173+
/// **'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.'**
1174+
String get emptyInboxMessage;
11691175
}
11701176

11711177
class _ZulipLocalizationsDelegate extends LocalizationsDelegate<ZulipLocalizations> {

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,7 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
620620

621621
@override
622622
String get scrollToBottomTooltip => 'Scroll to bottom';
623+
624+
@override
625+
String get emptyInboxMessage => 'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.';
623626
}

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,7 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
620620

621621
@override
622622
String get scrollToBottomTooltip => 'Scroll to bottom';
623+
624+
@override
625+
String get emptyInboxMessage => 'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.';
623626
}

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,7 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
620620

621621
@override
622622
String get scrollToBottomTooltip => 'Scroll to bottom';
623+
624+
@override
625+
String get emptyInboxMessage => 'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.';
623626
}

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,7 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
620620

621621
@override
622622
String get scrollToBottomTooltip => 'Scroll to bottom';
623+
624+
@override
625+
String get emptyInboxMessage => 'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.';
623626
}

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
620620

621621
@override
622622
String get scrollToBottomTooltip => 'Scroll to bottom';
623+
624+
@override
625+
String get emptyInboxMessage => 'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.';
623626
}

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
620620

621621
@override
622622
String get scrollToBottomTooltip => 'Scroll to bottom';
623+
624+
@override
625+
String get emptyInboxMessage => 'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.';
623626
}

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,7 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
620620

621621
@override
622622
String get scrollToBottomTooltip => 'Scroll to bottom';
623+
624+
@override
625+
String get emptyInboxMessage => 'There are no unread messages in your Inbox.\nCheck out the [combined feed] for recent messages.';
623626
}

lib/widgets/inbox.dart

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '../model/narrow.dart';
66
import '../model/recent_dm_conversations.dart';
77
import '../model/unreads.dart';
88
import 'action_sheet.dart';
9+
import 'color.dart';
910
import 'icons.dart';
1011
import 'message_list.dart';
1112
import 'sticky_header.dart';
@@ -160,6 +161,10 @@ class _InboxPageState extends State<InboxPageBody> with PerAccountStoreAwareStat
160161
sections.add(_StreamSectionData(streamId, countInStream, streamHasMention, topicItems));
161162
}
162163

164+
if (sections.isEmpty) {
165+
return const InboxEmptyWidget();
166+
}
167+
163168
return SafeArea(
164169
// Don't pad the bottom here; we want the list content to do that.
165170
bottom: false,
@@ -182,6 +187,92 @@ class _InboxPageState extends State<InboxPageBody> with PerAccountStoreAwareStat
182187
}
183188
}
184189

190+
class InboxEmptyWidget extends StatelessWidget {
191+
const InboxEmptyWidget({super.key});
192+
193+
// Splits a message containing text in square brackets into three parts.
194+
List<String> _splitMessage(String message) {
195+
final pattern = RegExp(r'(.*?)\[(.*?)\](.*)', dotAll: true);
196+
final match = pattern.firstMatch(message);
197+
198+
return match == null
199+
? [message, '', '']
200+
: [
201+
match.group(1) ?? '',
202+
match.group(2) ?? '',
203+
match.group(3) ?? '',
204+
];
205+
}
206+
207+
@override
208+
Widget build(BuildContext context) {
209+
final zulipLocalizations = ZulipLocalizations.of(context);
210+
final designVariables = DesignVariables.of(context);
211+
212+
final messageParts = _splitMessage(zulipLocalizations.emptyInboxMessage);
213+
214+
return Center(
215+
child: Padding(
216+
padding: const EdgeInsets.all(16),
217+
child: Column(
218+
mainAxisAlignment: MainAxisAlignment.start,
219+
children: [
220+
const SizedBox(height: 48),
221+
Icon(
222+
ZulipIcons.inbox_done,
223+
size: 80,
224+
color: designVariables.foreground.withFadedAlpha(0.3),
225+
),
226+
const SizedBox(height: 16),
227+
Text.rich(
228+
TextSpan(
229+
style: TextStyle(
230+
color: designVariables.labelSearchPrompt,
231+
fontSize: 17,
232+
fontWeight: FontWeight.w500,
233+
),
234+
children: [
235+
TextSpan(text: messageParts[0]),
236+
WidgetSpan(
237+
alignment: PlaceholderAlignment.baseline,
238+
baseline: TextBaseline.alphabetic,
239+
child: TextButton(
240+
style: TextButton.styleFrom(
241+
padding: EdgeInsets.zero,
242+
minimumSize: Size.zero,
243+
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
244+
splashFactory: NoSplash.splashFactory
245+
),
246+
onPressed: () => Navigator.push(context,
247+
MessageListPage.buildRoute(context: context,
248+
narrow: const CombinedFeedNarrow())),
249+
child: Text(
250+
messageParts[1],
251+
style: TextStyle(
252+
fontSize: 17,
253+
fontWeight: FontWeight.w500,
254+
color: designVariables.link,
255+
decoration: TextDecoration.underline,
256+
decorationStyle: TextDecorationStyle.solid,
257+
decorationThickness: 2.5,
258+
decorationColor: designVariables.link,
259+
height: 1.5,
260+
)
261+
),
262+
),
263+
),
264+
TextSpan(text: messageParts[2]),
265+
],
266+
),
267+
textAlign: TextAlign.center,
268+
),
269+
],
270+
),
271+
),
272+
);
273+
}
274+
}
275+
185276
sealed class _InboxSectionData {
186277
const _InboxSectionData();
187278
}

lib/widgets/theme.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
157157
groupDmConversationIcon: Colors.black.withValues(alpha: 0.5),
158158
groupDmConversationIconBg: const Color(0x33808080),
159159
inboxItemIconMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(),
160+
labelSearchPrompt: const Color(0xff000000).withValues(alpha: 0.5),
160161
loginOrDivider: const Color(0xffdedede),
161162
loginOrDividerText: const Color(0xff575757),
163+
link: const Color(0xff066bd0),
162164
modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.3),
163165
mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.8).toColor(),
164166
navigationButtonBg: Colors.black.withValues(alpha: 0.05),
@@ -209,8 +211,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
209211
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
210212
groupDmConversationIconBg: const Color(0x33cccccc),
211213
inboxItemIconMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(),
214+
labelSearchPrompt: const Color(0xffffffff).withValues(alpha: 0.5),
212215
loginOrDivider: const Color(0xff424242),
213216
loginOrDividerText: const Color(0xffa8a8a8),
217+
link: const Color(0xff00aaff),
214218
modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.5),
215219
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
216220
mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.6).toColor(),
@@ -263,8 +267,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
263267
required this.groupDmConversationIcon,
264268
required this.groupDmConversationIconBg,
265269
required this.inboxItemIconMarker,
270+
required this.labelSearchPrompt,
266271
required this.loginOrDivider,
267272
required this.loginOrDividerText,
273+
required this.link,
268274
required this.modalBarrierColor,
269275
required this.mutedUnreadBadge,
270276
required this.navigationButtonBg,
@@ -325,8 +331,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
325331
final Color groupDmConversationIcon;
326332
final Color groupDmConversationIconBg;
327333
final Color inboxItemIconMarker;
334+
final Color labelSearchPrompt;
328335
final Color loginOrDivider; // TODO(design-dark) need proper dark-theme color (this is ad hoc)
329336
final Color loginOrDividerText; // TODO(design-dark) need proper dark-theme color (this is ad hoc)
337+
final Color link;
330338
final Color modalBarrierColor;
331339
final Color mutedUnreadBadge;
332340
final Color navigationButtonBg;
@@ -374,8 +382,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
374382
Color? groupDmConversationIcon,
375383
Color? groupDmConversationIconBg,
376384
Color? inboxItemIconMarker,
385+
Color? labelSearchPrompt,
377386
Color? loginOrDivider,
378387
Color? loginOrDividerText,
388+
Color? link,
379389
Color? modalBarrierColor,
380390
Color? mutedUnreadBadge,
381391
Color? navigationButtonBg,
@@ -422,8 +432,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
422432
groupDmConversationIcon: groupDmConversationIcon ?? this.groupDmConversationIcon,
423433
groupDmConversationIconBg: groupDmConversationIconBg ?? this.groupDmConversationIconBg,
424434
inboxItemIconMarker: inboxItemIconMarker ?? this.inboxItemIconMarker,
435+
labelSearchPrompt: labelSearchPrompt ?? this.labelSearchPrompt,
425436
loginOrDivider: loginOrDivider ?? this.loginOrDivider,
426437
loginOrDividerText: loginOrDividerText ?? this.loginOrDividerText,
438+
link: link ?? this.link,
427439
modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor,
428440
mutedUnreadBadge: mutedUnreadBadge ?? this.mutedUnreadBadge,
429441
navigationButtonBg: navigationButtonBg ?? this.navigationButtonBg,
@@ -477,8 +489,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
477489
groupDmConversationIcon: Color.lerp(groupDmConversationIcon, other.groupDmConversationIcon, t)!,
478490
groupDmConversationIconBg: Color.lerp(groupDmConversationIconBg, other.groupDmConversationIconBg, t)!,
479491
inboxItemIconMarker: Color.lerp(inboxItemIconMarker, other.inboxItemIconMarker, t)!,
492+
labelSearchPrompt: Color.lerp(labelSearchPrompt, other.labelSearchPrompt, t)!,
480493
loginOrDivider: Color.lerp(loginOrDivider, other.loginOrDivider, t)!,
481494
loginOrDividerText: Color.lerp(loginOrDividerText, other.loginOrDividerText, t)!,
495+
link: Color.lerp(link, other.link, t)!,
482496
modalBarrierColor: Color.lerp(modalBarrierColor, other.modalBarrierColor, t)!,
483497
mutedUnreadBadge: Color.lerp(mutedUnreadBadge, other.mutedUnreadBadge, t)!,
484498
navigationButtonBg: Color.lerp(navigationButtonBg, other.navigationButtonBg, t)!,

test/widgets/inbox_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@ import 'package:flutter_checks/flutter_checks.dart';
44
import 'package:flutter_test/flutter_test.dart';
55
import 'package:zulip/api/model/events.dart';
66
import 'package:zulip/api/model/model.dart';
7+
import 'package:zulip/model/narrow.dart';
78
import 'package:zulip/model/store.dart';
89
import 'package:zulip/widgets/color.dart';
910
import 'package:zulip/widgets/home.dart';
1011
import 'package:zulip/widgets/icons.dart';
1112
import 'package:zulip/widgets/channel_colors.dart';
13+
import 'package:zulip/widgets/message_list.dart';
14+
import 'package:zulip/widgets/page.dart';
1215

16+
import '../api/fake_api.dart';
1317
import '../example_data.dart' as eg;
1418
import '../flutter_checks.dart';
1519
import '../model/binding.dart';
1620
import '../model/test_store.dart';
21+
import '../test_navigation.dart';
22+
import 'message_list_checks.dart';
1723
import 'test_app.dart';
24+
import 'page_checks.dart';
1825

1926
/// Repeatedly drags `view` by `moveStep` until `finder` is invisible.
2027
///
@@ -52,6 +59,7 @@ void main() {
5259
TestZulipBinding.ensureInitialized();
5360

5461
late PerAccountStore store;
62+
late FakeApiConnection connection;
5563

5664
Future<void> setupPage(WidgetTester tester, {
5765
List<ZulipStream>? streams,
@@ -203,6 +211,36 @@ void main() {
203211
await setupVarious(tester);
204212
});
205213

214+
testWidgets('empty inbox shows empty state', (tester) async {
215+
final pushedRoutes = <Route<dynamic>>[];
216+
final testNavObserver = TestNavigatorObserver()
217+
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
218+
219+
await setupPage(tester,
220+
unreadMessages: [],
221+
navigatorObserver: testNavObserver);
222+
pushedRoutes.clear();
223+
224+
connection = store.connection as FakeApiConnection;
225+
connection.prepare(json: eg.newestGetMessagesResult(
226+
foundOldest: true,
227+
messages: []).toJson());
228+
229+
expect(find.byIcon(ZulipIcons.inbox_done), findsOneWidget);
230+
expect(find.textContaining('There are no unread messages in your Inbox.'), findsOneWidget);
231+
232+
final channelsButton = find.text('channels');
233+
expect(channelsButton, findsOneWidget);
234+
235+
await tester.tap(channelsButton);
236+
await tester.pump();
237+
await tester.pump(const Duration(milliseconds: 250));
238+
239+
check(pushedRoutes).single.isA<WidgetRoute>().page
240+
.isA<MessageListPage>()
241+
.initNarrow.equals(const CombinedFeedNarrow());
242+
});
243+
206244
// TODO test that tapping a conversation row opens the message list
207245
// for the conversation
208246

0 commit comments

Comments
 (0)