diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..391a902 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 308466c..2926e76 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,62 +1,209 @@ PODS: + - app_links (7.0.0): + - Flutter - device_info_plus (0.0.1): - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter - Flutter (1.0.0) + - flutter_inappwebview_ios (0.0.1): + - Flutter + - flutter_inappwebview_ios/Core (= 0.0.1) + - OrderedSet (~> 6.0.3) + - flutter_inappwebview_ios/Core (0.0.1): + - Flutter + - OrderedSet (~> 6.0.3) + - flutter_local_notifications (0.0.1): + - Flutter - flutter_secure_storage_darwin (10.0.0): - Flutter - FlutterMacOS - flutter_timezone (0.0.1): - Flutter + - flutter_webrtc (1.4.0): + - Flutter + - WebRTC-SDK (= 144.7559.09) + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - media_kit_video (0.0.1): + - Flutter - mobile_scanner (7.0.0): - Flutter - FlutterMacOS + - ogg_opus_player (0.0.1): + - Flutter + - FlutterMacOS + - open_filex (0.0.2): + - Flutter + - OrderedSet (6.0.3) - package_info_plus (0.4.5): - Flutter + - photo_manager (3.9.0): + - Flutter + - FlutterMacOS + - record_ios (1.2.1): + - Flutter + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - SwiftyGif (5.4.5) + - url_launcher_ios (0.0.1): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter + - WebRTC-SDK (144.7559.09) DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) + - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) + - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) + - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) + - ogg_opus_player (from `.symlinks/plugins/ogg_opus_player/darwin`) + - open_filex (from `.symlinks/plugins/open_filex/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - photo_manager (from `.symlinks/plugins/photo_manager/darwin`) + - record_ios (from `.symlinks/plugins/record_ios/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - OrderedSet + - SDWebImage + - SwiftyGif + - WebRTC-SDK EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage_darwin: :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" flutter_timezone: :path: ".symlinks/plugins/flutter_timezone/ios" + flutter_webrtc: + :path: ".symlinks/plugins/flutter_webrtc/ios" + geolocator_apple: + :path: ".symlinks/plugins/geolocator_apple/darwin" + media_kit_video: + :path: ".symlinks/plugins/media_kit_video/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/darwin" + ogg_opus_player: + :path: ".symlinks/plugins/ogg_opus_player/darwin" + open_filex: + :path: ".symlinks/plugins/open_filex/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" + photo_manager: + :path: ".symlinks/plugins/photo_manager/darwin" + record_ios: + :path: ".symlinks/plugins/record_ios/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 + app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d - flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 - mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214 + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 + flutter_webrtc: 3ee86aa10e0313c38c93fdb215dd86e77014f96a + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + media_kit_video: f3b0d035d89def15cfbbcf7dc2ae278f201e2f83 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 + ogg_opus_player: 954784304f4e2722780018c4abf284e4f93cddf5 + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb + record_ios: 980fd386a97a35987d0fce3dfda4b26a38c90f4c + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52 + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + WebRTC-SDK: e6006119cd730d6315d875e4a421b6cc8bb88833 -PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e +PODFILE CHECKSUM: 527e8cb59c3d722cc71f78b05ab13f192b9335d3 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b6d29d3..ffaa7a0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ B1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png */; }; B1B2C3D41234567890ABCDF0 /* MinimalIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41234567890ABCDF0 /* MinimalIcon@3x.png */; }; E835CC948F2F948DC3BBF405 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AE8CB2654009FE276CACA4B /* Pods_RunnerTests.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,6 +70,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D7B0FFE63E0BD5D67D7758D /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -84,6 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 4B1A4BBCD56B3CE42AD0A480 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -111,6 +114,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -194,6 +198,9 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -219,6 +226,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -731,6 +741,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..c3fedb2 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + ?> sendButtonCallback({ + required int chatId, + required String messageId, + required String callbackId, + String? payload, + }) async { + final mid = int.tryParse(messageId); + if (mid == null) return null; + + final request = { + 'chatId': chatId, + 'messageId': mid, + 'callbackId': callbackId, + 'payload': ?payload, + }; + + final response = await _api.sendRequest(Opcode.msgSendCallback, request); + if (!response.isOk) return null; + final data = response.payload; + return data is Map ? Map.from(data) : null; + } + Future requestTranscription( int chatId, int messageId, diff --git a/lib/backend/modules/webapp.dart b/lib/backend/modules/webapp.dart index a78a20b..1189f82 100644 --- a/lib/backend/modules/webapp.dart +++ b/lib/backend/modules/webapp.dart @@ -24,12 +24,18 @@ class WebAppModule { WebAppModule(this._api); - Future fetchLaunch(int botId) async { + Future fetchLaunch( + int botId, { + String? startParam, + int? chatId, + }) async { if (_api.state != SessionState.online) { throw const WebAppUnavailable('Нет соединения с сервером'); } final packet = await _api.sendRequest(Opcode.webAppInitData, { 'botId': botId, + 'startParam': ?startParam, + 'chatId': ?chatId, }); if (!packet.isOk) { throw const WebAppUnavailable('Не удалось открыть мини-приложение'); diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index ace8a74..0cc2d99 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart'; import 'package:komet/main.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../backend/modules/messages.dart'; +import '../screens/webapp/web_app_screen.dart'; import '../../core/config/app_bubble_behavior.dart'; import '../../core/config/app_bubble_shape.dart'; import '../../core/config/komet_settings.dart'; @@ -194,9 +195,20 @@ class MessageBubble extends StatelessWidget { return text; } + InlineKeyboardAttachment? get _inlineKeyboard { + final attachments = message.attachments; + if (attachments == null) return null; + for (final a in attachments) { + if (a is InlineKeyboardAttachment && !a.isEmpty) return a; + } + return null; + } + MessageType _computeContentType() { if (message.isControl) return MessageType.control; - final attachments = message.attachments; + final attachments = message.attachments + ?.where((a) => a is! InlineKeyboardAttachment) + .toList(); if (attachments != null && attachments.isNotEmpty) { final first = attachments.first; if (first is ForwardedMessageAttachment) { @@ -433,6 +445,7 @@ class MessageBubble extends StatelessWidget { prevMessage?.senderId != message.senderId; final maxBubbleWidth = MediaQuery.sizeOf(context).width * 0.75; + final keyboard = _inlineKeyboard; final isVideoNote = _isVideoNote; final bubbleColor = isVideoNote ? Colors.transparent @@ -553,6 +566,11 @@ class MessageBubble extends StatelessWidget { ], ), ), + if (keyboard != null) + ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxBubbleWidth), + child: _buildInlineKeyboard(context, cs, keyboard), + ), if (reactionsUnder) _reactionsBar(cs), ], ), @@ -562,6 +580,169 @@ class MessageBubble extends StatelessWidget { ); } + Widget _buildInlineKeyboard( + BuildContext context, + ColorScheme cs, + InlineKeyboardAttachment keyboard, + ) { + return Padding( + padding: const EdgeInsets.only(top: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (final row in keyboard.rows) + if (row.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + for (var i = 0; i < row.length; i++) ...[ + if (i > 0) const SizedBox(width: 4), + Expanded( + child: _buildInlineKeyboardButton( + context, + cs, + keyboard, + row[i], + ), + ), + ], + ], + ), + ), + ], + ), + ); + } + + Widget _buildInlineKeyboardButton( + BuildContext context, + ColorScheme cs, + InlineKeyboardAttachment keyboard, + InlineKeyboardButton button, + ) { + final trailingIcon = switch (button.type) { + 'LINK' => Symbols.open_in_new, + 'OPEN_APP' => Symbols.chevron_right, + _ => null, + }; + return Material( + color: cs.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () => _onInlineButtonTap(context, keyboard, button), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + button.text, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: cs.primary, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + if (trailingIcon != null) ...[ + const SizedBox(width: 4), + Icon(trailingIcon, size: 16, color: cs.primary), + ], + ], + ), + ), + ), + ); + } + + Future _onInlineButtonTap( + BuildContext context, + InlineKeyboardAttachment keyboard, + InlineKeyboardButton button, + ) async { + switch (button.type) { + case 'LINK': + final url = button.url; + if (url != null && url.isNotEmpty) { + await openExternalUrl(context, url); + } + return; + case 'OPEN_APP': + await _openMiniApp(context, button); + return; + default: + final callbackId = keyboard.callbackId; + if (callbackId == null || callbackId.isEmpty) { + showCustomNotification(context, 'Кнопка не поддерживается'); + return; + } + final answer = await messagesModule.sendButtonCallback( + chatId: message.chatId, + messageId: message.id, + callbackId: callbackId, + payload: button.payload, + ); + if (!context.mounted) return; + final url = answer?['url']?.toString(); + if (url != null && url.isNotEmpty) { + await openExternalUrl(context, url); + return; + } + final text = answer?['text']?.toString(); + if (text != null && text.isNotEmpty) { + showCustomNotification(context, text); + } + } + } + + Future _openMiniApp( + BuildContext context, + InlineKeyboardButton button, + ) async { + final deeplink = button.webApp != null + ? Uri.tryParse(button.webApp!) + : null; + final startParam = + button.payload ?? + deeplink?.queryParameters['startapp'] ?? + deeplink?.queryParameters['startApp']; + final chatId = + int.tryParse(deeplink?.queryParameters['chat_id'] ?? '') ?? + message.chatId; + final botId = button.contactId; + + if (botId == null) { + showCustomNotification(context, 'Не удалось открыть приложение'); + return; + } + + if (kDebugMode) { + debugPrint( + '[inline-kb] OPEN_APP botId=$botId chatId=$chatId startParam=$startParam', + ); + } + + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => WebAppScreen( + title: button.text, + loader: () => webAppModule.fetchLaunch( + botId, + startParam: startParam, + chatId: chatId, + ), + ), + ), + ); + } + Map? _resolveReactionInfo() { if (reactionsListenable != null) { final v = reactionsListenable!.value; diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 01890f3..ef88181 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -12,6 +12,7 @@ enum AttachmentType { poll, share, call, + inlineKeyboard, } String? decodeAttachPreview(dynamic raw) { @@ -64,7 +65,7 @@ abstract class MessageAttachment { case 'SHARE': return ShareAttachment.fromMap(map); case 'INLINE_KEYBOARD': - return UnknownAttachment(map); + return InlineKeyboardAttachment.fromMap(map); default: return UnknownAttachment(map); } @@ -580,6 +581,93 @@ class ShareAttachment extends MessageAttachment { }; } +class InlineKeyboardButton { + final String type; + final String text; + final String? url; + final String? webApp; + final int? contactId; + final String? payload; + + const InlineKeyboardButton({ + required this.type, + required this.text, + this.url, + this.webApp, + this.contactId, + this.payload, + }); + + factory InlineKeyboardButton.fromMap(Map map) { + return InlineKeyboardButton( + type: (map['type'] as String? ?? '').toUpperCase(), + text: map['text']?.toString() ?? '', + url: map['url']?.toString(), + webApp: map['webApp']?.toString(), + contactId: map['contactId'] is int + ? map['contactId'] as int + : int.tryParse(map['contactId']?.toString() ?? ''), + payload: map['payload']?.toString(), + ); + } + + Map toMap() => { + 'type': type, + 'text': text, + if (url != null) 'url': url, + if (webApp != null) 'webApp': webApp, + if (contactId != null) 'contactId': contactId, + if (payload != null) 'payload': payload, + }; +} + +class InlineKeyboardAttachment extends MessageAttachment { + final String? callbackId; + final List> rows; + + const InlineKeyboardAttachment({ + this.callbackId, + required this.rows, + }) : super(type: AttachmentType.inlineKeyboard); + + bool get isEmpty => rows.every((row) => row.isEmpty); + + factory InlineKeyboardAttachment.fromMap(Map map) { + final keyboard = map['keyboard']; + final rawRows = keyboard is Map ? keyboard['buttons'] as List? : null; + final rows = >[]; + if (rawRows != null) { + for (final row in rawRows) { + if (row is! List) continue; + rows.add( + row + .whereType() + .map( + (b) => + InlineKeyboardButton.fromMap(Map.from(b)), + ) + .toList(), + ); + } + } + return InlineKeyboardAttachment( + callbackId: map['callbackId']?.toString(), + rows: rows, + ); + } + + @override + Map toMap() => { + '_type': 'INLINE_KEYBOARD', + if (callbackId != null) 'callbackId': callbackId, + 'keyboard': { + 'buttons': rows + .map((row) => row.map((b) => b.toMap()).toList()) + .toList(), + }, + }; +} + class ForwardedMessageAttachment extends MessageAttachment { final int originalSenderId; final String? originalSenderName;