feat: Handy-Chatliste als Messenger-Kachel (Vorschau, Uhrzeit, Ungelesen) + deutsche Beschriftungen
- rooms_mobile_tile.dart: auf dem Handy grosser Avatar, Name + Uhrzeit der letzten Nachricht, Vorschau (gleiche Texte wie Push: Medien-Labels, Antwort-Zitat entfernt; "Du:" bzw. Absender in Gruppen), Ungelesen-Zaehler. PC behaelt die kompakte Liste, Voice-Kanaele ihre Darstellung. - utils/date_labels.dart: chatListTime (heute Uhrzeit, Gestern, Wochentag, Datum; sicher an Zeitumstellungs-Tagen) + 5 Tests. - Sichtbare englische Texte eingedeutscht (Chats durchsuchen, Direktnachrichten, Raeume, Nachricht..., Mitglieder, Suchen, ...). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c5f10901e6
commit
c38e9c18b0
@@ -303,8 +303,8 @@ class _ChatComposerState extends ConsumerState<ChatComposer> {
|
||||
|
||||
final roomName = widget.room.getLocalizedDisplayname();
|
||||
final placeholder = widget.room.isDirectChat
|
||||
? 'Message…'
|
||||
: 'Message #$roomName';
|
||||
? 'Nachricht…'
|
||||
: 'Nachricht an #$roomName';
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
|
||||
@@ -144,13 +144,13 @@ class _ChatHeader extends ConsumerWidget {
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
tooltip: 'Search',
|
||||
tooltip: 'Suchen',
|
||||
onPressed: onOpenSearch,
|
||||
),
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.group_rounded),
|
||||
tooltip: 'Members',
|
||||
tooltip: 'Mitglieder',
|
||||
active: membersOpen,
|
||||
onPressed: onToggleMembers,
|
||||
),
|
||||
@@ -540,13 +540,13 @@ class _DmExpandingHeaderState extends ConsumerState<_DmExpandingHeader> {
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
tooltip: 'Search',
|
||||
tooltip: 'Suchen',
|
||||
onPressed: widget.onOpenSearch,
|
||||
),
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.group_rounded),
|
||||
tooltip: 'Members',
|
||||
tooltip: 'Mitglieder',
|
||||
active: widget.membersOpen,
|
||||
onPressed: widget.onToggleMembers,
|
||||
),
|
||||
|
||||
@@ -115,7 +115,7 @@ class NoChatSelected extends StatelessWidget {
|
||||
Icon(Icons.forum_outlined, size: 64, color: pt.fgDim),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Select a room to start chatting',
|
||||
'Wähle einen Chat aus',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -43,7 +43,7 @@ class MembersPanel extends ConsumerWidget {
|
||||
Icon(Icons.group_rounded, size: 16, color: pt.fg),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Members',
|
||||
'Mitglieder',
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 14,
|
||||
|
||||
@@ -29,7 +29,7 @@ class _Header extends ConsumerWidget {
|
||||
|
||||
final name = isRealSpace
|
||||
? (activeSpace?.getLocalizedDisplayname() ?? 'Space')
|
||||
: (spaceId == 'dms' ? 'Chats' : 'Rooms');
|
||||
: (spaceId == 'dms' ? 'Chats' : 'Räume');
|
||||
|
||||
void handleMenu(_SpaceMenuItem item) {
|
||||
switch (item) {
|
||||
@@ -478,7 +478,7 @@ class _FilterInput extends StatelessWidget {
|
||||
onChanged: onChanged,
|
||||
style: TextStyle(color: pt.fg, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Filter rooms',
|
||||
hintText: 'Chats durchsuchen',
|
||||
hintStyle: TextStyle(color: pt.fgDim, fontSize: 13),
|
||||
contentPadding: const EdgeInsets.fromLTRB(28, 7, 10, 7),
|
||||
filled: true,
|
||||
|
||||
@@ -99,10 +99,10 @@ class _RoomsList extends ConsumerWidget {
|
||||
if (filtered.isEmpty && invites.isEmpty) {
|
||||
return Center(
|
||||
child:
|
||||
Text('No rooms', style: TextStyle(color: pt.fgDim, fontSize: 13)));
|
||||
Text(isDms ? 'Noch keine Chats' : 'Keine Räume', style: TextStyle(color: pt.fgDim, fontSize: 13)));
|
||||
}
|
||||
|
||||
final sectionTitle = isDms ? 'Direct Messages' : 'Rooms';
|
||||
final sectionTitle = isDms ? 'Direktnachrichten' : 'Räume';
|
||||
final sectionId = isDms ? 'dms' : 'rooms';
|
||||
final isCollapsed = collapsed[sectionId] ?? false;
|
||||
|
||||
@@ -513,6 +513,18 @@ class _RoomItemState extends ConsumerState<_RoomItem> {
|
||||
final voiceParticipants = isVoice ? ref.watch(voiceParticipantsProvider(room.id)) : <Map<String, dynamic>>[];
|
||||
final client = ref.watch(matrixClientProvider).valueOrNull;
|
||||
|
||||
// Handy: Messenger-Kachel mit Vorschau (rooms_mobile_tile.dart).
|
||||
if (!isVoice && isMobileLayout(MediaQuery.sizeOf(context))) {
|
||||
return _MobileRoomTile(
|
||||
room: room,
|
||||
isActive: active,
|
||||
pt: pt,
|
||||
myUserId: client?.userID,
|
||||
onTap: widget.onTap,
|
||||
onLongPress: _showContextMenu,
|
||||
);
|
||||
}
|
||||
|
||||
Color textColor = (active || inVoice || unread > 0 || _hovered) ? pt.fg : pt.fgMuted;
|
||||
|
||||
return MouseRegion(
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
part of '../rooms_panel.dart';
|
||||
|
||||
// Chatlisten-Eintrag im Handy-Layout (Bernds Feedback 2026-10-07: die Handy-App
|
||||
// soll sich wie ein gewohnter Messenger anfühlen): großer Avatar, Name mit
|
||||
// Uhrzeit der letzten Nachricht, darunter die letzte Nachricht als Vorschau
|
||||
// und der Ungelesen-Zähler. Am PC bleibt die kompakte Discord-Liste
|
||||
// (_RoomItem); Voice-Kanäle behalten ihre eigene Darstellung.
|
||||
|
||||
const _kTileAvatar = 46.0;
|
||||
|
||||
class _MobileRoomTile extends StatelessWidget {
|
||||
final Room room;
|
||||
final bool isActive;
|
||||
final PyramidTheme pt;
|
||||
final String? myUserId;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
|
||||
const _MobileRoomTile({
|
||||
required this.room,
|
||||
required this.isActive,
|
||||
required this.pt,
|
||||
required this.myUserId,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final name = room.getLocalizedDisplayname();
|
||||
final unread = room.notificationCount;
|
||||
final mention = room.highlightCount > 0;
|
||||
final last = room.lastEvent;
|
||||
final preview = _roomPreview(room, myUserId);
|
||||
final time = last == null ? null : chatListTime(last.originServerTs);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
child: Container(
|
||||
color: isActive ? pt.accentSoft : Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
||||
child: Row(
|
||||
children: [
|
||||
room.isDirectChat
|
||||
? _DmAvatar(room: room, pt: pt, size: _kTileAvatar)
|
||||
: _RoomTileAvatar(room: room, pt: pt),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 15,
|
||||
fontWeight:
|
||||
unread > 0 ? FontWeight.w700 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (time != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
time,
|
||||
style: TextStyle(
|
||||
color: unread > 0 ? pt.accent : pt.fgDim,
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
unread > 0 ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
preview ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: unread > 0 ? pt.fgMuted : pt.fgDim,
|
||||
fontSize: 13.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (unread > 0) ...[
|
||||
const SizedBox(width: 8),
|
||||
_UnreadBadge(count: unread, isMention: mention, pt: pt),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Avatar für Räume (kein DM): Raumbild oder abgerundetes Symbol.
|
||||
class _RoomTileAvatar extends ConsumerWidget {
|
||||
final Room room;
|
||||
final PyramidTheme pt;
|
||||
const _RoomTileAvatar({required this.room, required this.pt});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final client = ref.watch(matrixClientProvider).valueOrNull;
|
||||
final fallback = Container(
|
||||
width: _kTileAvatar,
|
||||
height: _kTileAvatar,
|
||||
decoration: BoxDecoration(
|
||||
color: pt.bg3,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(
|
||||
room.encrypted ? Icons.lock_outline_rounded : Icons.tag_rounded,
|
||||
size: 22,
|
||||
color: pt.fgMuted,
|
||||
),
|
||||
);
|
||||
if (client == null || room.avatar == null) return fallback;
|
||||
return MxcAvatar(
|
||||
mxcUri: room.avatar,
|
||||
client: client,
|
||||
size: _kTileAvatar,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
placeholder: (_) => fallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Vorschau der letzten Nachricht: gleiche Texte wie in Benachrichtigungen
|
||||
/// (Medien als „📷 Foto“ usw., Antwort-Zitat entfernt), in Gruppen mit
|
||||
/// Absender, eigene Nachrichten mit „Du:“.
|
||||
String? _roomPreview(Room room, String? myUserId) {
|
||||
final e = room.lastEvent;
|
||||
if (e == null) return null;
|
||||
if (e.type == EventTypes.Encrypted) return '🔒 Verschlüsselte Nachricht';
|
||||
final body = notificationBody(e);
|
||||
if (body == null) return null;
|
||||
if (e.senderId == myUserId) return 'Du: $body';
|
||||
if (room.isDirectChat) return body;
|
||||
return '${e.senderFromMemoryOrFallback.calcDisplayname()}: $body';
|
||||
}
|
||||
@@ -3,7 +3,8 @@ part of '../rooms_panel.dart';
|
||||
class _DmAvatar extends ConsumerStatefulWidget {
|
||||
final Room room;
|
||||
final PyramidTheme pt;
|
||||
const _DmAvatar({required this.room, required this.pt});
|
||||
final double size;
|
||||
const _DmAvatar({required this.room, required this.pt, this.size = 22});
|
||||
|
||||
@override
|
||||
ConsumerState<_DmAvatar> createState() => _DmAvatarState();
|
||||
@@ -69,13 +70,13 @@ class _DmAvatarState extends ConsumerState<_DmAvatar> {
|
||||
avatar = MxcAvatar(
|
||||
mxcUri: widget.room.avatar,
|
||||
client: client,
|
||||
size: 22,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
size: widget.size,
|
||||
borderRadius: BorderRadius.circular(widget.size / 2),
|
||||
placeholder: (_) =>
|
||||
_InitialCircle(initial: initial, color: color, size: 22),
|
||||
_InitialCircle(initial: initial, color: color, size: widget.size),
|
||||
);
|
||||
} else {
|
||||
avatar = _InitialCircle(initial: initial, color: color, size: 22);
|
||||
avatar = _InitialCircle(initial: initial, color: color, size: widget.size);
|
||||
}
|
||||
|
||||
final dot = _dotColor();
|
||||
@@ -83,11 +84,11 @@ class _DmAvatarState extends ConsumerState<_DmAvatar> {
|
||||
return Stack(clipBehavior: Clip.none, children: [
|
||||
avatar,
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
right: widget.size > 30 ? 0 : -2,
|
||||
bottom: widget.size > 30 ? 0 : -2,
|
||||
child: Container(
|
||||
width: 9,
|
||||
height: 9,
|
||||
width: widget.size > 30 ? 13 : 9,
|
||||
height: widget.size > 30 ? 13 : 9,
|
||||
decoration: BoxDecoration(
|
||||
color: dot,
|
||||
shape: BoxShape.circle,
|
||||
@@ -123,9 +124,9 @@ class _InitialCircle extends StatelessWidget {
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
child: Center(
|
||||
child: Text(initial,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontSize: size * 0.45,
|
||||
fontWeight: FontWeight.w600))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import 'package:matrix/matrix.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:matrix/src/utils/space_child.dart';
|
||||
import 'package:pyramid/core/app_state.dart';
|
||||
import 'package:pyramid/core/appearance.dart';
|
||||
import 'package:pyramid/core/matrix_client.dart';
|
||||
import 'package:pyramid/core/notification_content.dart' show notificationBody;
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
import 'package:pyramid/features/rooms/rooms_provider.dart';
|
||||
import 'package:pyramid/features/call_ui/mini_call_widget.dart';
|
||||
@@ -15,6 +17,7 @@ import 'package:pyramid/features/call_signaling/call_signaling_service.dart';
|
||||
import 'package:pyramid/features/voice_channel/voice_channel_service.dart';
|
||||
import 'package:pyramid/features/rooms/user_panel.dart';
|
||||
import 'package:pyramid/features/spaces/space_admin_dialog.dart';
|
||||
import 'package:pyramid/utils/date_labels.dart';
|
||||
import 'package:pyramid/widgets/create_join_dialog.dart';
|
||||
import 'package:pyramid/widgets/hover_region.dart';
|
||||
import 'package:pyramid/widgets/mxc_image.dart';
|
||||
@@ -31,3 +34,4 @@ part 'panel/rooms_list.dart';
|
||||
part 'panel/rooms_voice.dart';
|
||||
part 'panel/rooms_invite_item.dart';
|
||||
part 'panel/rooms_shared.dart';
|
||||
part 'panel/rooms_mobile_tile.dart';
|
||||
|
||||
@@ -71,7 +71,7 @@ class SpacesRail extends ConsumerWidget {
|
||||
id: 'rooms',
|
||||
label: 'R',
|
||||
color: const Color(0xFFF59E0B),
|
||||
name: 'Rooms',
|
||||
name: 'Räume',
|
||||
active: activeSpaceId == 'rooms',
|
||||
unread: 0,
|
||||
pt: pt,
|
||||
@@ -179,7 +179,7 @@ class _HomeButtonState extends State<_HomeButton> {
|
||||
? (Matrix4.translationValues(0, -2, 0)..rotateZ(-0.05))
|
||||
: Matrix4.identity(),
|
||||
child: Tooltip(
|
||||
message: 'Direct Messages',
|
||||
message: 'Direktnachrichten',
|
||||
waitDuration: const Duration(milliseconds: 400),
|
||||
preferBelow: false,
|
||||
child: Stack(
|
||||
|
||||
Reference in New Issue
Block a user