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>
This commit is contained in:
2026-07-04 07:49:53 +10:00
parent a36df1c961
commit d2a2a1ab07
11 changed files with 106 additions and 34 deletions

View File

@@ -1,4 +1,4 @@
// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-03
// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-07-04
// Login screen. Username + password only. No registration link.
// Respects system theme preference.
@@ -55,7 +55,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
final theme = Theme.of(context);
return Scaffold(
body: SafeArea(
body: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF0A0F2E), Color(0xFF131A42)],
),
),
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
@@ -89,6 +97,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
),
),
),
),
);
}

View File

@@ -1,10 +1,11 @@
// Version: 1.3.1 | Created: 2026-04-01 | Updated: 2026-07-03
// Version: 1.4.0 | Created: 2026-04-01 | Updated: 2026-07-04
// Message bubble widget. Handles text, images, files, redacted, replies.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../app/theme.dart';
import '../../../shared/widgets/matrix_avatar.dart';
import '../domain/message_model.dart';
@@ -79,14 +80,19 @@ class _BubbleContent extends StatelessWidget {
final theme = Theme.of(context);
final bgColour = isMine
? theme.colorScheme.primary
: theme.colorScheme.surfaceContainerHighest;
? M8Colours.selfBubble
: M8Colours.darkSurfaceVariant;
final textColour = isMine ? Colors.white : theme.colorScheme.onSurface;
final maxBubbleWidth =
(MediaQuery.sizeOf(context).width.clamp(0, 560) * 0.78).toDouble();
return Container(
constraints: const BoxConstraints(maxWidth: 320),
constraints: BoxConstraints(maxWidth: maxBubbleWidth),
decoration: BoxDecoration(
color: bgColour,
border: isMine
? null
: Border.all(color: theme.colorScheme.outline.withAlpha(31)),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),

View File

@@ -1,4 +1,4 @@
// Version: 1.1.0 | Created: 2026-04-01
// Version: 1.1.1 | Created: 2026-04-01 | Updated: 2026-07-04
// Message input bar — text, send, attach file, reply quote.
// File picker enabled in Phase 2 via file_picker ^8.0.0.
@@ -195,7 +195,8 @@ class _MessageInputState extends State<MessageInput> {
),
textInputAction: TextInputAction.send,
onSubmitted: (_) => _submit(),
maxLines: null,
minLines: 1,
maxLines: 6,
keyboardType: TextInputType.multiline,
),
),

View File

@@ -1,4 +1,4 @@
// Version: 1.0.1 | Created: 2026-04-05 | Updated: 2026-07-03
// Version: 1.1.0 | Created: 2026-04-05 | Updated: 2026-07-04
// Welcome/landing screen — first thing unauthenticated users see.
// Two paths: join a Jitsi conference (no login) or sign in for Matrix chat.
@@ -39,7 +39,15 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
body: SafeArea(
body: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF0A0F2E), Color(0xFF131A42)],
),
),
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
@@ -156,6 +164,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
),
),
),
),
),
);
}

View File

@@ -1,9 +1,10 @@
// Version: 1.1.0 | Created: 2026-04-01 | Updated: 2026-04-02
// Individual room list tile. Kept under 100 lines.
// 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';
@@ -19,6 +20,8 @@ class RoomTile extends StatelessWidget {
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,
@@ -38,12 +41,15 @@ class RoomTile extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(153),
color: hasUnread
? theme.colorScheme.onSurface.withAlpha(230)
: M8Colours.subtleText,
fontWeight: hasUnread ? FontWeight.w500 : FontWeight.normal,
),
)
: null,
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (room.lastActivityAt != null)
@@ -52,13 +58,14 @@ class RoomTile extends StatelessWidget {
style: theme.textTheme.labelSmall?.copyWith(
color: hasUnread
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withAlpha(102),
: M8Colours.subtleText,
),
),
if (hasUnread) ...[
const SizedBox(height: 4),
_UnreadBadge(count: room.unreadCount),
],
] else
const SizedBox(height: 8),
],
),
onTap: onTap,

View File

@@ -1,4 +1,4 @@
// Version: 1.6.0 | Created: 2026-04-01 | Updated: 2026-07-04
// Version: 1.7.0 | Created: 2026-04-01 | Updated: 2026-07-04
// Main rooms list screen with bottom navigation.
import 'package:flutter/material.dart';
@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'dart:async';
import '../../../app/theme.dart';
import '../../../core/network/matrix_client.dart';
import '../../help/presentation/help_tab.dart';
import '../../panic/presentation/panic_button.dart';
@@ -123,19 +124,20 @@ class _RoomsScreenState extends ConsumerState<RoomsScreen> {
),
if (!_searchActive)
IconButton(
icon: const Icon(Icons.edit_square),
tooltip: 'New message',
onPressed: () => showUserSearchDialog(context),
icon: const Icon(Icons.explore_outlined),
tooltip: 'Find rooms',
onPressed: () => showPublicRoomsDialog(context),
),
],
)
: null,
body: _buildBody(),
floatingActionButton: _selectedIndex == 0 && !_searchActive
? FloatingActionButton(
onPressed: () => showPublicRoomsDialog(context),
tooltip: 'Find rooms',
child: const Icon(Icons.explore_outlined),
? FloatingActionButton.extended(
onPressed: () => showUserSearchDialog(context),
tooltip: 'New message',
icon: const Icon(Icons.edit_square),
label: const Text('New message'),
)
: null,
bottomNavigationBar: NavigationBar(
@@ -253,16 +255,16 @@ class _EmptyRoomsState extends StatelessWidget {
Text(
'No rooms yet',
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(153),
color: M8Colours.subtleText,
),
),
const SizedBox(height: 8),
Text(
'Use the pencil icon above to message someone, or find '
'Tap the New message button to message someone, or find '
'a public room to join.',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(102),
color: M8Colours.subtleText,
),
),
const SizedBox(height: 20),