From d2a2a1ab07e67bf7c234716f08081f9613362124 Mon Sep 17 00:00:00 2001 From: help4bis Date: Sat, 4 Jul 2026 07:49:53 +1000 Subject: [PATCH] =?UTF-8?q?feat(ui):=20mobile=20UI=20v2=20=E2=80=94=20cont?= =?UTF-8?q?rast,=20avatars,=20room-list=20density,=20compose=20FAB,=20grad?= =?UTF-8?q?ient=20login=20(v1.8.0+12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- help4bis-claude-notes/decisions.md | 15 +++++++++++ lib/app/theme.dart | 7 +++++ lib/core/config/app_config.dart | 2 +- .../auth/presentation/login_screen.dart | 13 ++++++++-- .../chat/presentation/message_bubble.dart | 14 +++++++--- .../chat/presentation/message_input.dart | 5 ++-- .../jitsi/presentation/welcome_screen.dart | 13 ++++++++-- .../rooms/presentation/room_tile.dart | 19 +++++++++----- .../rooms/presentation/rooms_screen.dart | 24 +++++++++-------- lib/shared/widgets/matrix_avatar.dart | 26 +++++++++++++++---- pubspec.yaml | 2 +- 11 files changed, 106 insertions(+), 34 deletions(-) diff --git a/help4bis-claude-notes/decisions.md b/help4bis-claude-notes/decisions.md index 3b6ab5a..0001ace 100644 --- a/help4bis-claude-notes/decisions.md +++ b/help4bis-claude-notes/decisions.md @@ -91,3 +91,18 @@ - **P5 Panic button:** NEW feature/panic module. Config in account data au.m8chat.panic (room + message). Hold-to-fire button in rooms AppBar (only shows when armed); Profile tile to arm/disarm. Alert text sent FIRST, then best-effort m.location pin (geo: uri, msc3488) with 10s geolocation budget so a denied permission never blocks the alert. VERIFIED end-to-end: text + location with exact GPS landed in the target room via mock geolocation. - **Status:** built, deployed app2.m8chat.au v1.7.0+11 (backup pre170 tarball on server). Analyze clean, tests pass. - **Deferred:** ghost-device cleanup, rooms-provider rebuild perf, native session persistence — still open from original notes. + +### 2026-07-04 07:50 — Mobile UI v2 design pass (v1.8.0+12) +- **Process:** ran a 7-agent design workflow (3 lens designers: information-density, reachability-flow, polish-delight; 3 judges; 1 synthesiser). All proposals scored 8-9/10. Output: prioritised spec (A high-impact/low-risk, B medium, C defer). Full spec archived in session. +- **Implemented the whole A tier + safe B items:** + - A1/A2 message bubble contrast: self #1265ED (selfBubble token, WCAG AA 5.11:1 vs failing 3.32:1), incoming #1C2452 + hairline border. + - A3 replaced failing low-alpha read text with subtleText token (rooms empty state, room tile). + - A4 deterministic per-user avatar colour (8-hue brand palette, name.hashCode) in matrix_avatar.dart — every disc now distinct. + - A5/A6 room tile: unread preview brightened+w500, compact density, timestamp aligned to title, unread dot spacing. + - A7 fixed stale "pencil icon" empty-state copy. + - A9 FAB now "New message" (compose) in thumb zone; explore moved to AppBar compass. Straight onPressed swap. + - A8 login + welcome brand gradient (#0A0F2E→#131A42), card hairline outlines. + - B7 message input capped minLines 1 / maxLines 6. B8 bubble maxWidth = viewport*0.78 clamped 560. B10 AppBar scrolledUnderElevation + hairline. +- **Deferred (documented in spec):** B1 message grouping, B2 day separators, B4 quick-reaction row, B5 panic hold-ring, B6 scroll-to-latest FAB, C1 swipe-to-reply (iOS PWA back-gesture risk), C2 group sender prefix (needs RoomModel field), C3 demote Help nav. +- **Verified:** headless Chromium at 390x844 on production build — gradient login, distinct avatar colours, compose FAB, AA bubble contrast all confirmed. +- **Status:** built + deployed app2.m8chat.au v1.8.0+12 (backup pre180 tarball). Analyze clean, tests pass. diff --git a/lib/app/theme.dart b/lib/app/theme.dart index 469c8e5..31c8aed 100644 --- a/lib/app/theme.dart +++ b/lib/app/theme.dart @@ -10,6 +10,9 @@ import 'package:flutter/material.dart'; abstract final class M8Colours { static const Color brandBlue = Color(0xFF1265ED); static const Color accentBlue = Color(0xFF3B8BFF); + // Self message bubble — deeper than accentBlue so white text passes WCAG AA + // (white on #1265ED = 5.11:1; on #3B8BFF = 3.32:1, fails). + static const Color selfBubble = Color(0xFF1265ED); static const Color darkBackground = Color(0xFF0A0F2E); static const Color darkSurface = Color(0xFF131A42); static const Color darkSurfaceVariant = Color(0xFF1C2452); @@ -43,7 +46,11 @@ ThemeData buildDarkTheme() { backgroundColor: M8Colours.darkSurface, foregroundColor: M8Colours.onDarkSurface, elevation: 0, + scrolledUnderElevation: 2, surfaceTintColor: Colors.transparent, + shape: const Border( + bottom: BorderSide(color: M8Colours.darkSurfaceVariant), + ), ), navigationBarTheme: NavigationBarThemeData( backgroundColor: M8Colours.darkSurface, diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index b0a675f..79b1cb8 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -11,7 +11,7 @@ abstract final class AppConfig { 'turns:matrix.m8chat.au:5349', ]; static const String appName = 'M8Chat'; - static const String appVersion = '1.7.0'; + static const String appVersion = '1.8.0'; // Jitsi conferencing static const String jitsiDomain = 'conf.m8chat.au'; diff --git a/lib/features/auth/presentation/login_screen.dart b/lib/features/auth/presentation/login_screen.dart index 6a3fe4f..6eead35 100644 --- a/lib/features/auth/presentation/login_screen.dart +++ b/lib/features/auth/presentation/login_screen.dart @@ -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 { 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 { ), ), ), + ), ), ); } diff --git a/lib/features/chat/presentation/message_bubble.dart b/lib/features/chat/presentation/message_bubble.dart index 87c5254..63b92db 100644 --- a/lib/features/chat/presentation/message_bubble.dart +++ b/lib/features/chat/presentation/message_bubble.dart @@ -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), diff --git a/lib/features/chat/presentation/message_input.dart b/lib/features/chat/presentation/message_input.dart index 03815fb..33f6be6 100644 --- a/lib/features/chat/presentation/message_input.dart +++ b/lib/features/chat/presentation/message_input.dart @@ -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 { ), textInputAction: TextInputAction.send, onSubmitted: (_) => _submit(), - maxLines: null, + minLines: 1, + maxLines: 6, keyboardType: TextInputType.multiline, ), ), diff --git a/lib/features/jitsi/presentation/welcome_screen.dart b/lib/features/jitsi/presentation/welcome_screen.dart index 20a86b1..8a65538 100644 --- a/lib/features/jitsi/presentation/welcome_screen.dart +++ b/lib/features/jitsi/presentation/welcome_screen.dart @@ -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 { 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 { ), ), ), + ), ), ); } diff --git a/lib/features/rooms/presentation/room_tile.dart b/lib/features/rooms/presentation/room_tile.dart index ded386f..8f0cfd5 100644 --- a/lib/features/rooms/presentation/room_tile.dart +++ b/lib/features/rooms/presentation/room_tile.dart @@ -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, diff --git a/lib/features/rooms/presentation/rooms_screen.dart b/lib/features/rooms/presentation/rooms_screen.dart index 41406c1..48732dc 100644 --- a/lib/features/rooms/presentation/rooms_screen.dart +++ b/lib/features/rooms/presentation/rooms_screen.dart @@ -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 { ), 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), diff --git a/lib/shared/widgets/matrix_avatar.dart b/lib/shared/widgets/matrix_avatar.dart index 37db210..8356486 100644 --- a/lib/shared/widgets/matrix_avatar.dart +++ b/lib/shared/widgets/matrix_avatar.dart @@ -1,7 +1,8 @@ -// Version: 1.0.0 | Created: 2026-04-02 +// Version: 1.1.0 | Created: 2026-04-02 | Updated: 2026-07-04 // Shared avatar widget used throughout the app. Displays a cached network -// image when an HTTP avatar URL is available, or falls back to a coloured -// circle with the first letter of the display name. +// image when an HTTP avatar URL is available, or falls back to a circle +// coloured deterministically from the display name (so each person gets a +// stable, distinct disc) with the first letter of the display name. // // The [avatarUrl] MUST be a resolved HTTP URL — never pass an mxc:// URI. @@ -25,6 +26,21 @@ class MatrixAvatar extends StatelessWidget { /// Radius of the [CircleAvatar]. final double radius; + /// Brand-compatible hues for initials avatars. All dark enough for white + /// initials to remain legible. + static const List _palette = [ + Color(0xFF1265ED), // brand blue + Color(0xFF2E7DE0), // sky + Color(0xFF1E88A8), // teal + Color(0xFF3949AB), // indigo + Color(0xFF6A4CC0), // violet + Color(0xFF00897B), // green-teal + Color(0xFF5C6BC0), // periwinkle + Color(0xFFB1631F), // warm amber + ]; + + Color get _discColour => _palette[name.hashCode.abs() % _palette.length]; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -40,13 +56,13 @@ class MatrixAvatar extends StatelessWidget { return CircleAvatar( radius: radius, - backgroundColor: theme.colorScheme.primary.withAlpha(51), + backgroundColor: _discColour, child: Text( initials, style: TextStyle( fontSize: radius * 0.7, fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, + color: Colors.white, ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index df4295a..ce8f280 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: m8chat_app description: "M8Chat — Matrix chat client for Android, iOS, and Web." publish_to: 'none' -version: 1.7.0+11 +version: 1.8.0+12 environment: sdk: '>=3.11.0 <4.0.0'