import 'dart:async'; import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; import '../../backend/modules/chats.dart'; import '../../backend/modules/links.dart'; import '../../core/cache/info_cache.dart'; import '../../core/links/max_link.dart'; import '../../core/storage/app_database.dart'; import '../../main.dart'; import '../screens/chats/chat_screen.dart'; import '../screens/contacts/open_contact_profile.dart'; import 'call_link_handler.dart'; import 'confirm_dialog.dart'; import 'custom_notification.dart'; import 'max_link_nav.dart'; import 'max_route_handler.dart'; import 'swipe_route.dart'; import 'web_qr_login.dart'; Future tryHandleMaxLink(BuildContext context, String url) async { final link = MaxLink.parse(url); if (link == null) return false; switch (link) { case MaxRootLink(): popToAppRoot(context); return true; case MaxCurrentLink(): return true; case MaxAuthLink(:final url): await confirmAndAuthorizeWebQrLogin(context, url); return true; case MaxCallLink(:final url): return tryHandleCallLink(context, url); case MaxStickerSetLink(:final path): return openStickerSetByPath(context, path); case MaxShareSelfLink(): return _shareOwnLink(context); case MaxShareTextLink(:final text): return shareTextToChat(context, text); case MaxFolderLink(:final folderId): return openFolderChatList(context, folderId); case MaxRouteLink(:final route, :final params): return openMaxRoute(context, route, params); case MaxContactIdLink(:final userId): return openContactById(context, userId); case MaxChatIdLink(:final chatId, :final messageId): return openChatById(context, chatId, messageId: messageId); case MaxWebAppLink(): return _openWebAppLink(context, link); case MaxContentLink(): return _openContentLink(context, link); } } Future _resolve(String url, String baseUrl) async { final resolved = await LinkModule.resolve(api, url); if (baseUrl == url) return resolved; if (resolved is ResolvedChat || resolved is ResolvedUser) return resolved; return LinkModule.resolve(api, baseUrl); } Future _openContentLink(BuildContext context, MaxContentLink link) async { final resolved = await _resolve(link.url, link.baseUrl); if (!context.mounted) return true; switch (resolved) { case null: return false; case ResolvedLinkError(:final message): showCustomNotification(context, message); return true; case ResolvedUser(:final contact): await _openContact(context, link, contact); return true; case ResolvedChat(): await _openResolvedChat(context, link, resolved); return true; } } Future _openWebAppLink(BuildContext context, MaxWebAppLink link) async { final resolved = await _resolve(link.url, link.url); if (!context.mounted) return true; final botId = await _botIdOf(resolved); if (!context.mounted) return true; if (botId == null) { final message = resolved is ResolvedLinkError ? resolved.message : 'Не удалось открыть приложение'; showCustomNotification(context, message); return true; } return openWebAppForBot(context, botId, startParam: link.startApp); } Future _botIdOf(ResolvedLink? resolved) async { switch (resolved) { case ResolvedUser(:final contact): final id = contact['id']; return id is int ? id : null; case ResolvedChat(:final chat): final chatId = chat['id']; if (chatId is! int) return null; if ((chat['type'] as String?) != 'DIALOG') return null; final myId = await currentAccountId(); return myId == 0 ? null : chatId ^ myId; default: return null; } } Future _shareOwnLink(BuildContext context) async { final myId = await currentAccountId(); if (myId == 0) return false; final info = await ContactInfoFetch.get(myId); if (!context.mounted) return true; final link = (info?.raw['link'] as String?)?.trim(); if (link == null || link.isEmpty) { showCustomNotification(context, 'У профиля нет публичной ссылки'); return true; } try { await Share.share(link); } catch (_) { if (context.mounted) { showCustomNotification(context, 'Не удалось поделиться ссылкой'); } } return true; } Future _openContact( BuildContext context, MaxContentLink link, Map contact, ) async { final id = contact['id']; if (id is! int) { showCustomNotification(context, 'Не удалось открыть профиль'); return; } final startPayload = link.startPayload; if (startPayload != null && await _startBotDialog(context, id, contact, startPayload)) { return; } if (!context.mounted) return; unawaited( openContactDialogProfile( context, contactId: id, name: _contactName(contact), avatarUrl: contact['baseUrl'] as String?, ), ); } Future _startBotDialog( BuildContext context, int botId, Map contact, String startPayload, ) async { final myId = await currentAccountId(); if (myId == 0) return false; final chatId = await AppDatabase.findDialogChatByParticipant(myId, botId) ?? (myId ^ botId); if (chatId <= 0 || !context.mounted) return false; _openChatAndStartBot( context, chatId: chatId, name: _contactName(contact), imageUrl: (contact['baseUrl'] as String?) ?? '', chatType: 'DIALOG', startPayload: startPayload, ); return true; } void _openChatAndStartBot( BuildContext context, { required int chatId, required String name, required String imageUrl, required String chatType, required String startPayload, }) { if (ChatScreen.startBotInVisibleChat(chatId, startPayload)) return; pushSwipeable( context, (_) => ChatScreen( chatId: chatId, name: name, imageUrl: imageUrl, chatType: chatType, botStartPayload: startPayload, ), ); } Future _openResolvedChat( BuildContext context, MaxContentLink link, ResolvedChat resolved, ) async { final chat = resolved.chat; final id = chat['id']; if (id is! int) { showCustomNotification(context, 'Не удалось открыть чат'); return; } final title = (chat['title'] as String?)?.trim() ?? ''; final type = (chat['type'] as String?) ?? 'CHAT'; final icon = (chat['baseIconUrl'] as String?) ?? ''; final access = chat['access']; final myId = await currentAccountId(); var isMember = myId != 0 && await AppDatabase.isChatInList(myId, id); await chats.cacheServerChat(chat, myId, inList: isMember); if (!context.mounted) return; if (link.kind == MaxContentKind.invite && access == 'PRIVATE' && !isMember) { final label = title.isEmpty ? 'этот чат' : '«$title»'; final confirmed = await showConfirmDialog( context, title: 'Вступить', message: 'Вступить в $label?', confirmLabel: 'Вступить', ); if (!confirmed || !context.mounted) return; final error = await LinkModule.join(api, link.url); if (error != null) { if (context.mounted) showCustomNotification(context, error); return; } isMember = true; await chats.cacheServerChat(chat, myId, inList: true); if (!context.mounted) return; } final startPayload = link.startPayload; if (startPayload != null && type == 'DIALOG') { _openChatAndStartBot( context, chatId: id, name: title, imageUrl: icon, chatType: type, startPayload: startPayload, ); return; } final target = _messageTarget(link, resolved.message); pushSwipeable( context, (_) => ChatScreen( chatId: id, name: title, imageUrl: icon, chatType: type, channelSubscribed: type == 'CHANNEL' ? isMember : null, initialMessageId: target?.id, initialMessageTime: target?.time, ), ); } ({String id, int? time})? _messageTarget( MaxContentLink link, Map? message, ) { final serverId = message?['id']?.toString(); final time = message?['time']; if (serverId != null && serverId.isNotEmpty) { return (id: serverId, time: time is int ? time : null); } final messageId = link.messageId; if (messageId == null) return null; return (id: messageId.toString(), time: null); } String _contactName(Map contact) { final names = contact['names']; if (names is List && names.isNotEmpty && names.first is Map) { final entry = names.first as Map; final full = (entry['name'] as String?)?.trim(); if (full != null && full.isNotEmpty) return full; final first = (entry['firstName'] as String?)?.trim() ?? ''; final last = (entry['lastName'] as String?)?.trim() ?? ''; final joined = '$first $last'.trim(); if (joined.isNotEmpty) return joined; } return 'Профиль'; }