Files
Qlyra/lib/frontend/widgets/max_link_handler.dart
T
klockky 4277b0a20d feat(links): поддержка max.ru-ссылок и komet:// диплинков
- резолв max.ru/{join,joincall,u,c,:auth,<channel>}
- открытие чатов/профилей/звонков и подтверждение веб-входа в приложении
- внешние диплинки: intent-filter (Android), URL-схемы (iOS/macOS), komet://-регистрация в рантайме (Windows/Linux)
- fix(calls): не слать opcode 79 до login (жижа я тебя выебу)
2026-06-23 14:18:51 +03:00

133 lines
3.8 KiB
Dart

import 'package:flutter/material.dart';
import '../../backend/modules/chats.dart';
import '../../backend/modules/links.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/contact_profile_screen.dart';
import 'call_link_handler.dart';
import 'confirm_dialog.dart';
import 'custom_notification.dart';
import 'swipe_route.dart';
import 'web_qr_login.dart';
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);
}
if (link.kind == MaxLinkKind.auth) {
await confirmAndAuthorizeWebQrLogin(context, link.url);
return true;
}
final resolved = await LinkModule.resolve(api, link.url);
if (!context.mounted) return true;
switch (resolved) {
case null:
return false;
case ResolvedLinkError(:final message):
showCustomNotification(context, message);
return true;
case ResolvedUser(:final contact):
_openContact(context, contact);
return true;
case ResolvedChat():
await _openResolvedChat(context, link, resolved);
return true;
}
}
void _openContact(BuildContext context, Map<dynamic, dynamic> contact) {
final id = contact['id'];
if (id is! int) {
showCustomNotification(context, 'Не удалось открыть профиль');
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ContactProfileScreen(
contactId: id,
initialName: _contactName(contact),
initialAvatarUrl: contact['baseUrl'] as String?,
),
),
);
}
Future<void> _openResolvedChat(
BuildContext context,
MaxLink 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 profile = await AppDatabase.loadActiveProfile();
final myId = profile?.id ?? 0;
final participants = chat['participants'];
final isMember = myId != 0 &&
participants is Map &&
participants.containsKey(myId.toString());
await ChatsModule.cacheServerChat(chat, myId, inList: isMember);
if (!context.mounted) return;
if (link.kind == MaxLinkKind.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;
}
if (!context.mounted) return;
}
pushSwipeable(
context,
(_) => ChatScreen(
chatId: id,
name: title,
imageUrl: icon,
chatType: type,
),
);
}
String _contactName(Map<dynamic, dynamic> 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 'Профиль';
}