Skip to content
48 changes: 42 additions & 6 deletions lib/widgets/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -831,12 +831,49 @@ class InlineIcon extends StatelessWidget {
}
}

/// An [InlineSpan] with a [ZulipIcons.check] icon (if resolved) and topic name.
///
/// Pass this to [Text.rich], which can be styled arbitrarily.
/// Pass the [fontSize] and [color] of surrounding text
/// so that the icons are sized and colored appropriately.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: This helper adds only one icon.

Suggested change
/// so that the icons are sized and colored appropriately.
/// so that the icon is sized and colored appropriately.

///
/// For a span with a channel name/icon and optional topic,
/// see [channelTopicLabelSpan].
InlineSpan topicLabelSpan({
required BuildContext context,
required TopicName topic,
required double fontSize,
required Color color,
}) {
final store = PerAccountStoreWidget.of(context);
final baselineType = localizedTextBaseline(context);

return TextSpan(children: [
if (topic.isResolved)
InlineIcon.asWidgetSpan(
icon: ZulipIcons.check,
fontSize: fontSize,
baselineType: baselineType,
color: color,
padAfter: true),
if (topic.unresolve().displayName != null)
TextSpan(text: topic.unresolve().displayName)
Comment on lines +859 to +860
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Can compute this topic.unresolve() once by assigning to a variable.

else
TextSpan(
style: TextStyle(fontStyle: FontStyle.italic),
text: store.realmEmptyTopicDisplayName),
]);
}

/// An [InlineSpan] with a channel privacy icon, channel name,
/// and optionally a chevron-right icon plus topic.
///
/// Pass this to [Text.rich], which can be styled arbitrarily.
/// Pass the [fontSize] and [color] of surrounding text
/// so that the icons are sized and colored appropriately.
///
/// For a span with just the topic (including checkmark icon as applicable)
/// see [topicLabelSpan].
InlineSpan channelTopicLabelSpan({
required BuildContext context,
required int channelId,
Expand Down Expand Up @@ -874,12 +911,11 @@ InlineSpan channelTopicLabelSpan({
color: color,
padBefore: true,
padAfter: true),
if (topic.displayName != null)
TextSpan(text: topic.displayName)
else
TextSpan(
style: TextStyle(fontStyle: FontStyle.italic),
text: store.realmEmptyTopicDisplayName),
topicLabelSpan(
context: context,
topic: topic,
fontSize: fontSize,
color: color),
],
]);
}
98 changes: 98 additions & 0 deletions test/widgets/text_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_checks/flutter_checks.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/api/model/model.dart';
import 'package:zulip/widgets/icons.dart';
import 'package:zulip/widgets/text.dart';

import '../example_data.dart' as eg;
import '../flutter_checks.dart';
import '../model/binding.dart';
import 'test_app.dart';
Expand Down Expand Up @@ -420,6 +423,101 @@ void main() {
testLocalizedTextBaseline(const Locale('und'), TextBaseline.alphabetic);
});

group('topicLabelSpan', () {
Future<void> prepareWidget(WidgetTester tester, {
required TopicName topic,
String? realmEmptyTopicDisplayName,
}) async {
addTearDown(testBinding.reset);
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
realmEmptyTopicDisplayName: realmEmptyTopicDisplayName));

await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: Builder(builder: (context) => Text.rich(
topicLabelSpan(
context: context,
topic: topic,
fontSize: 17,
color: Colors.black)))));
await tester.pump(); // global store
await tester.pump(); // per-account store
}

Finder findCheckIcon() => find.byWidgetPredicate(
(w) => w is InlineIcon && w.icon == ZulipIcons.check);

testWidgets('resolved topic shows checkmark and strips prefix', (tester) async {
await prepareWidget(tester, topic: eg.t('✔ some topic'));
check(findCheckIcon()).findsOne();
check(find.textContaining('some topic', findRichText: true)).findsOne();
check(find.textContaining('✔', findRichText: true)).findsNothing();
});

testWidgets('unresolved topic shows no checkmark', (tester) async {
await prepareWidget(tester, topic: eg.t('some topic'));
check(findCheckIcon()).findsNothing();
check(find.textContaining('some topic', findRichText: true)).findsOne();
});

testWidgets('empty topic shows italic placeholder', (tester) async {
await prepareWidget(tester, topic: eg.t(''),
realmEmptyTopicDisplayName: 'general chat');
check(find.textContaining('general chat', findRichText: true)).findsOne();

FontStyle? italicFontStyle;
final richText = tester.widget<RichText>(
find.textContaining('general chat', findRichText: true));
richText.text.visitChildren((span) {
if (span is TextSpan && span.text == 'general chat') {
italicFontStyle = span.style?.fontStyle;
return false;
}
return true;
});
check(italicFontStyle).equals(FontStyle.italic);
});
});

group('channelTopicLabelSpan', () {
final channel = eg.stream();

Future<void> prepareWidget(WidgetTester tester, {
required TopicName topic,
}) async {
addTearDown(testBinding.reset);
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
streams: [channel],
subscriptions: [eg.subscription(channel)]));

await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: Builder(builder: (context) => Text.rich(
channelTopicLabelSpan(
context: context,
channelId: channel.streamId,
topic: topic,
fontSize: 17,
color: Colors.black)))));
await tester.pump(); // global store
await tester.pump(); // per-account store
}

Finder findCheckIcon() => find.byWidgetPredicate(
(w) => w is InlineIcon && w.icon == ZulipIcons.check);

testWidgets('resolved topic shows checkmark and strips prefix', (tester) async {
await prepareWidget(tester, topic: eg.t('✔ some topic'));
check(findCheckIcon()).findsOne();
check(find.textContaining('some topic', findRichText: true)).findsOne();
check(find.textContaining('✔', findRichText: true)).findsNothing();
});

testWidgets('unresolved topic shows no checkmark', (tester) async {
await prepareWidget(tester, topic: eg.t('some topic'));
check(findCheckIcon()).findsNothing();
check(find.textContaining('some topic', findRichText: true)).findsOne();
});
});

group('TextWithLink', () {
testWidgets('responds correctly to taps', (tester) async {
int calls = 0;
Expand Down