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
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
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}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user