Skip to content

msglist: Friendlier placeholder text when narrow has no messages (simple version) #1650

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

Merged
merged 3 commits into from
Jul 3, 2025
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
4 changes: 4 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,10 @@
"others": {"type": "String", "example": "Alice, Bob"}
}
},
"emptyMessageList": "There are no messages here.",
"@emptyMessageList": {
"description": "Placeholder for some message-list pages when there are no messages."
},
"messageListGroupYouWithYourself": "Messages with yourself",
"@messageListGroupYouWithYourself": {
"description": "Message list recipient header for a DM group that only includes yourself."
Expand Down
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,12 @@ abstract class ZulipLocalizations {
/// **'DMs with {others}'**
String dmsWithOthersPageTitle(String others);

/// Placeholder for some message-list pages when there are no messages.
///
/// In en, this message translates to:
/// **'There are no messages here.'**
String get emptyMessageList;

/// Message list recipient header for a DM group that only includes yourself.
///
/// In en, this message translates to:
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
return 'DMs with $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Messages with yourself';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_de.dart
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
return 'DNs mit $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Nachrichten mit dir selbst';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
return 'DMs with $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Messages with yourself';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_it.dart
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
return 'MD con $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Messaggi con te stesso';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
return 'DMs with $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Messages with yourself';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
return 'DMs with $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Messages with yourself';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
return 'DM z $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Zapiski na własne konto';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
return 'ЛС с $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Сообщения с собой';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
return 'DMs with $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Messages with yourself';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_sl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
return 'Neposredna sporočila z $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Sporočila sebi';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_uk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
return 'Особисті повідомлення з $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Повідомлення з собою';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_zh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
return 'DMs with $others';
}

@override
String get emptyMessageList => 'There are no messages here.';

@override
String get messageListGroupYouWithYourself => 'Messages with yourself';

Expand Down
34 changes: 0 additions & 34 deletions lib/widgets/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,40 +148,6 @@ class _HomePageState extends State<HomePage> {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page: Move no-content placeholder widget to page.dart, from home.dart

Should this commit be marked as nfc?

}

/// A "no content here" message, for the Inbox, Subscriptions, and DMs pages.
///
/// This should go near the root of the "page body"'s widget subtree.
/// In particular, it handles the horizontal device insets.
/// (The vertical insets are handled externally, by the app bar and bottom nav.)
class PageBodyEmptyContentPlaceholder extends StatelessWidget {
const PageBodyEmptyContentPlaceholder({super.key, required this.message});

final String message;

@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);

return SafeArea(
minimum: EdgeInsets.symmetric(horizontal: 24),
child: Padding(
padding: EdgeInsets.only(top: 48, bottom: 16),
child: Align(
alignment: Alignment.topCenter,
// TODO leading and trailing elements, like in Figma (given as SVGs):
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=5957-167736&m=dev
child: Text(
textAlign: TextAlign.center,
style: TextStyle(
color: designVariables.labelSearchPrompt,
fontSize: 17,
height: 23 / 17,
).merge(weightVariableTextStyle(context, wght: 500)),
message))));
}
}


const kTryAnotherAccountWaitPeriod = Duration(seconds: 5);

class _LoadingPlaceholderPage extends StatefulWidget {
Expand Down
2 changes: 1 addition & 1 deletion lib/widgets/inbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import '../model/narrow.dart';
import '../model/recent_dm_conversations.dart';
import '../model/unreads.dart';
import 'action_sheet.dart';
import 'home.dart';
import 'icons.dart';
import 'message_list.dart';
import 'page.dart';
import 'sticky_header.dart';
import 'store.dart';
import 'text.dart';
Expand Down
7 changes: 7 additions & 0 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -859,8 +859,15 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat

@override
Widget build(BuildContext context) {
final zulipLocalizations = ZulipLocalizations.of(context);

if (!model.fetched) return const Center(child: CircularProgressIndicator());

if (model.items.isEmpty && model.haveNewest && model.haveOldest) {
return PageBodyEmptyContentPlaceholder(
message: zulipLocalizations.emptyMessageList);
}

// Pad the left and right insets, for small devices in landscape.
return SafeArea(
// Don't let this be the place we pad the bottom inset. When there's
Expand Down
39 changes: 39 additions & 0 deletions lib/widgets/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import 'package:flutter/material.dart';

import 'store.dart';
import 'text.dart';
import 'theme.dart';

/// An [InheritedWidget] for near the root of a page's widget subtree,
/// providing its [BuildContext].
Expand Down Expand Up @@ -210,3 +212,40 @@ class LoadingPlaceholderPage extends StatelessWidget {
);
}
}

/// A "no content here" message for when a page has no content to show.
///
/// Suitable for the inbox, the message-list page, etc.
///
/// This handles the horizontal device insets
/// and the bottom inset when needed (in a message list with no compose box).
/// The top inset is handled externally by the app bar.
// TODO(#311) If the message list gets a bottom nav, the bottom inset will
// always be handled externally too; simplify implementation and dartdoc.
class PageBodyEmptyContentPlaceholder extends StatelessWidget {
const PageBodyEmptyContentPlaceholder({super.key, required this.message});

final String message;

@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);

return SafeArea(
minimum: EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Padding(
padding: EdgeInsets.only(top: 48),
child: Align(
alignment: Alignment.topCenter,
// TODO leading and trailing elements, like in Figma (given as SVGs):
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=5957-167736&m=dev
child: Text(
textAlign: TextAlign.center,
style: TextStyle(
color: designVariables.labelSearchPrompt,
fontSize: 17,
height: 23 / 17,
).merge(weightVariableTextStyle(context, wght: 500)),
message))));
}
}
2 changes: 1 addition & 1 deletion lib/widgets/recent_dm_conversations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import '../model/narrow.dart';
import '../model/recent_dm_conversations.dart';
import '../model/unreads.dart';
import 'content.dart';
import 'home.dart';
import 'icons.dart';
import 'message_list.dart';
import 'new_dm_sheet.dart';
import 'page.dart';
import 'store.dart';
import 'text.dart';
import 'theme.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/widgets/subscription_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import '../generated/l10n/zulip_localizations.dart';
import '../model/narrow.dart';
import '../model/unreads.dart';
import 'action_sheet.dart';
import 'home.dart';
import 'icons.dart';
import 'message_list.dart';
import 'page.dart';
import 'store.dart';
import 'text.dart';
import 'theme.dart';
Expand Down
36 changes: 33 additions & 3 deletions test/widgets/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ void main() {
return findScrollView(tester).controller;
}

final contentInputFinder = find.byWidgetPredicate(
(widget) => widget is TextField && widget.controller is ComposeContentController);

group('MessageListPage', () {
testWidgets('ancestorOf finds page state from message', (tester) async {
await setupMessageListPage(tester,
Expand Down Expand Up @@ -346,6 +349,36 @@ void main() {
});
});

group('no-messages placeholder', () {
final findPlaceholder = find.byType(PageBodyEmptyContentPlaceholder);

testWidgets('Combined feed', (tester) async {
await setupMessageListPage(tester, narrow: CombinedFeedNarrow(), messages: []);
check(
find.descendant(
of: findPlaceholder,
matching: find.textContaining('There are no messages here.')),
).findsOne();
});

testWidgets('when `messages` empty but `outboxMessages` not empty, show outboxes, not placeholder', (tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good thought to test this.

final channel = eg.stream();
await setupMessageListPage(tester,
narrow: TopicNarrow(channel.streamId, eg.t('topic')),
streams: [channel],
messages: []);
check(findPlaceholder).findsOne();

connection.prepare(json: SendMessageResult(id: 1).toJson());
await tester.enterText(contentInputFinder, 'asdfjkl;');
await tester.tap(find.byIcon(ZulipIcons.send));
await tester.pump(kLocalEchoDebounceDuration);

check(findPlaceholder).findsNothing();
check(find.text('asdfjkl;')).findsOne();
Comment on lines +373 to +378
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is a nice user-focused structure for the test.

});
});

group('presents message content appropriately', () {
testWidgets('content not asked to consume insets (including bottom), even without compose box', (tester) async {
// Regression test for: https://github.com/zulip/zulip-flutter/issues/736
Expand Down Expand Up @@ -1837,9 +1870,6 @@ void main() {
final topicNarrow = eg.topicNarrow(stream.streamId, topic);
const content = 'outbox message content';

final contentInputFinder = find.byWidgetPredicate(
(widget) => widget is TextField && widget.controller is ComposeContentController);

Finder outboxMessageFinder = find.widgetWithText(
OutboxMessageWithPossibleSender, content, skipOffstage: true);

Expand Down