Skip to content

add emoji picker for reactions #566

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ agpVersion=8.1.4
# A helpful discussion is at:
# https://stackoverflow.com/a/74425347
kotlinVersion=1.9.10

# For compatibility with emoji_picker_flutter, see https://github.com/Fintasys/emoji_picker_flutter/issues/189#issuecomment-1965697752
kotlin.jvm.target.validation.mode=IGNORE
8 changes: 8 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"@permissionsDeniedReadExternalStorage": {
"description": "Message for dialog asking the user to grant permissions for external storage read access."
},
"actionSheetOptionAddReaction": "Add reaction",
"@actionSheetOptionAddReaction": {
"description": "Label for add reaction button on action sheet."
},
"actionSheetOptionCopy": "Copy message text",
"@actionSheetOptionCopy": {
"description": "Label for copy message text button on action sheet."
Expand All @@ -67,6 +71,10 @@
"@errorCouldNotFetchMessageSource": {
"description": "Error message when the source of a message could not be fetched."
},
"errorAddingReactionFailed": "Adding reaction failed",
"@errorAddingReactionFailed": {
"description": "Error message when adding a reaction to a message failed."
},
"errorCopyingFailed": "Copying failed",
"@errorCopyingFailed": {
"description": "Error message when copying the text of a message to the user's system clipboard failed."
Expand Down
2,122 changes: 2,122 additions & 0 deletions lib/emoji.dart

Large diffs are not rendered by default.

104 changes: 68 additions & 36 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
Expand All @@ -6,6 +7,7 @@ import 'package:share_plus/share_plus.dart';
import '../api/exception.dart';
import '../api/model/model.dart';
import '../api/route/messages.dart';
import '../emoji.dart';
import 'clipboard.dart';
import 'compose_box.dart';
import 'dialog.dart';
Expand All @@ -18,26 +20,17 @@ 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 hasThumbsUpReactionVote = message.reactions
?.aggregated.any((reactionWithVotes) =>
reactionWithVotes.reactionType == ReactionType.unicodeEmoji
&& reactionWithVotes.emojiCode == '1f44d'
&& reactionWithVotes.userIds.contains(store.selfUserId))
?? false;

showDraggableScrollableModalBottomSheet(
context: context,
builder: (BuildContext _) {
return Column(children: [
if (!hasThumbsUpReactionVote) AddThumbsUpButton(message: message, messageListContext: context),
AddReactionButton(message: message, messageListContext: context),
StarButton(message: message, messageListContext: context),
ShareButton(message: message, messageListContext: context),
if (isComposeBoxOffered) QuoteAndReplyButton(
Expand Down Expand Up @@ -73,10 +66,8 @@ 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({
class AddReactionButton extends MessageActionSheetMenuItemButton {
AddReactionButton({
super.key,
required super.message,
required super.messageListContext,
Expand All @@ -86,34 +77,75 @@ class AddThumbsUpButton extends MessageActionSheetMenuItemButton {

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

@override get onPressed => (BuildContext context) async {
// dismiss action sheet
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);
}
await showModalBottomSheet(
context: context,
clipBehavior: Clip.hardEdge,
builder: (BuildContext emojiPickerContext) {
return Padding(
// apply bottom padding to handle keyboard opening via https://github.com/flutter/flutter/issues/71418
padding: EdgeInsets.only(bottom: MediaQuery.of(emojiPickerContext).viewInsets.bottom),
child: EmojiPicker(
config: Config(
checkPlatformCompatibility: false,
emojiSet: getEmojiToDisplay(),
// TODO figure out why tests fail without RecentTabBehavior.NONE
categoryViewConfig: const CategoryViewConfig(recentTabBehavior: RecentTabBehavior.NONE)),
onEmojiSelected: (_, Emoji? emoji) async {
if (emoji == null) {
// dismiss emoji picker
Navigator.of(emojiPickerContext).pop();
return;
}
final emojiName = emoji.name;
final emojiCode = getEmojiCode(emoji);
String? errorMessage;
try {
await addReaction(PerAccountStoreWidget.of(messageListContext).connection,
messageId: message.id,
reactionType: ReactionType.unicodeEmoji,
emojiCode: emojiCode,
emojiName: emojiName,
);
if (!emojiPickerContext.mounted) return;
Navigator.of(emojiPickerContext).pop();
} catch (e) {
debugPrint('Error adding reaction: $e');
if (!emojiPickerContext.mounted) return;

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

final zulipLocalizations = ZulipLocalizations.of(messageListContext);
await showErrorDialog(context: emojiPickerContext,
title: zulipLocalizations.errorAddingReactionFailed, message: errorMessage);
}
}));
});
};

/// Returns the emoji set to display in the emoji picker.
List<CategoryEmoji> getEmojiToDisplay() {
final selfUserId = PerAccountStoreWidget.of(messageListContext).selfUserId;
final selfUserUnicodeReactions = message.reactions
?.aggregated.where((reactionWithVotes) =>
reactionWithVotes.reactionType == ReactionType.unicodeEmoji
&& reactionWithVotes.userIds.contains(selfUserId))
.map((reactionWithVotes) => reactionWithVotes.emojiName);
return selfUserUnicodeReactions != null
? filterUnicodeEmojiSet(emojiSet, selfUserUnicodeReactions) : emojiSet;
}
}

class StarButton extends MessageActionSheetMenuItemButton {
Expand Down
4 changes: 4 additions & 0 deletions linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

#include "generated_plugin_registrant.h"

#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) emoji_picker_flutter_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "EmojiPickerFlutterPlugin");
emoji_picker_flutter_plugin_register_with_registrar(emoji_picker_flutter_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
Expand Down
1 change: 1 addition & 0 deletions linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#

list(APPEND FLUTTER_PLUGIN_LIST
emoji_picker_flutter
file_selector_linux
sqlite3_flutter_libs
url_launcher_linux
Expand Down
4 changes: 4 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@ import FlutterMacOS
import Foundation

import device_info_plus
import emoji_picker_flutter
import file_selector_macos
import firebase_core
import firebase_messaging
import flutter_local_notifications
import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
import sqlite3_flutter_libs
import url_launcher_macos

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
64 changes: 64 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.15.0"
emoji_picker_flutter:
dependency: "direct main"
description:
name: emoji_picker_flutter
sha256: "871339250c00dc469b7fdaaec84f4e10ffa435e730a4f3f3fd06ebd5289ea5ad"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
fake_async:
dependency: "direct dev"
description:
Expand Down Expand Up @@ -908,6 +916,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.1"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shelf:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ dependencies:
sqlite3_flutter_libs: ^0.5.13
url_launcher: ^6.1.11
url_launcher_android: ">=6.1.0"
emoji_picker_flutter: ^2.1.0

dev_dependencies:
flutter_driver:
Expand Down
22 changes: 16 additions & 6 deletions test/widgets/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,20 @@ void main() {
(store.connection as FakeApiConnection).prepare(httpStatus: 400, json: fakeResponseJson);
}

group('AddThumbsUpButton', () {
Future<void> tapButton(WidgetTester tester) async {
group('AddReactionButton', () {
Future<void> tapThumbsUpEmoji(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
await tester.pumpAndSettle(); // Wait for emoji picker to appear
await tester.ensureVisible(find.byIcon(Icons.tag_faces));
await tester.tap(find.byIcon(Icons.tag_faces));
await tester.dragUntilVisible(
find.text('👍').hitTestable(),
// TODO use a constant imported from emoji_picker_flutter once that's upstreamed.
find.byKey(const Key('emojiScrollView')),
const Offset(0, -300),
);
await tester.tap(find.text('👍'));
}

testWidgets('success', (WidgetTester tester) async {
Expand All @@ -112,7 +121,7 @@ void main() {

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

check(connection.lastRequest).isA<http.Request>()
Expand All @@ -129,6 +138,7 @@ void main() {
final message = eg.streamMessage();
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;

final connection = store.connection as FakeApiConnection;

Expand All @@ -137,11 +147,11 @@ void main() {
'msg': 'Invalid message(s)',
'result': 'error',
});
await tapButton(tester);
await tapThumbsUpEmoji(tester);
await tester.pump(Duration.zero); // error arrives; error dialog shows

await tester.tap(find.byWidget(checkErrorDialog(tester,
expectedTitle: 'Adding reaction failed',
expectedTitle: zulipLocalizations.errorAddingReactionFailed,
expectedMessage: 'Invalid message(s)')));
});
});
Expand Down
3 changes: 3 additions & 0 deletions windows/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@

#include "generated_plugin_registrant.h"

#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>

void RegisterPlugins(flutter::PluginRegistry* registry) {
EmojiPickerFlutterPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseCorePluginCApiRegisterWithRegistrar(
Expand Down
Loading