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>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
+155 -8
View File
@@ -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
+22
View File
@@ -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 = "<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>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* 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 */;
}
@@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "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>
<BuildActionEntry
buildForTesting = "YES"
+24
View File
@@ -333,6 +333,8 @@ class ReplyInfo {
return 'Ссылка';
case AttachmentType.control:
return '';
case AttachmentType.inlineKeyboard:
return '';
}
}
return '';
@@ -871,6 +873,28 @@ class MessagesModule {
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(
int chatId,
int messageId,
+7 -1
View File
@@ -24,12 +24,18 @@ class WebAppModule {
WebAppModule(this._api);
Future<WebAppLaunch> fetchLaunch(int botId) async {
Future<WebAppLaunch> 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('Не удалось открыть мини-приложение');
+182 -1
View File
@@ -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<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() {
if (reactionsListenable != null) {
final v = reactionsListenable!.value;
+89 -1
View File
@@ -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<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 {
final int originalSenderId;
final String? originalSenderName;