feat: полное отображение типов сообщений + предпросмотр ссылок и оптимизация фото

Модели (lib/models/):
- attachment.dart — новые типы ShareAttachment (OG-карточка ссылки) и
  CallAttachment (звонок); FILE получил вложенное превью и поле token;
  LOCATION — поле zoom; единый декодер previewData (сырой WebP → data-URI)
  вынесен в decodeAttachPreview
- poll.dart — корректный парсинг голосов ({userId,timestamp}) и признак
  своего голоса (options & 1); helper withStateMap

Лента сообщений (message_bubble.dart):
- SHARE — карточка предпросмотра ссылки (картинка, домен, заголовок,
  описание), тап открывает URL
- CALL — отображение звонков: аудио/видео/групповые/пропущенные,
  длительность, направление; симметричная иконка
- LOCATION — карточка геопозиции, тап открывает карту
- FILE — превью-картинка над строкой файла
- кликабельные ссылки в тексте и подписях (LinkText)
- фикс лишней ширины бабблов гео/share/опроса (IntrinsicWidth + stretch)
- оптимизация рендера фото: убран fade-in, уменьшен размер декода
  плиток фото-сетки

Опросы:
- интерактивное голосование через opcode 304 (polls.dart, poll_view.dart):
  одиночный/множественный выбор, отметка своего ответа, проценты

Список чатов (chats.dart):
- превью типа вложения для сообщений без текста («Фото», «Опрос: …»,
  «Звонок», «Геопозиция», «Файл: …», «Ссылка: …» и т.д.)

Настройки разработчика:
- тумблер «Предпросмотр ссылок» (app_link_preview.dart) — отключает
  SHARE-карточки, оставляя текст со ссылкой; применяется на лету

Прочее:
- зависимость url_launcher ^6.3.1 (open_external_url / open_location_on_map)
- bump версии 0.5.0+11 → 0.5.0+12
This commit is contained in:
klockky
2026-06-11 17:14:36 +03:00
parent 760cdcd877
commit e3635b5d29
14 changed files with 1085 additions and 145 deletions
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppLinkPreview {
static const prefKey = 'dev_link_preview';
static const bool defaultValue = true;
static final ValueNotifier<bool> current = ValueNotifier(defaultValue);
static Future<bool> load() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(prefKey) ?? defaultValue;
}
static Future<void> save(bool value) async {
current.value = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(prefKey, value);
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/widgets.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../frontend/widgets/custom_notification.dart';
Future<void> openExternalUrl(BuildContext context, String url) async {
final uri = Uri.tryParse(url);
if (uri == null) {
showCustomNotification(context, 'Некорректная ссылка');
return;
}
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && context.mounted) {
showCustomNotification(context, 'Не удалось открыть ссылку');
}
}
Future<void> openLocationOnMap(
BuildContext context,
double latitude,
double longitude, {
double? zoom,
}) async {
final z = (zoom ?? 15).round();
final geo = Uri.parse('geo:$latitude,$longitude?z=$z');
if (await canLaunchUrl(geo)) {
final ok = await launchUrl(geo, mode: LaunchMode.externalApplication);
if (ok) return;
}
if (!context.mounted) return;
await openExternalUrl(
context,
'https://yandex.ru/maps/?pt=$longitude,$latitude&z=$z&l=map',
);
}