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