feat(channels): inline-кнопки под постами в каналах

This commit is contained in:
klockky
2026-06-28 12:22:08 +03:00
parent fbc3225383
commit c23a6dcfd6
8 changed files with 497 additions and 13 deletions
-2
View File
@@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict> </dict>
</plist> </plist>
+155 -8
View File
@@ -1,62 +1,209 @@
PODS: PODS:
- app_links (7.0.0):
- Flutter
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - 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 (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_secure_storage_darwin (10.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- flutter_timezone (0.0.1): - flutter_timezone (0.0.1):
- Flutter - 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): - mobile_scanner (7.0.0):
- Flutter - Flutter
- FlutterMacOS - 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): - package_info_plus (0.4.5):
- Flutter - 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): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - 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: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - 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_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - 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`) - 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`) - 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`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/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: EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
:path: 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: flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
flutter_timezone: flutter_timezone:
:path: ".symlinks/plugins/flutter_timezone/ios" :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: mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin" :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: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :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: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin: sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/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: SPEC CHECKSUMS:
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 flutter_webrtc: 3ee86aa10e0313c38c93fdb215dd86e77014f96a
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d 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 COCOAPODS: 1.16.2
+22
View File
@@ -18,6 +18,7 @@
B1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41234567890ABCDEF /* MinimalIcon@2x.png */; }; 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 */; }; 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 */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -69,6 +70,7 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -84,6 +86,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
4B1A4BBCD56B3CE42AD0A480 /* Pods_Runner.framework in Frameworks */, 4B1A4BBCD56B3CE42AD0A480 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -111,6 +114,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -194,6 +198,9 @@
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
97C146ED1CF9000F007C117D /* Runner */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
@@ -219,6 +226,9 @@
/* Begin PBXProject section */ /* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
@@ -731,6 +741,18 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
} }
@@ -5,6 +5,24 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
+24
View File
@@ -333,6 +333,8 @@ class ReplyInfo {
return 'Ссылка'; return 'Ссылка';
case AttachmentType.control: case AttachmentType.control:
return ''; return '';
case AttachmentType.inlineKeyboard:
return '';
} }
} }
return ''; return '';
@@ -871,6 +873,28 @@ class MessagesModule {
return response.isOk; return response.isOk;
} }
Future<Map<String, dynamic>?> 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<String, dynamic>.from(data) : null;
}
Future<TranscriptionResult> requestTranscription( Future<TranscriptionResult> requestTranscription(
int chatId, int chatId,
int messageId, int messageId,
+7 -1
View File
@@ -24,12 +24,18 @@ class WebAppModule {
WebAppModule(this._api); WebAppModule(this._api);
Future<WebAppLaunch> fetchLaunch(int botId) async { Future<WebAppLaunch> fetchLaunch(
int botId, {
String? startParam,
int? chatId,
}) async {
if (_api.state != SessionState.online) { if (_api.state != SessionState.online) {
throw const WebAppUnavailable('Нет соединения с сервером'); throw const WebAppUnavailable('Нет соединения с сервером');
} }
final packet = await _api.sendRequest(Opcode.webAppInitData, { final packet = await _api.sendRequest(Opcode.webAppInitData, {
'botId': botId, 'botId': botId,
'startParam': ?startParam,
'chatId': ?chatId,
}); });
if (!packet.isOk) { if (!packet.isOk) {
throw const WebAppUnavailable('Не удалось открыть мини-приложение'); throw const WebAppUnavailable('Не удалось открыть мини-приложение');
+182 -1
View File
@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
import 'package:komet/main.dart'; import 'package:komet/main.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../backend/modules/messages.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_behavior.dart';
import '../../core/config/app_bubble_shape.dart'; import '../../core/config/app_bubble_shape.dart';
import '../../core/config/komet_settings.dart'; import '../../core/config/komet_settings.dart';
@@ -194,9 +195,20 @@ class MessageBubble extends StatelessWidget {
return text; 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() { MessageType _computeContentType() {
if (message.isControl) return MessageType.control; 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) { if (attachments != null && attachments.isNotEmpty) {
final first = attachments.first; final first = attachments.first;
if (first is ForwardedMessageAttachment) { if (first is ForwardedMessageAttachment) {
@@ -433,6 +445,7 @@ class MessageBubble extends StatelessWidget {
prevMessage?.senderId != message.senderId; prevMessage?.senderId != message.senderId;
final maxBubbleWidth = MediaQuery.sizeOf(context).width * 0.75; final maxBubbleWidth = MediaQuery.sizeOf(context).width * 0.75;
final keyboard = _inlineKeyboard;
final isVideoNote = _isVideoNote; final isVideoNote = _isVideoNote;
final bubbleColor = isVideoNote final bubbleColor = isVideoNote
? Colors.transparent ? 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), 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<void> _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<void> _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() { Map? _resolveReactionInfo() {
if (reactionsListenable != null) { if (reactionsListenable != null) {
final v = reactionsListenable!.value; final v = reactionsListenable!.value;
+89 -1
View File
@@ -12,6 +12,7 @@ enum AttachmentType {
poll, poll,
share, share,
call, call,
inlineKeyboard,
} }
String? decodeAttachPreview(dynamic raw) { String? decodeAttachPreview(dynamic raw) {
@@ -64,7 +65,7 @@ abstract class MessageAttachment {
case 'SHARE': case 'SHARE':
return ShareAttachment.fromMap(map); return ShareAttachment.fromMap(map);
case 'INLINE_KEYBOARD': case 'INLINE_KEYBOARD':
return UnknownAttachment(map); return InlineKeyboardAttachment.fromMap(map);
default: default:
return UnknownAttachment(map); 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<String, dynamic> 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<String, dynamic> 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<List<InlineKeyboardButton>> rows;
const InlineKeyboardAttachment({
this.callbackId,
required this.rows,
}) : super(type: AttachmentType.inlineKeyboard);
bool get isEmpty => rows.every((row) => row.isEmpty);
factory InlineKeyboardAttachment.fromMap(Map<String, dynamic> map) {
final keyboard = map['keyboard'];
final rawRows = keyboard is Map ? keyboard['buttons'] as List? : null;
final rows = <List<InlineKeyboardButton>>[];
if (rawRows != null) {
for (final row in rawRows) {
if (row is! List) continue;
rows.add(
row
.whereType<Map>()
.map(
(b) =>
InlineKeyboardButton.fromMap(Map<String, dynamic>.from(b)),
)
.toList(),
);
}
}
return InlineKeyboardAttachment(
callbackId: map['callbackId']?.toString(),
rows: rows,
);
}
@override
Map<String, dynamic> 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 { class ForwardedMessageAttachment extends MessageAttachment {
final int originalSenderId; final int originalSenderId;
final String? originalSenderName; final String? originalSenderName;