Skip to content

Commit

Permalink
msglist: Support adding a thumbs-up reaction
Browse files Browse the repository at this point in the history
Fixes: zulip#125
  • Loading branch information
chrisbobbe authored and gnprice committed Dec 1, 2023
1 parent 12029ee commit 8f43bc6
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 0 deletions.
55 changes: 55 additions & 0 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,27 @@ import 'store.dart';
///
/// Must have a [MessageListPage] ancestor.
void showMessageActionSheet({required BuildContext context, required Message message}) {
final store = PerAccountStoreWidget.of(context);

// The UI that's conditioned on this won't live-update during this appearance
// of the action sheet (we avoid calling composeBoxControllerOf in a build
// method; see its doc). But currently it will be constant through the life of
// any message list, so that's fine.
final isComposeBoxOffered = MessageListPage.composeBoxControllerOf(context) != null;

final selfUserId = store.account.userId;
final hasThumbsUpReactionVote = message.reactions
?.aggregated.any((reactionWithVotes) =>
reactionWithVotes.reactionType == ReactionType.unicodeEmoji
&& reactionWithVotes.emojiCode == '1f44d'
&& reactionWithVotes.userIds.contains(selfUserId))
?? false;

showDraggableScrollableModalBottomSheet(
context: context,
builder: (BuildContext _) {
return Column(children: [
if (!hasThumbsUpReactionVote) AddThumbsUpButton(message: message, messageListContext: context),
ShareButton(message: message, messageListContext: context),
if (isComposeBoxOffered) QuoteAndReplyButton(
message: message,
Expand Down Expand Up @@ -60,6 +72,49 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget {
}
}

// This button is very temporary, to complete #125 before we have a way to
// choose an arbitrary reaction (#388). So, skipping i18n.
class AddThumbsUpButton extends MessageActionSheetMenuItemButton {
AddThumbsUpButton({
super.key,
required super.message,
required super.messageListContext,
});

@override get icon => Icons.add_reaction_outlined;

@override
String label(ZulipLocalizations zulipLocalizations) {
return 'React with 👍'; // TODO(i18n) skip translation for now
}

@override get onPressed => (BuildContext context) async {
Navigator.of(context).pop();
String? errorMessage;
try {
await addReaction(PerAccountStoreWidget.of(messageListContext).connection,
messageId: message.id,
reactionType: ReactionType.unicodeEmoji,
emojiCode: '1f44d',
emojiName: '+1',
);
} catch (e) {
if (!messageListContext.mounted) return;

switch (e) {
case ZulipApiException():
errorMessage = e.message;
// TODO specific messages for common errors, like network errors
// (support with reusable code)
default:
}

await showErrorDialog(context: context,
title: 'Adding reaction failed', message: errorMessage);
}
};
}

class ShareButton extends MessageActionSheetMenuItemButton {
ShareButton({
super.key,
Expand Down
51 changes: 51 additions & 0 deletions test/widgets/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
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/compose.dart';
Expand All @@ -19,6 +20,7 @@ import '../example_data.dart' as eg;
import '../flutter_checks.dart';
import '../model/binding.dart';
import '../model/test_store.dart';
import '../stdlib_checks.dart';
import '../test_clipboard.dart';
import '../test_share_plus.dart';
import 'compose_box_checks.dart';
Expand Down Expand Up @@ -91,6 +93,54 @@ void main() {
(store.connection as FakeApiConnection).prepare(httpStatus: 400, json: fakeResponseJson);
}

group('AddThumbsUpButton', () {
Future<void> tapButton(WidgetTester tester) async {
await tester.ensureVisible(find.byIcon(Icons.add_reaction_outlined, skipOffstage: false));
await tester.tap(find.byIcon(Icons.add_reaction_outlined));
await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
}

testWidgets('success', (WidgetTester tester) async {
final message = eg.streamMessage();
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);

final connection = store.connection as FakeApiConnection;
connection.prepare(json: {});
await tapButton(tester);
await tester.pump(Duration.zero);

check(connection.lastRequest).isA<http.Request>()
..method.equals('POST')
..url.path.equals('/api/v1/messages/${message.id}/reactions')
..bodyFields.deepEquals({
'reaction_type': 'unicode_emoji',
'emoji_code': '1f44d',
'emoji_name': '+1',
});
});

testWidgets('request has an error', (WidgetTester tester) async {
final message = eg.streamMessage();
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);

final connection = store.connection as FakeApiConnection;

connection.prepare(httpStatus: 400, json: {
'code': 'BAD_REQUEST',
'msg': 'Invalid message(s)',
'result': 'error',
});
await tapButton(tester);
await tester.pump(Duration.zero); // error arrives; error dialog shows

await tester.tap(find.byWidget(checkErrorDialog(tester,
expectedTitle: 'Adding reaction failed',
expectedMessage: 'Invalid message(s)')));
});
});

group('ShareButton', () {
// Tests should call this.
MockSharePlus setupMockSharePlus() {
Expand Down Expand Up @@ -169,6 +219,7 @@ void main() {
///
/// Checks that there is a quote-and-reply button.
Future<void> tapQuoteAndReplyButton(WidgetTester tester) async {
await tester.ensureVisible(find.byIcon(Icons.format_quote_outlined, skipOffstage: false));
final quoteAndReplyButton = findQuoteAndReplyButton(tester);
check(quoteAndReplyButton).isNotNull();
await tester.tap(find.byWidget(quoteAndReplyButton!));
Expand Down

0 comments on commit 8f43bc6

Please sign in to comment.