Skip to content
22 changes: 9 additions & 13 deletions lib/widgets/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -501,20 +501,12 @@ class TopicAutocomplete extends AutocompleteField<TopicAutocompleteQuery, TopicA

final designVariables = DesignVariables.of(context);

TextStyle style = TextStyle(
final style = TextStyle(
fontSize: 17,
height: 20 / 17,
color: designVariables.contextMenuItemLabel,
).merge(weightVariableTextStyle(context, wght: 500));

final String text;
if (option.topic.displayName == null) {
final store = PerAccountStoreWidget.of(context);
text = store.realmEmptyTopicDisplayName;
style = style.copyWith(fontStyle: FontStyle.italic);
} else {
text = option.topic.displayName!;
}

return InkWell(
onTap: () {
_onTapOption(context, option);
Expand All @@ -529,10 +521,14 @@ class TopicAutocomplete extends AutocompleteField<TopicAutocompleteQuery, TopicA
padding: const EdgeInsetsDirectional.fromSTEB(12, 2, 10, 2),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
child: Text.rich(
topicLabelSpan(
context: context,
topic: option.topic,
fontSize: style.fontSize!,
color: style.color!),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: style,
text)))));
style: style)))));
}
}
28 changes: 18 additions & 10 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -616,16 +616,21 @@ class MessageListAppBarTitle extends StatelessWidget {
}) {
final store = PerAccountStoreWidget.of(context);
final designVariables = DesignVariables.of(context);
// (We customize titleTextStyle for Zulip; see zulipThemeData.)
final titleTextStyle = Theme.of(context).appBarTheme.titleTextStyle!;
final icon = stream == null ? null
: iconDataForTopicVisibilityPolicy(
store.topicVisibilityPolicy(stream.streamId, topic));
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: Text(topic.displayName ?? store.realmEmptyTopicDisplayName, style: TextStyle(
fontSize: 13,
fontStyle: topic.displayName == null ? FontStyle.italic : null,
).merge(weightVariableTextStyle(context)))),
Flexible(child: Text.rich(
topicLabelSpan(
context: context,
topic: topic,
fontSize: 13,
color: titleTextStyle.color!),
style: TextStyle(fontSize: 13).merge(weightVariableTextStyle(context)))),
if (icon != null)
Padding(
padding: const EdgeInsetsDirectional.only(start: 4),
Expand Down Expand Up @@ -1906,18 +1911,22 @@ class StreamMessageRecipientHeader extends StatelessWidget {
]));
}

final topicStyle = recipientHeaderTextStyle(context);
final topicWidget = Padding(
padding: const EdgeInsets.symmetric(vertical: 11),
child: Row(
children: [
Flexible(
child: Text(topic.displayName ?? store.realmEmptyTopicDisplayName,
child: Text.rich(
topicLabelSpan(
context: context,
topic: topic,
fontSize: topicStyle.fontSize!,
color: topicStyle.color!),
// TODO: Give a way to see the whole topic (maybe a
// long-press interaction?)
overflow: TextOverflow.ellipsis,
style: recipientHeaderTextStyle(context,
fontStyle: topic.displayName == null ? FontStyle.italic : null,
))),
style: topicStyle)),
const SizedBox(width: 4),
Icon(size: 14, color: designVariables.title.withFadedAlpha(0.5),
// A null [Icon.icon] makes a blank space.
Expand Down Expand Up @@ -2015,13 +2024,12 @@ class DmRecipientHeader extends StatelessWidget {
}
}

TextStyle recipientHeaderTextStyle(BuildContext context, {FontStyle? fontStyle}) {
TextStyle recipientHeaderTextStyle(BuildContext context) {
return TextStyle(
color: DesignVariables.of(context).title,
fontSize: 16,
letterSpacing: proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
height: (18 / 16),
fontStyle: fontStyle,
).merge(weightVariableTextStyle(context, wght: 600));
}

Expand Down
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),
],
]);
}
9 changes: 6 additions & 3 deletions test/widgets/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -875,8 +875,9 @@ void main() {

final topicRow = find.descendant(
of: find.byType(ZulipAppBar),
matching: find.text(
effectiveTopic.displayName ?? eg.defaultRealmEmptyTopicDisplayName));
matching: find.textContaining(
effectiveTopic.unresolve().displayName ?? eg.defaultRealmEmptyTopicDisplayName,
findRichText: true));
await tester.longPress(topicRow);
// sheet appears onscreen; default duration of bottom-sheet enter animation
await tester.pump(const Duration(milliseconds: 250));
Expand All @@ -896,7 +897,9 @@ void main() {

await tester.longPress(find.descendant(
of: find.byType(RecipientHeader),
matching: find.text(effectiveMessage.topic.displayName!)));
matching: find.textContaining(
effectiveMessage.topic.unresolve().displayName!,
findRichText: true)));
// sheet appears onscreen; default duration of bottom-sheet enter animation
await tester.pump(const Duration(milliseconds: 250));
}
Expand Down
20 changes: 20 additions & 0 deletions test/widgets/autocomplete_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:zulip/widgets/compose_box.dart';
import 'package:zulip/widgets/icons.dart';
import 'package:zulip/widgets/image.dart';
import 'package:zulip/widgets/message_list.dart';
import 'package:zulip/widgets/text.dart';
import 'package:zulip/widgets/user.dart';

import '../api/fake_api.dart';
Expand Down Expand Up @@ -690,6 +691,25 @@ void main() {
check(find.text('general chat')).findsOne();
});

testWidgets('show resolved-topic check icon', (tester) async {
final topic = eg.getChannelTopicsEntry(name: '✔ some topic');
final topicInputFinder = await setupToTopicInput(tester, topics: [topic]);

// TODO(#226): Remove this extra edit when this bug is fixed.
await tester.enterText(topicInputFinder, 'some');
await tester.enterText(topicInputFinder, 'some t');
await tester.pumpAndSettle();

final itemFinder = find.ancestor(
of: find.textContaining('some topic', findRichText: true),
matching: find.byType(InkWell));
check(find.descendant(of: itemFinder,
matching: find.byWidgetPredicate(
(w) => w is InlineIcon && w.icon == ZulipIcons.check))).findsOne();
check(find.descendant(of: itemFinder,
matching: find.textContaining('✔', findRichText: true))).findsNothing();
});

testWidgets('autocomplete to realmEmptyTopicDisplayName sets topic to empty string', (tester) async {
final topic = eg.getChannelTopicsEntry(name: '');
final topicInputFinder = await setupToTopicInput(tester, topics: [topic],
Expand Down
34 changes: 34 additions & 0 deletions test/widgets/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import 'package:zulip/widgets/image.dart';
import 'package:zulip/widgets/message_list.dart';
import 'package:zulip/widgets/page.dart';
import 'package:zulip/widgets/store.dart';
import 'package:zulip/widgets/text.dart';
import 'package:zulip/widgets/channel_colors.dart';
import 'package:zulip/widgets/theme.dart';
import 'package:zulip/widgets/topic_list.dart';
Expand Down Expand Up @@ -251,6 +252,22 @@ void main() {
channel.name, eg.defaultRealmEmptyTopicDisplayName);
});

testWidgets('show resolved-topic check icon in topic narrow', (tester) async {
final channel = eg.stream();
await setupMessageListPage(tester,
narrow: eg.topicNarrow(channel.streamId, '✔ some topic'),
subscriptions: [eg.subscription(channel)],
messages: [eg.streamMessage(stream: channel, topic: '✔ some topic')]);
final appBarFinder = find.byType(MessageListAppBarTitle);
check(find.descendant(of: appBarFinder,
matching: find.byWidgetPredicate(
(w) => w is InlineIcon && w.icon == ZulipIcons.check))).findsOne();
check(find.descendant(of: appBarFinder,
matching: find.textContaining('some topic', findRichText: true))).findsOne();
check(find.descendant(of: appBarFinder,
matching: find.textContaining('✔', findRichText: true))).findsNothing();
});

void testChannelIconInChannelRow(IconData expectedIcon, {
required bool isWebPublic,
required bool inviteOnly,
Expand Down Expand Up @@ -1799,6 +1816,23 @@ void main() {
check(findInMessageList(eg.defaultRealmEmptyTopicDisplayName)).single;
});

testWidgets('show resolved-topic check icon', (tester) async {
final message = eg.streamMessage(
stream: stream, topic: '✔ some topic');
await setupMessageListPage(tester,
narrow: const CombinedFeedNarrow(),
messages: [message], subscriptions: [eg.subscription(stream)]);
await tester.pump();
final headerFinder = find.byType(StreamMessageRecipientHeader);
check(find.descendant(of: headerFinder,
matching: find.byWidgetPredicate(
(w) => w is InlineIcon && w.icon == ZulipIcons.check))).findsOne();
check(find.descendant(of: headerFinder,
matching: find.textContaining('some topic', findRichText: true))).findsOne();
check(find.descendant(of: headerFinder,
matching: find.textContaining('✔', findRichText: true))).findsNothing();
});

testWidgets('show topic visibility icon when followed', (tester) async {
await setupMessageListPage(tester,
narrow: const CombinedFeedNarrow(),
Expand Down
Loading