feat: вродь диплинки и /start

This commit is contained in:
Jganenokk
2026-07-30 21:23:20 +07:00
parent f6de41f11d
commit 25f13f0492
15 changed files with 1352 additions and 156 deletions
+113 -40
View File
@@ -1,9 +1,11 @@
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';
@@ -12,7 +14,8 @@ import '../screens/contacts/open_contact_profile.dart';
import 'call_link_handler.dart';
import 'confirm_dialog.dart';
import 'custom_notification.dart';
import 'sticker_pack_sheet.dart';
import 'max_link_nav.dart';
import 'max_route_handler.dart';
import 'swipe_route.dart';
import 'web_qr_login.dart';
@@ -20,20 +23,47 @@ Future<bool> tryHandleMaxLink(BuildContext context, String url) async {
final link = MaxLink.parse(url);
if (link == null) return false;
if (link.kind == MaxLinkKind.call) {
return tryHandleCallLink(context, url);
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);
}
}
if (link.kind == MaxLinkKind.auth) {
await confirmAndAuthorizeWebQrLogin(context, link.url);
return true;
}
Future<ResolvedLink?> _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);
}
if (link.kind == MaxLinkKind.stickerSet) {
return _openStickerSet(context, link.url);
}
final resolved = await _resolve(link);
Future<bool> _openContentLink(BuildContext context, MaxContentLink link) async {
final resolved = await _resolve(link.url, link.baseUrl);
if (!context.mounted) return true;
switch (resolved) {
@@ -51,36 +81,62 @@ Future<bool> tryHandleMaxLink(BuildContext context, String url) async {
}
}
Future<ResolvedLink?> _resolve(MaxLink link) async {
final resolved = await LinkModule.resolve(api, link.url);
if (link.startPayload == null || link.baseUrl == link.url) return resolved;
if (resolved is ResolvedChat || resolved is ResolvedUser) return resolved;
return LinkModule.resolve(api, link.baseUrl);
}
Future<bool> _openStickerSet(BuildContext context, String url) async {
final path = url
.replaceFirst(
RegExp(r'^https?://(?:www\.)?max\.ru/', caseSensitive: false),
'',
)
.split('?')
.first
.split('#')
.first;
final set = await stickersModule.resolveSetByLink(path);
Future<bool> _openWebAppLink(BuildContext context, MaxWebAppLink link) async {
final resolved = await _resolve(link.url, link.url);
if (!context.mounted) return true;
if (set == null) {
showCustomNotification(context, 'Стикерпак недоступен');
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;
}
await showStickerPackSheet(context, knownSetId: set.id);
return openWebAppForBot(context, botId, startParam: link.startApp);
}
Future<int?> _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<bool> _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<void> _openContact(
BuildContext context,
MaxLink link,
MaxContentLink link,
Map<dynamic, dynamic> contact,
) async {
final id = contact['id'];
@@ -112,8 +168,7 @@ Future<bool> _startBotDialog(
Map<dynamic, dynamic> contact,
String startPayload,
) async {
final profile = await AppDatabase.loadActiveProfile();
final myId = profile?.id ?? 0;
final myId = await currentAccountId();
if (myId == 0) return false;
final chatId =
@@ -155,7 +210,7 @@ void _openChatAndStartBot(
Future<void> _openResolvedChat(
BuildContext context,
MaxLink link,
MaxContentLink link,
ResolvedChat resolved,
) async {
final chat = resolved.chat;
@@ -170,14 +225,15 @@ Future<void> _openResolvedChat(
final icon = (chat['baseIconUrl'] as String?) ?? '';
final access = chat['access'];
final profile = await AppDatabase.loadActiveProfile();
final myId = profile?.id ?? 0;
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 == MaxLinkKind.invite && access == 'PRIVATE' && !isMember) {
if (link.kind == MaxContentKind.invite &&
access == 'PRIVATE' &&
!isMember) {
final label = title.isEmpty ? 'этот чат' : '«$title»';
final confirmed = await showConfirmDialog(
context,
@@ -210,6 +266,7 @@ Future<void> _openResolvedChat(
return;
}
final target = _messageTarget(link, resolved.message);
pushSwipeable(
context,
(_) => ChatScreen(
@@ -218,10 +275,26 @@ Future<void> _openResolvedChat(
imageUrl: icon,
chatType: type,
channelSubscribed: type == 'CHANNEL' ? isMember : null,
initialMessageId: target?.id,
initialMessageTime: target?.time,
),
);
}
({String id, int? time})? _messageTarget(
MaxContentLink link,
Map<dynamic, dynamic>? 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<dynamic, dynamic> contact) {
final names = contact['names'];
if (names is List && names.isNotEmpty && names.first is Map) {