Files
m8chat-app2/lib/features/rooms/presentation/room_tile.dart
help4bis d2a2a1ab07 feat(ui): mobile UI v2 — contrast, avatars, room-list density, compose FAB, gradient login (v1.8.0+12)
From a 7-agent design workflow (3 lenses judged + synthesised). Shipped the
high-impact/low-risk tier:
- message bubbles: self #1265ED (WCAG AA pass), incoming #1C2452 + hairline
- deterministic per-user avatar colours (8-hue brand palette)
- room tile: brighter unread preview, compact density, aligned timestamp
- FAB -> New message (thumb zone), explore -> AppBar
- login + welcome brand gradient, card outlines
- input maxLines cap, bubble max-width viewport fraction, AppBar hairline

All verified on production build in headless Chromium at mobile size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 07:49:53 +10:00

101 lines
3.0 KiB
Dart

// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-07-04
// Individual room list tile. Kept under 120 lines.
import 'package:flutter/material.dart';
import 'package:timeago/timeago.dart' as timeago;
import '../../../app/theme.dart';
import '../../../shared/widgets/matrix_avatar.dart';
import '../domain/room_model.dart';
class RoomTile extends StatelessWidget {
const RoomTile({super.key, required this.room, required this.onTap});
final RoomModel room;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final hasUnread = room.unreadCount > 0;
return ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
leading: MatrixAvatar(
name: room.displayName,
avatarUrl: room.avatarUrl,
radius: 24,
),
title: Text(
room.displayName,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: hasUnread ? FontWeight.w600 : FontWeight.normal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: room.lastMessagePreview != null
? Text(
room.lastMessagePreview!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: hasUnread
? theme.colorScheme.onSurface.withAlpha(230)
: M8Colours.subtleText,
fontWeight: hasUnread ? FontWeight.w500 : FontWeight.normal,
),
)
: null,
trailing: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (room.lastActivityAt != null)
Text(
timeago.format(room.lastActivityAt!, locale: 'en_short'),
style: theme.textTheme.labelSmall?.copyWith(
color: hasUnread
? theme.colorScheme.primary
: M8Colours.subtleText,
),
),
if (hasUnread) ...[
const SizedBox(height: 4),
_UnreadBadge(count: room.unreadCount),
] else
const SizedBox(height: 8),
],
),
onTap: onTap,
);
}
}
class _UnreadBadge extends StatelessWidget {
const _UnreadBadge({required this.count});
final int count;
@override
Widget build(BuildContext context) {
final label = count > 99 ? '99+' : count.toString();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(10),
),
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
);
}
}