Skip to content
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

compose: Enforce max topic/content length by code points, following API #1239

Merged
merged 7 commits into from
Jan 23, 2025

Conversation

chrisbobbe
Copy link
Collaborator

Fixes #1238.

Done by computing String.runes (the number of Unicode code points) unless we know that the result can't exceed the threshold number of code points. In particular, we don't compute it unless String.length (the number of UTF-16 code units) exceeds the threshold number of code points.

Copy link
Member

@PIG208 PIG208 left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! I read all the commits and it looks good to me overall. Left some comments.

@@ -95,6 +95,23 @@ void main() {
..url.path.equals('/api/v1/users/me/${narrow.streamId}/topics');
}

/// Set the content input's text to [content], using [WidgetTester.enterText].
Future<void> enterContent(WidgetTester tester, {
required ChannelNarrow narrow,
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this is unused. For entering content, just having await tester.enterText(contentInputFinder, content); should be fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It gets two callers in this commit, and two more in a later commit.

At the branch tip:

$ git grep enterContent -- test/widgets/compose_box_test.dart
test/widgets/compose_box_test.dart:  Future<void> enterContent(WidgetTester tester, {
test/widgets/compose_box_test.dart:          await enterContent(tester, narrow: narrow, content: content);
test/widgets/compose_box_test.dart:        await enterContent(tester, narrow: narrow, content: 'a' * kMaxMessageLengthCodePoints);
test/widgets/compose_box_test.dart:          await enterContent(tester, narrow: narrow, content: 'some content');
test/widgets/compose_box_test.dart:        await enterContent(tester, narrow: narrow, content: 'some content');

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I like the clear parallelism when it's used alongside enterTopic:

          await enterTopic(tester, narrow: narrow, topic: 'some topic');
          await enterContent(tester, narrow: narrow, content: content);

/// is more expensive than getting the number of UTF-16 code units
/// ([String.length]), so we avoid it when the result definitely won't exceed
/// [maxLengthUnicodeCodePoints].
int? get lengthUnicodeCodePointsIfLong => _lengthUnicodeCodePointsIfLong;
Copy link
Member

Choose a reason for hiding this comment

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

What if we maintain the length internally, and expose only a method that tests if the content is too long? We already have access to the length limit via maxLengthUnicodeCodePoints and hiding this cache might simplify the checks a bit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, makes sense; I can remove this public getter.

and expose only a method that tests if the content is too long

How about keeping the validationErrors getter as the way consumers get this information, without adding a new method?

@chrisbobbe chrisbobbe force-pushed the pr-topic-content-length-code-points branch from e18196d to fe080cb Compare January 17, 2025 22:40
@chrisbobbe
Copy link
Collaborator Author

chrisbobbe commented Jan 17, 2025

Thanks for the review! Revision pushed, this time atop #1290 for the sake of CI.

@PIG208
Copy link
Member

PIG208 commented Jan 17, 2025

Thanks! This revision looks good to me.

@PIG208 PIG208 assigned gnprice and unassigned PIG208 Jan 17, 2025
@PIG208 PIG208 added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Jan 17, 2025
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review!

@chrisbobbe chrisbobbe requested a review from gnprice January 17, 2025 23:18
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks for fixing this! The implementation all looks good to me; small comments on the tests.

@@ -95,6 +96,23 @@ void main() {
..url.path.equals('/api/v1/users/me/${narrow.streamId}/topics');
}

/// Set the content input's text to [content], using [WidgetTester.enterText].
Future<void> enterContent(WidgetTester tester, {
required ChannelNarrow narrow,
Copy link
Member

Choose a reason for hiding this comment

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

This parameter doesn't seem to be used.

(Maybe that's what @PIG208 meant at https://github.com/zulip/zulip-flutter/pull/1239/files#r1901513335 ?)

Copy link
Member

Choose a reason for hiding this comment

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

Oh right. I thought I was referring to the helper as a whole when re-reviewing, but initially the comment was brought up because of this parameter.

Comment on lines 104 to 105
final contentInputFinder = find.byWidgetPredicate(
(widget) => widget is TextField && widget.controller is ComposeContentController);
Copy link
Member

Choose a reason for hiding this comment

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

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

This is equivalent to the finder already defined above, right?

Comment on lines 260 to 261
doTest('too-long content is rejected',
content: makeStringWithCodePoints(kMaxMessageLengthCodePoints + 1), expectError: true);
Copy link
Member

Choose a reason for hiding this comment

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

nit: since this helper creates a test case, find a name for it starting with "test", like test and testWidgets

Copy link
Member

Choose a reason for hiding this comment

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

(But also this is probably clearer if the logic these tests have in common goes into a helper function they each call, rather than having them wrapped like this.)

Comment on lines 261 to 264
content: makeStringWithCodePoints(kMaxMessageLengthCodePoints + 1), expectError: true);

doTest('max-length content not rejected',
content: makeStringWithCodePoints(kMaxMessageLengthCodePoints), expectError: false);
Copy link
Member

Choose a reason for hiding this comment

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

nit: lines too long: the key information at expectError: is past 80 columns

const graphemeCluster = '👨‍👩‍👦';
assert(graphemeCluster.runes.length == 5);
assert(graphemeCluster.length == 8);
assert(graphemeCluster.characters.length == 1);
Copy link
Member

Choose a reason for hiding this comment

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

The fact about .characters isn't really used here, right? This could be any single emoji code point (for any of the emoji of the past decade or so, in the range U+10000 and up) and it'd have more UTF-16 code units than code points.

I guess it is helpful that the tests below confirm that at M+1 code points we start rejecting the value even when it's less than M "characters" / grapheme clusters. So maybe the thing to do is to just add that to this function's doc: the number of characters in that sense is less than [n].

@chrisbobbe chrisbobbe force-pushed the pr-topic-content-length-code-points branch from fe080cb to c2e7a38 Compare January 22, 2025 23:03
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

We have the same comment where we check the content length:

  if (textNormalized.length > kMaxMessageLengthCodePoints)

and it applies here, too; the API doc on max_topic_length in
/register also says it's in Unicode code points:
  https://zulip.com/api/register-queue

And give our max-topic-length variable a more descriptive name,
again like we do with kMaxMessageLengthCodePoints.
With zulip#996, these tests will have to start checking for separate
per-platform flavors of alert dialog. Best if they all do so through
this one codepath.
@gnprice gnprice force-pushed the pr-topic-content-length-code-points branch from c2e7a38 to a23309a Compare January 23, 2025 05:55
@gnprice gnprice merged commit a23309a into zulip:main Jan 23, 2025
@gnprice
Copy link
Member

gnprice commented Jan 23, 2025

Thanks! All looks good; merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

compose: Enforce max topic/content length by Unicode code points, not UTF-16 code units
3 participants