-
Notifications
You must be signed in to change notification settings - Fork 306
msglist: Add "Copy message link" button to action sheet #713
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
Conversation
bcec983
to
81b1b8b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! Everything looks good, except for a small nit in the comment below.
test/widgets/action_sheet_test.dart
Outdated
await setupToMessageActionSheet(tester, message: message, narrow: narrow); | ||
|
||
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think both blocks are handling the arrangement steps for the test case so that extra white space feels as an extra
await setupToMessageActionSheet(tester, message: message, narrow: narrow); | |
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); | |
await setupToMessageActionSheet(tester, message: message, narrow: narrow); | |
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); |
81b1b8b
to
b12406b
Compare
Thanks @Khader-1 for the review. Revision pushed. PTAL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! LGTM.
Please be sure to add a still screenshot of visual changes alongside any videos. It is very difficult to review some things on a video (like how the options in the menu look all together, in this case). |
@alya An image has been added, sorry for the inconvenience. Please have a look. Next time I should not increase the GIF speed. |
Thanks! Increased speed may be fine; I'd like to have a still screenshot alongside it regardless, so that it's easy to think about it. |
I would label the option "Copy link to message" to match the web app, unless we feel like that's too long. |
It also seems like it should be next to the "Share" option, so maybe we should move "Share" down to be last? We'll probably want to think about the organization more (referring to the web app and the RN app) as we add more options. |
31aa087
to
f87b14e
Compare
@alya Revision pushed with the changes you suggested. Visuals in the PR comment are also updated. PTAL. |
Works for me, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sm-sayedi Thanks for working on this. This looks great :)
2 minor non blocking comments.
-
It was not super obvious what Share did without going through the issue and reading the context. A more explicit version like "Share message text" could be more useful since now it is placed below "Copy link to message" option. I think some users could confuse Share option to "Share link to message".
-
The icon for Copy message text uses the copy icon but the copy link Icon is that of a URL. There is a mismatch in how we uses the icons. In one case we use the icon to highlight the action(copy) but in the second case it is used to highlight the entity(link) to which action is applied. It would be nice to have a uniform design pattern here.
Thanks @hackerkid for the review! You're right, there is room for further adjustments. I think @gnprice first suggested replacing "Share" with the "Copy link to message" option in zulip-mobile #2623, but I am not sure if it's still the plan. Regarding the icons, the copy icon was already used for the "Copy message text" option, and for the "Copy link to message" option, I just followed the web in using the link icon. Maybe @terpimost has a suggestion about this! |
f87b14e
to
9467fae
Compare
Probably #design is a good place to discuss the icons, for broader visibility. I wouldn't block merging the PR on that, though. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, @sm-sayedi! Small comments below.
lib/widgets/action_sheet.dart
Outdated
copyWithPopup(context: context, | ||
copyWithPopup(context: messageListContext, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
action-sheet: Pass the correct `BuildContext` for showing `SnackBar`
Interesting! Thanks for catching this user-facing bug. I've filed #732 for it, so this commit (and the PR description) can point to it.
It looks like context
is the context for the action sheet, and at this point the action sheet has been dismissed, so it's a bug to try to do anything with context
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to add a regression test for this bug. Reading through test/widgets/clipboard_test.dart, I think this might look something like the following:
--- test/widgets/action_sheet_test.dart
+++ test/widgets/action_sheet_test.dart
@@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:zulip/api/model/model.dart';
import 'package:zulip/api/route/messages.dart';
+import 'package:zulip/model/binding.dart';
import 'package:zulip/model/compose.dart';
import 'package:zulip/model/internal_link.dart';
import 'package:zulip/model/localizations.dart';
@@ -465,6 +466,9 @@ void main() {
}
testWidgets('success', (WidgetTester tester) async {
+ // for #732 regression check below
+ testBinding.deviceInfoResult = IosDeviceInfo(systemVersion: '16.0');
+
final message = eg.streamMessage();
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
@@ -473,6 +477,14 @@ void main() {
await tapCopyMessageTextButton(tester);
await tester.pump(Duration.zero);
check(await Clipboard.getData('text/plain')).isNotNull().text.equals('Hello world');
+
+ // Regression check for #732
+ final snackBar = tester.widget<SnackBar>(find.byType(SnackBar));
+ check(snackBar.behavior).equals(SnackBarBehavior.floating);
+ final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
+ tester.widget(find.descendant(matchRoot: true,
+ of: find.byWidget(snackBar.content),
+ matching: find.text(zulipLocalizations.successMessageCopied)));
});
testWidgets('request has an error', (WidgetTester tester) async {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then in the later commit adding "Copy message link", we can have a similar check, but it doesn't need "regression check" comments because it doesn't have a history of being affected by #732. (It's just a nice check to have, since it's easy to get the code wrong, like with the other button.)
"actionSheetOptionCopy": "Copy message text", | ||
"@actionSheetOptionCopy": { | ||
"actionSheetOptionCopyMessageText": "Copy message text", | ||
"@actionSheetOptionCopyMessageText": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. I think I'd like to rename successMessageCopied
to successMessageTextCopied
, too.
I think it would also be good to make that UI string more parallel to this existing "Copy message text" button. Instead of "Message Copied" (which has the wrong capitalization anyway), it could say "Message text copied".
Changing the user-facing text won't be NFC, so if it goes in this commit, the commit should be unmarked [nfc]
. I don't think I have a preference if that change goes here or in a different commit.
lib/widgets/action_sheet.dart
Outdated
if (!hasThumbsUpReactionVote) AddThumbsUpButton(message: message, messageListContext: context), | ||
StarButton(message: message, messageListContext: context), | ||
ShareButton(message: message, messageListContext: context), | ||
if (isComposeBoxOffered) QuoteAndReplyButton( | ||
message: message, | ||
messageListContext: context, | ||
), | ||
CopyMessageTextButton(message: message, messageListContext: context), | ||
ShareButton(message: message, messageListContext: context), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
action-sheet: Move the "Share" option to the bottom of options
Let's also move the 'ShareButton' group in the tests, so the tests still match the order of the code they're testing.
lib/widgets/action_sheet.dart
Outdated
); | ||
|
||
copyWithPopup(context: messageListContext, | ||
successContent: Text(zulipLocalizations.successLinkCopied), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make a new string in zulipLocalizations
, probably with the name successMessageLinkCopied
, saying "Message link copied", similar to the "Message text copied" from my previous comment. The successLinkCopied
string is meant for a different interaction (long-pressing a link that appears in rendered message content).
test/widgets/action_sheet_test.dart
Outdated
final expectedLink = narrowLink(store, narrow, nearMessageId: message.id).toString(); | ||
await tapCopyMessageLinkButton(tester); | ||
await tester.pump(Duration.zero); | ||
check(await Clipboard.getData('text/plain')).isNotNull().text.equals(expectedLink); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to compute expectedLink
before simulating the button tap? If narrowLink
had any side effects on the store
it's passed, then this wouldn't be quite representative of the interaction we're trying to test. I think narrowLink
doesn't have such side effects (and is unlikely to grow any). But if we call it after the test has finished processing the button tap, then it's just something we don't have to think about.
a5866e0
to
6b309a2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @chrisbobbe for the review! Revision pushed! Added one comment below. PTAL.
@@ -472,6 +476,14 @@ void main() { | |||
await tapCopyMessageTextButton(tester); | |||
await tester.pump(Duration.zero); | |||
check(await Clipboard.getData('text/plain')).isNotNull().text.equals('Hello world'); | |||
|
|||
// regression check for #732 | |||
final snackbar = tester.widget<SnackBar>(find.byType(SnackBar)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test (and the next test for CopyMessageLinkButton
) still passes with the old code where the wrong BuildContext
is passed! In the test environment, that context seems to be mounted even Navigator.of(context).pop()
is called before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh interesting; thanks for noticing this. I'm not sure what could be going on there. I'm pretty sure I would have tried this when I suggested the regression test in #713 (comment) . One thing I'm curious about: does #732 still reproduce with the old code when you run the app?
I guess it's possible that some other change happened that ended up fixing it.
…aha! On version 0.0.16 on iOS, #732 actually doesn't reproduce anymore! So I guess that's what happened.
We still want to pass the correct BuildContext
, though, since it's reasonable for it to be unmounted after the action sheet is popped, and it would be a bug to do things with it after it's unmounted. But this should go in its own commit, without the Fixes
line. An earlier commit can add the regression check, and also leave out the Fixes
line, and just say that the bug seems to have been fixed at some point after we discovered it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I'm curious about: does #732 still reproduce with the old code when you run the app?
Yeah, the bug is still there when we run the app on a real device (checked on my Android 12).
On version 0.0.16 on iOS, #732 actually doesn't reproduce anymore!
It is reproduced on Android devices with sdkInt <= 32
. If we put some logging when testing with a real device, the BuildContext
is unmounted, but with tests, it is still mounted. So the bug still exists, it seems that the BuildContext
popping mechanism is kind of weird in the testing environment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah OK; thanks for checking that.
Are you seeing the bug with both the "Copy message text" button and the "Copy link to message" button? One difference is that the former waits for a network request between the .pop()
and the copyWithPopup
. I wonder if the context is unmounted asynchronously, some duration after the .pop()
, and that duration is racing with the network request. (Sometimes one finishes first, sometimes the other.)
I think this might be what's happening. On my iPhone, if I edit the "Copy message text" code to remove the await fetchRawContentWithFeedback
(hard-coding rawContent
to some string instead), then I replace it with await Future.delayed(Duration(milliseconds: n));
, the bug reproduces when n
is large (like 1000) but not when n
is small (like 10). When it reproduces, context.mounted
is false, as you've seen too; when it doesn't reproduce, it's true.
If this seems right to you, then I think the fix is to make the test simulate the network request taking much longer than the unmounting that it's racing against. I'm having trouble getting this to work properly, but it should be possible, and Greg may be able to help if you have trouble with it too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, following that hypothesis, here's a (hacky) diff that succeeds in reproducing the issue after git checkout @~~~ -- lib
to get the code from before the bugfix, but passes after the bugfix:
diff --git test/api/fake_api.dart test/api/fake_api.dart
index 1cbbffcdc..5e4411133 100644
--- test/api/fake_api.dart
+++ test/api/fake_api.dart
@@ -94,7 +94,7 @@ class FakeHttpClient extends http.BaseClient {
return Future(() => throw exception);
case _PreparedSuccess(:var bytes, :var httpStatus):
final byteStream = http.ByteStream.fromBytes(bytes);
- return Future(() => http.StreamedResponse(
+ return Future.delayed(Duration(milliseconds: 1000), () => http.StreamedResponse(
byteStream, httpStatus, request: request));
}
}
diff --git test/widgets/action_sheet_test.dart test/widgets/action_sheet_test.dart
index db7852627..b54fa947d 100644
--- test/widgets/action_sheet_test.dart
+++ test/widgets/action_sheet_test.dart
@@ -411,7 +411,9 @@ void main() {
prepareRawContentResponseSuccess(store, message: message, rawContent: 'Hello world');
await tapCopyMessageTextButton(tester);
- await tester.pump(Duration.zero);
+ for (int i = 0; i < 11; i++) {
+ await tester.pump(const Duration(milliseconds: 100));
+ }
check(await Clipboard.getData('text/plain')).isNotNull().text.equals('Hello world');
// regression check for #732
I'll see about wrapping the fake_api.dart
side of that into an option you can pass to FakeApiConnection.prepare
.
Then @sm-sayedi please experiment and try to simplify those magic numbers of 1000ms, 100ms, 11 iterations — don't need to spend a ton of time doing so, but if we can it'd be nice to reduce it to something that sheds a bit more light on just how this gets triggered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I pushed a draft commit to the tip of this PR:
571df4b wip fake_api [nfc]: Add delay
option to connection.prepare; TODO test
I'll need to write a test for that test code, and then can send it as a PR. In the meantime the only thing it's missing is such a test, so @sm-sayedi you can build atop that and plan to rebase atop the finished version once it lands.
With that commit in place, a somewhat less hacky diff that reproduces the issue is:
diff --git test/widgets/action_sheet_test.dart test/widgets/action_sheet_test.dart
index db7852627..ea91f632e 100644
--- test/widgets/action_sheet_test.dart
+++ test/widgets/action_sheet_test.dart
@@ -90,7 +90,7 @@ void main() {
// Prepare fetch-raw-Markdown response
// TODO: Message should really only differ from `message`
// in its content / content_type, not in `id` or anything else.
- (store.connection as FakeApiConnection).prepare(json:
+ (store.connection as FakeApiConnection).prepare(delay: Duration(seconds: 1), json:
GetMessageResult(message: eg.streamMessage(contentMarkdown: rawContent)).toJson());
}
@@ -411,7 +411,9 @@ void main() {
prepareRawContentResponseSuccess(store, message: message, rawContent: 'Hello world');
await tapCopyMessageTextButton(tester);
- await tester.pump(Duration.zero);
+ for (int i = 0; i < 11; i++) {
+ await tester.pump(const Duration(milliseconds: 100));
+ }
check(await Clipboard.getData('text/plain')).isNotNull().text.equals('Hello world');
// regression check for #732
Then the prepareRawContentResponseSuccess
should take an optional duration to configure that behavior; I'll leave that for you to do as part of writing the test for the bugfix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sent that delay
feature as #770.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you seeing the bug with both the "Copy message text" button and the "Copy link to message" button?
Yeah, for the "Copy link to message" button the bug is present both in manual and widget testing. As you explained in #713 comment, .pop()
takes a small amount of time to finish. When testing on my phone, it took around 20 milliseconds for the context to be unmounted.
For the "Copy message text" button the bug is only present in widget testing (which is resolved in the latest revision). In manual testing, await fetchRawContentWithFeedback
gives the BuildContext
enough time to be unmounted.
Thanks! I replied above (#713 (comment) ) with thoughts on #732, otherwise LGTM. |
6b309a2
to
88f4042
Compare
Marking for Greg's review, as I think there may be a way forward regarding #732, see: #713 (comment) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sm-sayedi, and thanks everyone for the previous reviews! This code all looks great modulo one small comment below.
We'll also want to resolve the subthread above, to get a working test for that bug fix. The hypothesis at #713 (comment) makes sense to me. I'll see if I can quick whip up a way to make a FakeApiConnection request take a long (simulated) time to complete.
test/widgets/action_sheet_test.dart
Outdated
await tester.pump(Duration.zero); | ||
check(await Clipboard.getData('text/plain')).isNotNull().text.equals('Hello world'); | ||
|
||
// regression check for #732 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's split this into its own test case (its own separate testWidgets
call).
That will duplicate a few lines from this test case, but that's OK. It'll help make both test cases easier to read and to understand what they're saying, by telling one story at a time instead of telling two stories at once.
This allows simulating a request that takes some time to return and consequently races with something else. For example here: zulip#713 (comment) This is NFC because `Future.delayed(Duration.zero, f)` is exactly equivalent to `Future(f)`. (The docs aren't real clear on this; but reading the implementations confirms they boil down to the very same `Timer` constructor call, and then the docs do seem to be trying to say that when read in light of that information.)
This allows simulating a request that takes some time to return and consequently races with something else. For example here: zulip#713 (comment) This is NFC because `Future.delayed(Duration.zero, f)` is exactly equivalent to `Future(f)`. (The docs aren't real clear on this; but reading the implementations confirms they boil down to the very same `Timer` constructor call, and then the docs do seem to be trying to say that when read in light of that information.)
571df4b
to
26c6977
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @chrisbobbe and @gnprice for the reviews. Revision pushed. PTAL.
One comment in the sub-thread above and a few below.
test/widgets/action_sheet_test.dart
Outdated
check(await Clipboard.getData('text/plain')).isNotNull().text.equals(expectedLink); | ||
}); | ||
|
||
testWidgets('copies message link to clipboard with a snackbar', (tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't able to make this test not pass when the wrong context was passed. The approach that you suggested for the "Copy message text" button doesn't quite apply here as we don't await for any operation as we do in "Copy message text" (await fetchRawContentWithFeedback
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think the lack of such an await
means that if this "Copy message link" button were to pass the wrong context to copyWithPopup
, it would be only a latent bug and not a live one.
That is, the outward behavior would still be completely correct, and it'd be a bug only in the sense that the code would be misleading and would be susceptible to starting to do the wrong thing in the future as a result of otherwise-innocent changes. Like introducing a step with an await
.
It'd still be good to have a test that would catch such a breakage. After all, the main purpose of a test is precisely to catch when an innocent-seeming change accidentally breaks something.
But I don't see a way to write such a test that I'd actually expect to successfully break in that case. It'd involve somehow causing any futures that the button's onPress
method awaited to take a long (simulated) time to complete, while the navigation animation pressed on ahead… and I don't see a way to target those delays like that, given that there isn't currently anything it actually does await on and there's no way to predict what (if anything) it might someday in the future grow a need to await on.
So it's fine to not have that form of test for this new button.
test/widgets/action_sheet_test.dart
Outdated
for (int i = 0; i < 5; i++) { | ||
await tester.pump(const Duration(milliseconds: 100)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (int i = 0; i < 5; i++) { | |
await tester.pump(const Duration(milliseconds: 100)); | |
} | |
await tester.pump(const Duration(milliseconds: 500)); |
I wonder why this line doesn't behave the same!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first version will advance the simulated clock by 100ms, then render a frame; then advance another 100ms, then render another frame; and so on 5 times.
The second version will advance the simulated clock by 500ms, and then render a frame.
So the first version renders four frames before the 500ms mark, in addition to one frame at it. Each of those frames will rebuild widgets that need rebuilding, which may add and remove things from the tree. If one of those frames causes the action sheet's context to get removed from the tree, then that'd do it.
26c6977
to
8987c50
Compare
The reason for this renaming is to have a clear distinction between the existing "copy message text" and the upcoming "copy message link" buttons.
Before this, the `SnackBar` wouldn't show as the correct `BuildContext` was not passed to `copyWithPopup` method. Fixes: zulip#732
This is because in the next commit(s) the "Copy link to message" button will be added and we want to make it next to the "Share" button at the bottom of the options.
8987c50
to
22c0f3e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sm-sayedi for the revision! Looks good; merging, with two adjustments:
As described below, one of these test cases can be left out. I revised the commit to do that.
I also added a new commit with some small clarifications:
aeb6a94 action-sheet test: Simplify and explain a bit more the #732 repro test
including clearly labeling that whole test case as a regression test for #732, and taking material from #713 (comment) . Please take a look at those changes.
test/widgets/action_sheet_test.dart
Outdated
final expectedLink = narrowLink(store, narrow, nearMessageId: message.id).toString(); | ||
check(await Clipboard.getData('text/plain')).isNotNull().text.equals(expectedLink); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is redundant with the same check in the test case above it. Best to leave it out, so that each test is focused on one question and it's therefore easier to understand what the test is about.
test/widgets/action_sheet_test.dart
Outdated
final snackbar = tester.widget<SnackBar>(find.byType(SnackBar)); | ||
check(snackbar.behavior).equals(SnackBarBehavior.floating); | ||
final zulipLocalizations = GlobalLocalizations.zulipLocalizations; | ||
tester.widget(find.descendant(matchRoot: true, | ||
of: find.byWidget(snackbar.content), | ||
matching: find.text(zulipLocalizations.successMessageLinkCopied))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the discussion above at #713 (comment) , I think there isn't really anything this test case is testing that isn't covered by the tests for copyWithPopup
.
In the regression test above, we're testing that the button calls copyWithPopup
in a right way, rather than a wrong way. But this test currently doesn't really have a way to distinguish a wrong vs. a right way of calling that function. So at most it's testing that we do call copyWithPopup
. I think that isn't worth the cost of having this test here, and having to reread and try to understand it when changing things. (Possibly it could be worth the cost if we found a really concise, simple way of expressing this test.) So let's just leave this test case out.
A new option is added to the long press action sheet of a message to copy the message link to the clipboard.
Here's an illustration:
Fixes: #673
Fixes: #732