From 829dd7a0d0c97c2ac5aaafd05a6846c3d596f802 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 12 May 2025 14:12:33 +0100 Subject: [PATCH] feat: chat reactions closes #2 --- lib/widgets/chat.dart | 115 +++++++++++++----- lib/widgets/profile_modal.dart | 42 +++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 72 +++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 9 files changed, 214 insertions(+), 29 deletions(-) diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index c80ce23..f845976 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -246,40 +246,97 @@ class _ChatMessageWidget extends StatelessWidget { final profile = state.data ?? Metadata(pubKey: msg.pubKey); return GestureDetector( onLongPress: () { - showModalBottomSheet( - context: context, - constraints: BoxConstraints.expand(), - builder: (ctx) => ProfileModalWidget(profile: profile, event: msg), - ); + if (ndk.accounts.canSign) { + showModalBottomSheet( + context: context, + constraints: BoxConstraints.expand(), + builder: + (ctx) => ProfileModalWidget(profile: profile, event: msg), + ); + } }, - child: RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: AvatarWidget(profile: profile, size: 24), - alignment: PlaceholderAlignment.middle, - ), - TextSpan(text: " "), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: ProfileNameWidget( - profile: profile, - style: TextStyle( - color: - msg.pubKey == stream.info.host - ? PRIMARY_1 - : SECONDARY_1, - ), - ), - ), - TextSpan(text: " "), - TextSpan(text: msg.content, style: TextStyle(color: FONT_COLOR)), - ], - ), + child: Column( + spacing: 2, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _chatText(profile), + RxFilter( + filters: [ + Filter(kinds: [9735, 7], eTags: [msg.id]), + ], + builder: (ctx, data) => _chatReactions(data), + ), + ], ), ); }); } + + Widget _chatText(Metadata profile) { + return RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: AvatarWidget(profile: profile, size: 24), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: " "), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: ProfileNameWidget( + profile: profile, + style: TextStyle( + color: msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1, + ), + ), + ), + TextSpan(text: " "), + TextSpan(text: msg.content, style: TextStyle(color: FONT_COLOR)), + ], + ), + ); + } + + Widget _chatReactions(List? events) { + if ((events?.length ?? 0) == 0) return SizedBox.shrink(); + + final zaps = events! + .where((e) => e.kind == 9735) + .map((e) => ZapReceipt.fromEvent(e)); + final reactions = events.where((e) => e.kind == 7); + + return Row( + spacing: 8, + children: [ + if (zaps.isNotEmpty) + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration(color: LAYER_2, borderRadius: DEFAULT_BR), + child: Text( + formatSats(zaps.fold(0, (acc, v) => acc + (v.amountSats ?? 0))), + ), + ), + if (reactions.isNotEmpty) + ...reactions + .fold(>{}, (acc, v) { + acc[v.content] ??= Set(); + acc[v.content]!.add(v.pubKey); + return acc; + }) + .entries + .map( + (v) => Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: LAYER_2, + borderRadius: DEFAULT_BR, + ), + child: Center(child: Text(v.key)), + ), + ), + ], + ); + } } class _WriteMessageWidget extends StatefulWidget { diff --git a/lib/widgets/profile_modal.dart b/lib/widgets/profile_modal.dart index bda9207..9718efe 100644 --- a/lib/widgets/profile_modal.dart +++ b/lib/widgets/profile_modal.dart @@ -1,5 +1,11 @@ +import 'dart:developer' as developer; + +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/foundation.dart' as foundation; import 'package:flutter/material.dart'; import 'package:ndk/ndk.dart'; +import 'package:zap_stream_flutter/main.dart'; +import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/widgets/button.dart'; import 'package:zap_stream_flutter/widgets/mute_button.dart'; import 'package:zap_stream_flutter/widgets/profile.dart'; @@ -23,6 +29,42 @@ class ProfileModalWidget extends StatelessWidget { spacing: 10, children: [ ProfileWidget(profile: profile), + EmojiPicker( + onEmojiSelected: (category, emoji) { + developer.log(emoji.emoji); + ndk.broadcast.broadcastReaction( + eventId: event.id, + reaction: emoji.emoji, + ); + Navigator.pop(context); + }, + config: Config( + height: 256, + checkPlatformCompatibility: true, + emojiViewConfig: EmojiViewConfig( + emojiSizeMax: + 28 * + (foundation.defaultTargetPlatform == TargetPlatform.iOS + ? 1.20 + : 1.0), + backgroundColor: LAYER_1, + ), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.categoryBar, + middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.searchBar, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: LAYER_2, + buttonColor: PRIMARY_1, + ), + categoryViewConfig: CategoryViewConfig(backgroundColor: LAYER_2), + searchViewConfig: SearchViewConfig( + backgroundColor: LAYER_2, + buttonIconColor: PRIMARY_1, + ), + ), + ), BasicButton.text( "Zap", onTap: () { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 90bf20a..50757f4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,12 +6,16 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include 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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index d9ffb5e..1305530 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_linux flutter_secure_storage_linux objectbox_flutter_libs diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f37b229..f5b647f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,22 +5,26 @@ import FlutterMacOS import Foundation +import emoji_picker_flutter import file_selector_macos import flutter_secure_storage_macos import objectbox_flutter_libs import package_info_plus import path_provider_foundation +import shared_preferences_foundation import sqflite_darwin import url_launcher_macos import video_player_avfoundation import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index d539fef..27d3d6f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.11" + emoji_picker_flutter: + dependency: "direct main" + description: + name: emoji_picker_flutter + sha256: "9a44c102079891ea5877f78c70f2e3c6e9df7b7fe0a01757d31f1046eeaa016d" + url: "https://pub.dev" + source: hosted + version: "4.3.0" equatable: dependency: transitive description: @@ -777,6 +785,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -894,6 +958,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 456a87c..d9c01f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: url_launcher: ^6.3.1 chewie: ^1.11.3 image_picker: ^1.1.2 + emoji_picker_flutter: ^4.3.0 dependency_overrides: ndk: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 753b413..90d6b41 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + EmojiPickerFlutterPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0aa0b23..39eaff0 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + emoji_picker_flutter file_selector_windows flutter_secure_storage_windows objectbox_flutter_libs