Files
pyramid/lib/features/chat/pinned_messages_panel.dart
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

216 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:matrix/matrix.dart';
import 'package:pyramid/core/theme.dart';
import 'package:pyramid/features/chat/chat_provider.dart';
class PinnedMessagesPanel extends ConsumerWidget {
final String roomId;
final VoidCallback onClose;
const PinnedMessagesPanel({
super.key,
required this.roomId,
required this.onClose,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final pt = PyramidTheme.of(context);
final room = ref.watch(roomProvider(roomId));
final timeline = ref.watch(timelineProvider(roomId)).valueOrNull;
if (room == null) return const SizedBox.shrink();
final pinnedIds = List<String>.from(
room.getState(EventTypes.RoomPinnedEvents)?.content['pinned'] as List? ?? [],
);
final pinned = pinnedIds
.map((id) => timeline?.events.where((e) => e.eventId == id).firstOrNull)
.where((e) => e != null)
.cast<Event>()
.toList();
return Container(
width: 320,
decoration: BoxDecoration(
color: pt.bg1,
border: Border(left: BorderSide(color: pt.border)),
),
child: Column(
children: [
// Header
Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: pt.border)),
),
child: Row(
children: [
Icon(Icons.push_pin_rounded, size: 16, color: pt.fgMuted),
const SizedBox(width: 8),
Expanded(
child: Text(
'Angepinnte Nachrichten',
style: TextStyle(
color: pt.fg,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
GestureDetector(
onTap: onClose,
child: Icon(Icons.close_rounded, size: 18, color: pt.fgDim),
),
],
),
),
// List
Expanded(
child: pinnedIds.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.push_pin_outlined, size: 40, color: pt.fgDim),
const SizedBox(height: 12),
Text(
'Keine angepinnten Nachrichten',
style: TextStyle(color: pt.fgMuted, fontSize: 14),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: pinned.isEmpty ? pinnedIds.length : pinned.length,
itemBuilder: (ctx, i) {
if (pinned.isEmpty) {
return _PinnedPlaceholder(pt: pt);
}
return _PinnedMessageCard(
event: pinned[i],
room: room,
pt: pt,
onUnpin: () => _unpin(room, pinned[i].eventId),
);
},
),
),
],
),
);
}
void _unpin(Room room, String eventId) {
final pinned = List<String>.from(
room.getState(EventTypes.RoomPinnedEvents)?.content['pinned'] as List? ?? [],
);
pinned.remove(eventId);
room.setPinnedEvents(pinned).catchError((_) => '');
}
}
class _PinnedPlaceholder extends StatelessWidget {
final PyramidTheme pt;
const _PinnedPlaceholder({required this.pt});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: pt.bg2,
borderRadius: BorderRadius.circular(pt.rBase),
border: Border.all(color: pt.border),
),
child: Text(
'Nachricht wird geladen…',
style: TextStyle(color: pt.fgDim, fontSize: 13, fontStyle: FontStyle.italic),
),
);
}
}
class _PinnedMessageCard extends StatelessWidget {
final Event event;
final Room room;
final PyramidTheme pt;
final VoidCallback onUnpin;
const _PinnedMessageCard({
required this.event,
required this.room,
required this.pt,
required this.onUnpin,
});
@override
Widget build(BuildContext context) {
final sender = event.senderFromMemoryOrFallback.calcDisplayname();
final time = _formatTime(event.originServerTs);
final body = event.body;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: pt.bg2,
borderRadius: BorderRadius.circular(pt.rBase),
border: Border.all(color: pt.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.push_pin_rounded, size: 12, color: pt.accent),
const SizedBox(width: 6),
Expanded(
child: Text(
sender,
style: TextStyle(
color: pt.fg,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
Text(time, style: TextStyle(color: pt.fgDim, fontSize: 11)),
const SizedBox(width: 6),
GestureDetector(
onTap: onUnpin,
child: Tooltip(
message: 'Entpinnen',
child: Icon(Icons.push_pin_outlined, size: 14, color: pt.fgDim),
),
),
],
),
const SizedBox(height: 6),
Text(
body,
style: TextStyle(color: pt.fgMuted, fontSize: 13),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
String _formatTime(DateTime t) {
final now = DateTime.now();
final diff = now.difference(t);
if (diff.inDays == 0) {
return '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
}
if (diff.inDays < 7) return 'vor ${diff.inDays}d';
return '${t.day}.${t.month}.${t.year}';
}
}