25ed765a03
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
216 lines
6.5 KiB
Dart
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}';
|
|
}
|
|
}
|