Files
Qlyra/lib/frontend/widgets/link_text.dart
T
klockky e3635b5d29 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
2026-06-11 17:14:36 +03:00

69 lines
1.7 KiB
Dart

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../../core/utils/link_opener.dart';
final RegExp _urlPattern = RegExp(
r'(https?://[^\s<>]+|www\.[^\s<>]+)',
caseSensitive: false,
);
class LinkText extends StatefulWidget {
final String text;
final TextStyle style;
const LinkText({super.key, required this.text, required this.style});
static bool hasLinks(String? text) =>
text != null && _urlPattern.hasMatch(text);
@override
State<LinkText> createState() => _LinkTextState();
}
class _LinkTextState extends State<LinkText> {
final List<TapGestureRecognizer> _recognizers = [];
@override
void dispose() {
for (final r in _recognizers) {
r.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
for (final r in _recognizers) {
r.dispose();
}
_recognizers.clear();
final spans = <InlineSpan>[];
var cursor = 0;
for (final match in _urlPattern.allMatches(widget.text)) {
if (match.start > cursor) {
spans.add(TextSpan(text: widget.text.substring(cursor, match.start)));
}
final url = match.group(0)!;
final target = url.startsWith('www.') ? 'https://$url' : url;
final recognizer = TapGestureRecognizer()
..onTap = () => openExternalUrl(context, target);
_recognizers.add(recognizer);
spans.add(
TextSpan(
text: url,
style: const TextStyle(decoration: TextDecoration.underline),
recognizer: recognizer,
),
);
cursor = match.end;
}
if (cursor < widget.text.length) {
spans.add(TextSpan(text: widget.text.substring(cursor)));
}
return Text.rich(TextSpan(style: widget.style, children: spans));
}
}