refactor: Gott-Datei message_group.dart in 7 Dateien aufgeteilt
Gleiche Technik wie bei settings_modal.dart: reine Verschiebung per Dart part/part-of, keine Logikänderung. message_group.dart schrumpft von 2775 auf 30 Zeilen (nur noch Bibliothekskopf). Verifiziert per automatisiertem Blockvergleich gegen das Original und flutter analyze (Baseline unverändert). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
part of '../message_group.dart';
|
||||
|
||||
class _LinkPreviewData {
|
||||
final String? title;
|
||||
final String? description;
|
||||
final String? imageMxc;
|
||||
const _LinkPreviewData({this.title, this.description, this.imageMxc});
|
||||
bool get isEmpty =>
|
||||
(title == null || title!.isEmpty) &&
|
||||
(description == null || description!.isEmpty) &&
|
||||
imageMxc == null;
|
||||
}
|
||||
|
||||
class _LinkPreview extends ConsumerStatefulWidget {
|
||||
final String url;
|
||||
final PyramidTheme pt;
|
||||
const _LinkPreview({required this.url, required this.pt});
|
||||
|
||||
// url → preview (null = fetched but nothing usable). Avoids refetching on
|
||||
// every rebuild/scroll.
|
||||
static final Map<String, _LinkPreviewData?> _cache = {};
|
||||
|
||||
@override
|
||||
ConsumerState<_LinkPreview> createState() => _LinkPreviewState();
|
||||
}
|
||||
|
||||
class _LinkPreviewState extends ConsumerState<_LinkPreview> {
|
||||
_LinkPreviewData? _data;
|
||||
bool _done = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
if (_LinkPreview._cache.containsKey(widget.url)) {
|
||||
setState(() { _data = _LinkPreview._cache[widget.url]; _done = true; });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
final hs = client.homeserver.toString();
|
||||
final enc = Uri.encodeQueryComponent(widget.url);
|
||||
final headers = {'authorization': 'Bearer ${client.accessToken}'};
|
||||
|
||||
var res = await client.httpClient.get(
|
||||
Uri.parse('$hs/_matrix/client/v1/media/preview_url?url=$enc'),
|
||||
headers: headers,
|
||||
);
|
||||
if (res.statusCode == 404) {
|
||||
// Legacy fallback for older servers.
|
||||
res = await client.httpClient.get(
|
||||
Uri.parse('$hs/_matrix/media/v3/preview_url?url=$enc'),
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
_LinkPreviewData? data;
|
||||
if (res.statusCode == 200) {
|
||||
final json = jsonDecode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>;
|
||||
data = _LinkPreviewData(
|
||||
title: (json['og:title'] as String?)?.trim(),
|
||||
description: (json['og:description'] as String?)?.trim(),
|
||||
imageMxc: json['og:image'] as String?,
|
||||
);
|
||||
if (data.isEmpty) data = null;
|
||||
}
|
||||
_LinkPreview._cache[widget.url] = data;
|
||||
if (mounted) setState(() { _data = data; _done = true; });
|
||||
} catch (_) {
|
||||
_LinkPreview._cache[widget.url] = null;
|
||||
if (mounted) setState(() { _data = null; _done = true; });
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final data = _data;
|
||||
if (!_done || data == null) return const SizedBox.shrink();
|
||||
final pt = widget.pt;
|
||||
final client = ref.watch(matrixClientProvider).valueOrNull;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 2),
|
||||
child: GestureDetector(
|
||||
onTap: () => launchUrl(Uri.parse(widget.url), mode: LaunchMode.externalApplication),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(pt.rBase),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: pt.bg2,
|
||||
border: Border(left: BorderSide(color: pt.accent, width: 3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (data.imageMxc != null && client != null)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 180),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: MxcImage(
|
||||
mxcUri: data.imageMxc!,
|
||||
client: client,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (data.title != null && data.title!.isNotEmpty)
|
||||
Text(
|
||||
data.title!,
|
||||
style: TextStyle(color: pt.fg, fontSize: 13, fontWeight: FontWeight.w600),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (data.description != null && data.description!.isNotEmpty) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
data.description!,
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 12, height: 1.35),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReplyPreview extends StatelessWidget {
|
||||
final Event replyEvent;
|
||||
final PyramidTheme pt;
|
||||
const _ReplyPreview({required this.replyEvent, required this.pt});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final senderName = replyEvent.senderFromMemoryOrFallback.calcDisplayname();
|
||||
|
||||
// Media-aware preview: show an icon + label instead of a raw filename,
|
||||
// matching WhatsApp/Discord. Falls back to stripped text for m.text.
|
||||
final (IconData? icon, String body) = _previewFor(replyEvent);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: IntrinsicHeight(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
child: Container(
|
||||
color: pt.accentSoft,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Accent bar flush with the left edge of the tinted card.
|
||||
Container(width: 3, color: pt.accent),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 5, 10, 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(senderName, style: TextStyle(color: pt.accent, fontSize: 12, fontWeight: FontWeight.w600)),
|
||||
Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 13, color: pt.fgMuted),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
body,
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(IconData?, String) _previewFor(Event e) {
|
||||
switch (e.messageType) {
|
||||
case 'm.image':
|
||||
return (Icons.image_outlined, 'Bild');
|
||||
case 'm.video':
|
||||
return (Icons.videocam_outlined, 'Video');
|
||||
case 'm.audio':
|
||||
return (Icons.mic_none_rounded, 'Sprachnachricht');
|
||||
case 'm.file':
|
||||
return (Icons.insert_drive_file_outlined, e.body.isNotEmpty ? e.body : 'Datei');
|
||||
}
|
||||
if (e.type == EventTypes.Sticker) return (Icons.emoji_emotions_outlined, 'Sticker');
|
||||
|
||||
// Plain text: strip nested reply fallback, collapse to first line.
|
||||
String body = e.body.replaceAll('\r\n', '\n');
|
||||
if (body.startsWith('> ')) {
|
||||
final lines = body.split('\n');
|
||||
var i = 0;
|
||||
while (i < lines.length && (lines[i].startsWith('> ') || lines[i].trimRight() == '>')) {
|
||||
i++;
|
||||
}
|
||||
if (i < lines.length && lines[i].trim().isEmpty) i++;
|
||||
if (i < lines.length) body = lines.skip(i).join(' ').trim();
|
||||
}
|
||||
return (null, body.split('\n').first);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user