Skip to content

Commit

Permalink
compose: Respect realm setting for mandatory topics
Browse files Browse the repository at this point in the history
Signed-off-by: Zixuan James Li <[email protected]>
  • Loading branch information
PIG208 authored and gnprice committed Jan 30, 2025
1 parent f76b003 commit f1646c4
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 17 deletions.
39 changes: 24 additions & 15 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,14 @@ enum TopicValidationError {
}

class ComposeTopicController extends ComposeController<TopicValidationError> {
ComposeTopicController() {
ComposeTopicController({required this.store}) {
_update();
}

// TODO: subscribe to this value:
// https://zulip.com/help/require-topics
final mandatory = true;
PerAccountStore store;

// TODO(#668): listen to [PerAccountStore] once we subscribe to this value
bool get mandatory => store.realmMandatoryTopics;

// TODO(#307) use `max_topic_length` instead of hardcoded limit
@override final maxLengthUnicodeCodePoints = kMaxTopicLengthCodePoints;
Expand Down Expand Up @@ -1227,7 +1228,10 @@ sealed class ComposeBoxController {
}

class StreamComposeBoxController extends ComposeBoxController {
final topic = ComposeTopicController();
StreamComposeBoxController({required PerAccountStore store})
: topic = ComposeTopicController(store: store);

final ComposeTopicController topic;
final topicFocusNode = FocusNode();

@override
Expand Down Expand Up @@ -1308,16 +1312,20 @@ abstract class ComposeBoxState extends State<ComposeBox> {
ComposeBoxController get controller;
}

class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
@override ComposeBoxController get controller => _controller;
late final ComposeBoxController _controller;
class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateMixin<ComposeBox> implements ComposeBoxState {
@override ComposeBoxController get controller => _controller!;
ComposeBoxController? _controller;

@override
void initState() {
super.initState();
void onNewStore() {
switch (widget.narrow) {
case ChannelNarrow():
_controller = StreamComposeBoxController();
final store = PerAccountStoreWidget.of(context);
if (_controller == null) {
_controller = StreamComposeBoxController(store: store);
} else {
(controller as StreamComposeBoxController).topic.store = store;
}
case TopicNarrow():
case DmNarrow():
_controller = FixedDestinationComposeBoxController();
Expand All @@ -1330,7 +1338,7 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {

@override
void dispose() {
_controller.dispose();
controller.dispose();
super.dispose();
}

Expand Down Expand Up @@ -1370,15 +1378,16 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
return _ComposeBoxContainer(body: null, errorBanner: errorBanner);
}

final controller = this.controller;
final narrow = widget.narrow;
switch (_controller) {
switch (controller) {
case StreamComposeBoxController(): {
narrow as ChannelNarrow;
body = _StreamComposeBoxBody(controller: _controller, narrow: narrow);
body = _StreamComposeBoxBody(controller: controller, narrow: narrow);
}
case FixedDestinationComposeBoxController(): {
narrow as SendableNarrow;
body = _FixedDestinationComposeBoxBody(controller: _controller, narrow: narrow);
body = _FixedDestinationComposeBoxBody(controller: controller, narrow: narrow);
}
}

Expand Down
3 changes: 2 additions & 1 deletion test/model/autocomplete_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,8 @@ void main() {

final description = 'topic-input with text: $markedText produces: ${expectedQuery?.raw ?? 'No Query!'}';
test(description, () {
final controller = ComposeTopicController();
final store = eg.store();
final controller = ComposeTopicController(store: store);
controller.value = parsed.value;
if (expectedQuery == null) {
check(controller).autocompleteIntent.isNull();
Expand Down
59 changes: 58 additions & 1 deletion test/widgets/compose_box_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ void main() {
User? selfUser,
List<User> otherUsers = const [],
List<ZulipStream> streams = const [],
bool? mandatoryTopics,
}) async {
if (narrow case ChannelNarrow(:var streamId) || TopicNarrow(: var streamId)) {
assert(streams.any((stream) => stream.streamId == streamId),
Expand All @@ -54,7 +55,9 @@ void main() {
addTearDown(testBinding.reset);
selfUser ??= eg.selfUser;
final selfAccount = eg.account(user: selfUser);
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot());
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot(
realmMandatoryTopics: mandatoryTopics,
));

store = await testBinding.globalStore.perAccount(selfAccount.id);

Expand Down Expand Up @@ -558,6 +561,60 @@ void main() {
});
});

group('sending to empty topic', () {
late ZulipStream channel;

Future<void> setupAndTapSend(WidgetTester tester, {
required String topicInputText,
required bool mandatoryTopics,
}) async {
TypingNotifier.debugEnable = false;
addTearDown(TypingNotifier.debugReset);

channel = eg.stream();
final narrow = ChannelNarrow(channel.streamId);
await prepareComposeBox(tester,
narrow: narrow, streams: [channel],
mandatoryTopics: mandatoryTopics);

await enterTopic(tester, narrow: narrow, topic: topicInputText);
await tester.enterText(contentInputFinder, 'test content');
await tester.tap(find.byIcon(ZulipIcons.send));
await tester.pump();
}

void checkMessageNotSent(WidgetTester tester) {
check(connection.takeRequests()).isEmpty();
checkErrorDialog(tester,
expectedTitle: 'Message not sent',
expectedMessage: 'Topics are required in this organization.');
}

testWidgets('empty topic -> "(no topic)"', (tester) async {
await setupAndTapSend(tester,
topicInputText: '',
mandatoryTopics: false);
check(connection.lastRequest).isA<http.Request>()
..method.equals('POST')
..url.path.equals('/api/v1/messages')
..bodyFields['topic'].equals('(no topic)');
});

testWidgets('if topics are mandatory, reject empty topic', (tester) async {
await setupAndTapSend(tester,
topicInputText: '',
mandatoryTopics: true);
checkMessageNotSent(tester);
});

testWidgets('if topics are mandatory, reject "(no topic)"', (tester) async {
await setupAndTapSend(tester,
topicInputText: '(no topic)',
mandatoryTopics: true);
checkMessageNotSent(tester);
});
});

group('uploads', () {
void checkAppearsLoading(WidgetTester tester, bool expected) {
final sendButtonElement = tester.element(find.ancestor(
Expand Down

0 comments on commit f1646c4

Please sign in to comment.