Skip to content

Commit 00d42b4

Browse files
committed
msglist: Support adding a thumbs-up reaction
Fixes: zulip#125
1 parent 4c5abf1 commit 00d42b4

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed

lib/widgets/action_sheet.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,27 @@ import 'store.dart';
1717
///
1818
/// Must have a [MessageListPage] ancestor.
1919
void showMessageActionSheet({required BuildContext context, required Message message}) {
20+
final store = PerAccountStoreWidget.of(context);
21+
2022
// The UI that's conditioned on this won't live-update during this appearance
2123
// of the action sheet (we avoid calling composeBoxControllerOf in a build
2224
// method; see its doc). But currently it will be constant through the life of
2325
// any message list, so that's fine.
2426
final isComposeBoxOffered = MessageListPage.composeBoxControllerOf(context) != null;
27+
28+
final selfUserId = store.account.userId;
29+
final hasThumbsUpReactionVote = message.reactions
30+
?.aggregated.any((reactionWithVotes) =>
31+
reactionWithVotes.reactionType == ReactionType.unicodeEmoji
32+
&& reactionWithVotes.emojiCode == '1f44d'
33+
&& reactionWithVotes.userIds.contains(selfUserId))
34+
?? false;
35+
2536
showDraggableScrollableModalBottomSheet(
2637
context: context,
2738
builder: (BuildContext _) {
2839
return Column(children: [
40+
if (!hasThumbsUpReactionVote) AddThumbsUpButton(message: message, messageListContext: context),
2941
ShareButton(message: message, messageListContext: context),
3042
if (isComposeBoxOffered) QuoteAndReplyButton(
3143
message: message,
@@ -60,6 +72,49 @@ abstract class MessageActionSheetMenuItemButton extends StatelessWidget {
6072
}
6173
}
6274

75+
// This button is very temporary, to complete #125 before we have a way to
76+
// choose an arbitrary reaction (#388). So, skipping tests and i18n.
77+
class AddThumbsUpButton extends MessageActionSheetMenuItemButton {
78+
AddThumbsUpButton({
79+
super.key,
80+
required super.message,
81+
required super.messageListContext,
82+
});
83+
84+
@override get icon => Icons.add_reaction_outlined;
85+
86+
@override
87+
String label(ZulipLocalizations zulipLocalizations) {
88+
return 'React with 👍'; // TODO(i18n) skip translation for now
89+
}
90+
91+
@override get onPressed => (BuildContext context) async {
92+
Navigator.of(context).pop();
93+
String? errorMessage;
94+
try {
95+
await addReaction(PerAccountStoreWidget.of(messageListContext).connection,
96+
messageId: message.id,
97+
reactionType: ReactionType.unicodeEmoji,
98+
emojiCode: '1f44d',
99+
emojiName: '+1',
100+
);
101+
} catch (e) {
102+
if (!messageListContext.mounted) return;
103+
104+
switch (e) {
105+
case ZulipApiException():
106+
errorMessage = e.message;
107+
// TODO specific messages for common errors, like network errors
108+
// (support with reusable code)
109+
default:
110+
}
111+
112+
await showErrorDialog(context: context,
113+
title: 'Adding reaction failed', message: errorMessage);
114+
}
115+
};
116+
}
117+
63118
class ShareButton extends MessageActionSheetMenuItemButton {
64119
ShareButton({
65120
super.key,

test/widgets/action_sheet_test.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
2+
13
import 'package:checks/checks.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter/services.dart';
46
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
57
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:http/http.dart' as http;
69
import 'package:zulip/api/model/model.dart';
710
import 'package:zulip/api/route/messages.dart';
811
import 'package:zulip/model/compose.dart';
@@ -19,6 +22,7 @@ import '../example_data.dart' as eg;
1922
import '../flutter_checks.dart';
2023
import '../model/binding.dart';
2124
import '../model/test_store.dart';
25+
import '../stdlib_checks.dart';
2226
import '../test_clipboard.dart';
2327
import '../test_share_plus.dart';
2428
import 'compose_box_checks.dart';
@@ -90,6 +94,63 @@ void main() {
9094
(store.connection as FakeApiConnection).prepare(httpStatus: 400, json: fakeResponseJson);
9195
}
9296

97+
group('AddThumbsUpButton', () {
98+
setUp(() async {
99+
TestZulipBinding.ensureInitialized();
100+
TestWidgetsFlutterBinding.ensureInitialized();
101+
});
102+
103+
tearDown(() async {
104+
testBinding.reset();
105+
});
106+
107+
Future<void> tapButton(WidgetTester tester) async {
108+
await tester.ensureVisible(find.byIcon(Icons.add_reaction_outlined, skipOffstage: false));
109+
await tester.tap(find.byIcon(Icons.add_reaction_outlined));
110+
await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
111+
}
112+
113+
testWidgets('success', (WidgetTester tester) async {
114+
final message = eg.streamMessage();
115+
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
116+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
117+
118+
final connection = store.connection as FakeApiConnection;
119+
connection.prepare(json: {});
120+
await tapButton(tester);
121+
await tester.pump(Duration.zero);
122+
123+
check(connection.lastRequest).isA<http.Request>()
124+
..method.equals('POST')
125+
..url.path.equals('/api/v1/messages/${message.id}/reactions')
126+
..bodyFields.deepEquals({
127+
'reaction_type': 'unicode_emoji',
128+
'emoji_code': '1f44d',
129+
'emoji_name': '+1',
130+
});
131+
});
132+
133+
testWidgets('request has an error', (WidgetTester tester) async {
134+
final message = eg.streamMessage();
135+
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
136+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
137+
138+
final connection = store.connection as FakeApiConnection;
139+
140+
connection.prepare(httpStatus: 400, json: {
141+
'code': 'BAD_REQUEST',
142+
'msg': 'Invalid message(s)',
143+
'result': 'error',
144+
});
145+
await tapButton(tester);
146+
await tester.pump(Duration.zero); // error arrives; error dialog shows
147+
148+
await tester.tap(find.byWidget(checkErrorDialog(tester,
149+
expectedTitle: 'Adding reaction failed',
150+
expectedMessage: 'Invalid message(s)')));
151+
});
152+
});
153+
93154
group('ShareButton', () {
94155
// Tests should call setupMockSharePlus.
95156
setUp(() async {
@@ -177,6 +238,7 @@ void main() {
177238
///
178239
/// Checks that there is a quote-and-reply button.
179240
Future<void> tapQuoteAndReplyButton(WidgetTester tester) async {
241+
await tester.ensureVisible(find.byIcon(Icons.format_quote_outlined, skipOffstage: false));
180242
final quoteAndReplyButton = findQuoteAndReplyButton(tester);
181243
check(quoteAndReplyButton).isNotNull();
182244
await tester.tap(find.byWidget(quoteAndReplyButton!));

0 commit comments

Comments
 (0)