✨ Stickers hint
This commit is contained in:
		| @@ -71,6 +71,8 @@ PODS: | ||||
|     - GoogleUtilities/UserDefaults (~> 7.8) | ||||
|     - nanopb (< 2.30911.0, >= 2.30908.0) | ||||
|   - Flutter (1.0.0) | ||||
|   - flutter_keyboard_visibility (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_secure_storage (6.0.0): | ||||
|     - Flutter | ||||
|   - flutter_webrtc (0.11.3): | ||||
| @@ -136,6 +138,8 @@ PODS: | ||||
|     - FlutterMacOS | ||||
|   - permission_handler_apple (9.3.0): | ||||
|     - Flutter | ||||
|   - pointer_interceptor_ios (0.0.1): | ||||
|     - Flutter | ||||
|   - PromisesObjC (2.4.0) | ||||
|   - protocol_handler_ios (0.0.1): | ||||
|     - Flutter | ||||
| @@ -174,6 +178,7 @@ DEPENDENCIES: | ||||
|   - firebase_core (from `.symlinks/plugins/firebase_core/ios`) | ||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) | ||||
|   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) | ||||
|   - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) | ||||
|   - gal (from `.symlinks/plugins/gal/darwin`) | ||||
| @@ -187,6 +192,7 @@ DEPENDENCIES: | ||||
|   - pasteboard (from `.symlinks/plugins/pasteboard/ios`) | ||||
|   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) | ||||
|   - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) | ||||
|   - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) | ||||
|   - protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`) | ||||
|   - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) | ||||
|   - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) | ||||
| @@ -229,6 +235,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/firebase_messaging/ios" | ||||
|   Flutter: | ||||
|     :path: Flutter | ||||
|   flutter_keyboard_visibility: | ||||
|     :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" | ||||
|   flutter_secure_storage: | ||||
|     :path: ".symlinks/plugins/flutter_secure_storage/ios" | ||||
|   flutter_webrtc: | ||||
| @@ -255,6 +263,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/path_provider_foundation/darwin" | ||||
|   permission_handler_apple: | ||||
|     :path: ".symlinks/plugins/permission_handler_apple/ios" | ||||
|   pointer_interceptor_ios: | ||||
|     :path: ".symlinks/plugins/pointer_interceptor_ios/ios" | ||||
|   protocol_handler_ios: | ||||
|     :path: ".symlinks/plugins/protocol_handler_ios/ios" | ||||
|   screen_brightness_ios: | ||||
| @@ -288,6 +298,7 @@ SPEC CHECKSUMS: | ||||
|   FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd | ||||
|   FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 | ||||
|   flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 | ||||
|   flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f | ||||
|   gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 | ||||
| @@ -304,6 +315,7 @@ SPEC CHECKSUMS: | ||||
|   pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 | ||||
|   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 | ||||
|   permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 | ||||
|   pointer_interceptor_ios: 508241697ff0947f853c061945a8b822463947c1 | ||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||
|   protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990 | ||||
|   screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/models/account.dart'; | ||||
| import 'package:solian/models/attachment.dart'; | ||||
| import 'package:solian/services.dart'; | ||||
|  | ||||
| class Sticker { | ||||
|   int id; | ||||
| @@ -30,6 +32,14 @@ class Sticker { | ||||
|     required this.account, | ||||
|   }); | ||||
|  | ||||
|   String get textPlaceholder => '${pack?.prefix}$alias'.camelCase!; | ||||
|   String get textWarpedPlaceholder => ':$textPlaceholder:'; | ||||
|  | ||||
|   String get imageUrl => ServiceFinder.buildUrl( | ||||
|         'files', | ||||
|         '/attachments/$attachmentId', | ||||
|       ); | ||||
|  | ||||
|   factory Sticker.fromJson(Map<String, dynamic> json) => Sticker( | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import 'package:solian/services.dart'; | ||||
|  | ||||
| class StickerProvider extends GetxController { | ||||
|   final RxMap<String, String> aliasImageMapping = RxMap(); | ||||
|   final RxMap<String, List<Sticker>> availableStickers = RxMap(); | ||||
|   final RxList<Sticker> availableStickers = RxList.empty(growable: true); | ||||
|  | ||||
|   Future<void> refreshAvailableStickers() async { | ||||
|     final client = ServiceFinder.configureClient('files'); | ||||
| @@ -20,16 +20,9 @@ class StickerProvider extends GetxController { | ||||
|       for (final pack in out) { | ||||
|         for (final sticker in (pack.stickers ?? List<Sticker>.empty())) { | ||||
|           sticker.pack = pack; | ||||
|           final imageUrl = ServiceFinder.buildUrl( | ||||
|             'files', | ||||
|             '/attachments/${sticker.attachmentId}', | ||||
|           ); | ||||
|           aliasImageMapping['${pack.prefix}${sticker.alias}'.camelCase!] = | ||||
|               imageUrl; | ||||
|           if (availableStickers[pack.prefix] == null) { | ||||
|             availableStickers[pack.prefix] = List.empty(growable: true); | ||||
|           } | ||||
|           availableStickers[pack.prefix]!.add(sticker); | ||||
|               sticker.imageUrl; | ||||
|           availableStickers.add(sticker); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -1,17 +1,35 @@ | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_typeahead/flutter_typeahead.dart'; | ||||
| import 'package:font_awesome_flutter/font_awesome_flutter.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/exts.dart'; | ||||
| import 'package:solian/models/account.dart'; | ||||
| import 'package:solian/models/channel.dart'; | ||||
| import 'package:solian/models/event.dart'; | ||||
| import 'package:solian/platform.dart'; | ||||
| import 'package:solian/providers/attachment_uploader.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/stickers.dart'; | ||||
| import 'package:solian/widgets/attachments/attachment_editor.dart'; | ||||
| import 'package:solian/widgets/chat/chat_event.dart'; | ||||
| import 'package:badges/badges.dart' as badges; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class ChatMessageSuggestion { | ||||
|   final String type; | ||||
|   final Widget leading; | ||||
|   final String display; | ||||
|   final String content; | ||||
|  | ||||
|   ChatMessageSuggestion({ | ||||
|     required this.type, | ||||
|     required this.leading, | ||||
|     required this.display, | ||||
|     required this.content, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| class ChatMessageInput extends StatefulWidget { | ||||
|   final Event? edit; | ||||
|   final Event? reply; | ||||
| @@ -37,8 +55,8 @@ class ChatMessageInput extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _ChatMessageInputState extends State<ChatMessageInput> { | ||||
|   final TextEditingController _textController = TextEditingController(); | ||||
|   final FocusNode _focusNode = FocusNode(); | ||||
|   TextEditingController _textController = TextEditingController(); | ||||
|   FocusNode _focusNode = FocusNode(); | ||||
|  | ||||
|   final List<int> _attachments = List.empty(growable: true); | ||||
|  | ||||
| @@ -156,7 +174,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> { | ||||
|       widget.onSent(message); | ||||
|     } | ||||
|  | ||||
|     resetInput(); | ||||
|     _resetInput(); | ||||
|  | ||||
|     if (_editTo != null) { | ||||
|       resp = await client.put( | ||||
| @@ -175,7 +193,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void resetInput() { | ||||
|   void _resetInput() { | ||||
|     if (widget.onReset != null) widget.onReset!(); | ||||
|     _editTo = null; | ||||
|     _replyTo = null; | ||||
| @@ -184,7 +202,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> { | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void syncWidget() { | ||||
|   void _syncWidget() { | ||||
|     if (widget.edit != null && widget.edit!.type.startsWith('messages')) { | ||||
|       final body = EventMessageBody.fromJson(widget.edit!.body); | ||||
|       _editTo = widget.edit!; | ||||
| @@ -197,9 +215,44 @@ class _ChatMessageInputState extends State<ChatMessageInput> { | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   Widget _buildSuggestion(ChatMessageSuggestion suggestion) { | ||||
|     return ListTile( | ||||
|       leading: suggestion.leading, | ||||
|       title: Text(suggestion.display), | ||||
|       subtitle: Text(suggestion.content), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _insertSuggestion(ChatMessageSuggestion suggestion) { | ||||
|     final replaceText = | ||||
|         _textController.text.substring(0, _textController.selection.baseOffset); | ||||
|     var startText = ''; | ||||
|     final afterText = replaceText == _textController.text | ||||
|         ? '' | ||||
|         : _textController.text | ||||
|             .substring(_textController.selection.baseOffset + 1); | ||||
|     var insertText = ''; | ||||
|  | ||||
|     if (suggestion.type == 'emotes') { | ||||
|       insertText = '${suggestion.content} '; | ||||
|       startText = replaceText.replaceFirstMapped( | ||||
|         RegExp(r':(?:([a-z0-9_+-]+)~)?([a-z0-9_+-]+)$'), | ||||
|         (Match m) => insertText, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (insertText.isNotEmpty && startText.isNotEmpty) { | ||||
|       _textController.text = startText + afterText; | ||||
|       _textController.selection = TextSelection( | ||||
|         baseOffset: startText.length, | ||||
|         extentOffset: startText.length, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void didUpdateWidget(covariant ChatMessageInput oldWidget) { | ||||
|     syncWidget(); | ||||
|     _syncWidget(); | ||||
|     super.didUpdateWidget(oldWidget); | ||||
|   } | ||||
|  | ||||
| @@ -207,7 +260,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> { | ||||
|   Widget build(BuildContext context) { | ||||
|     final notifyBannerActions = [ | ||||
|       TextButton( | ||||
|         onPressed: resetInput, | ||||
|         onPressed: _resetInput, | ||||
|         child: Text('cancel'.tr), | ||||
|       ) | ||||
|     ]; | ||||
| @@ -251,21 +304,74 @@ class _ChatMessageInputState extends State<ChatMessageInput> { | ||||
|             mainAxisAlignment: MainAxisAlignment.start, | ||||
|             children: [ | ||||
|               Expanded( | ||||
|                 child: TextField( | ||||
|                 child: TypeAheadField<ChatMessageSuggestion>( | ||||
|                   direction: VerticalDirection.up, | ||||
|                   hideOnEmpty: true, | ||||
|                   hideOnLoading: true, | ||||
|                   controller: _textController, | ||||
|                   focusNode: _focusNode, | ||||
|                   maxLines: null, | ||||
|                   autocorrect: true, | ||||
|                   keyboardType: TextInputType.text, | ||||
|                   decoration: InputDecoration.collapsed( | ||||
|                     hintText: widget.placeholder ?? | ||||
|                         'messageInputPlaceholder'.trParams( | ||||
|                           {'channel': '#${widget.channel.alias}'}, | ||||
|                         ), | ||||
|                   ), | ||||
|                   onSubmitted: (_) => _sendMessage(), | ||||
|                   onTapOutside: (_) => | ||||
|                       FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   hideOnSelect: false, | ||||
|                   debounceDuration: const Duration(milliseconds: 50), | ||||
|                   onSelected: (value) { | ||||
|                     _insertSuggestion(value); | ||||
|                   }, | ||||
|                   itemBuilder: (context, item) { | ||||
|                     return _buildSuggestion(item); | ||||
|                   }, | ||||
|                   builder: (context, controller, focusNode) { | ||||
|                     return TextField( | ||||
|                       controller: _textController, | ||||
|                       focusNode: _focusNode, | ||||
|                       maxLines: null, | ||||
|                       autocorrect: true, | ||||
|                       keyboardType: TextInputType.text, | ||||
|                       decoration: InputDecoration.collapsed( | ||||
|                         hintText: widget.placeholder ?? | ||||
|                             'messageInputPlaceholder'.trParams({ | ||||
|                               'channel': '#${widget.channel.alias}', | ||||
|                             }), | ||||
|                       ), | ||||
|                       onSubmitted: (_) => _sendMessage(), | ||||
|                       onTapOutside: (_) => | ||||
|                           FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     ); | ||||
|                   }, | ||||
|                   suggestionsCallback: (search) { | ||||
|                     final searchText = _textController.text | ||||
|                         .substring(0, _textController.selection.baseOffset); | ||||
|  | ||||
|                     final emojiMatch = | ||||
|                         RegExp(r':(?:([a-z0-9_+-]+)~)?([a-z0-9_+-]+)$') | ||||
|                             .firstMatch(searchText); | ||||
|                     if (emojiMatch != null) { | ||||
|                       final StickerProvider stickers = Get.find(); | ||||
|                       final emoteSearch = emojiMatch[2]!; | ||||
|                       return stickers.availableStickers | ||||
|                           .where((x) => | ||||
|                               x.textWarpedPlaceholder.contains(emoteSearch)) | ||||
|                           .map( | ||||
|                             (x) => ChatMessageSuggestion( | ||||
|                               type: 'emotes', | ||||
|                               leading: PlatformInfo.canCacheImage | ||||
|                                   ? CachedNetworkImage( | ||||
|                                       imageUrl: x.imageUrl, | ||||
|                                       width: 28, | ||||
|                                       height: 28, | ||||
|                                     ) | ||||
|                                   : Image.network( | ||||
|                                       x.imageUrl, | ||||
|                                       width: 28, | ||||
|                                       height: 28, | ||||
|                                     ), | ||||
|                               display: x.name, | ||||
|                               content: x.textWarpedPlaceholder, | ||||
|                             ), | ||||
|                           ) | ||||
|                           .toList(); | ||||
|                     } | ||||
|  | ||||
|                     return null; | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|               IconButton( | ||||
|   | ||||
							
								
								
									
										88
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -566,6 +566,54 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.0.1" | ||||
|   flutter_keyboard_visibility: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility | ||||
|       sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.0.0" | ||||
|   flutter_keyboard_visibility_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_linux | ||||
|       sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   flutter_keyboard_visibility_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_macos | ||||
|       sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   flutter_keyboard_visibility_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_platform_interface | ||||
|       sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|   flutter_keyboard_visibility_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_web | ||||
|       sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|   flutter_keyboard_visibility_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_keyboard_visibility_windows | ||||
|       sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   flutter_launcher_icons: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -675,6 +723,14 @@ packages: | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.0" | ||||
|   flutter_typeahead: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_typeahead | ||||
|       sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.2.0" | ||||
|   flutter_web_plugins: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -1288,6 +1344,38 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.8" | ||||
|   pointer_interceptor: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor | ||||
|       sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.1+2" | ||||
|   pointer_interceptor_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor_ios | ||||
|       sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.1" | ||||
|   pointer_interceptor_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor_platform_interface | ||||
|       sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.0+1" | ||||
|   pointer_interceptor_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pointer_interceptor_web | ||||
|       sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.10.2+1" | ||||
|   pool: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -67,6 +67,7 @@ dependencies: | ||||
|   avatar_stack: ^1.2.0 | ||||
|   async: ^2.11.0 | ||||
|   field_suggestion: ^0.2.5 | ||||
|   flutter_typeahead: ^5.2.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user