Files
m8chat-app2/lib/features/jitsi/presentation/welcome_screen.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

228 lines
7.5 KiB
Dart

// 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.
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
final _linkController = TextEditingController();
@override
void dispose() {
_linkController.dispose();
super.dispose();
}
void _joinConference() {
final link = _linkController.text.trim();
if (link.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please paste a meeting link.')),
);
return;
}
context.go('/jitsi', extra: link);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
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),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// --- Logo & tagline ---
Column(
children: [
SvgPicture.asset(
'assets/images/m8logo.svg',
width: 80,
height: 80,
),
const SizedBox(height: 16),
Text(
'M8Chat',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
'Chat and conference, all in one place',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(153),
),
),
],
),
const SizedBox(height: 48),
// --- Conference section ---
_SectionCard(
theme: theme,
isDark: isDark,
icon: Icons.videocam_outlined,
title: 'Join a Conference',
subtitle: 'Paste your meeting link to join instantly. No account needed.',
child: Column(
children: [
TextField(
controller: _linkController,
decoration: const InputDecoration(
hintText: 'conf.m8chat.au/YourMeeting',
prefixIcon: Icon(Icons.link),
),
textInputAction: TextInputAction.go,
onSubmitted: (_) => _joinConference(),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: _joinConference,
icon: const Icon(Icons.videocam),
label: const Text('Join Meeting'),
),
],
),
),
const SizedBox(height: 20),
// --- Divider ---
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'or',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(102),
),
),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 20),
// --- Sign in section ---
_SectionCard(
theme: theme,
isDark: isDark,
icon: Icons.chat_bubble_outline,
title: 'Sign In to Chat',
subtitle: 'Access your rooms, messages and video calls.',
child: OutlinedButton.icon(
onPressed: () => context.go('/login'),
icon: const Icon(Icons.login),
label: const Text('Sign In'),
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide(color: theme.colorScheme.primary),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
),
),
),
);
}
}
/// Styled card for each welcome section.
class _SectionCard extends StatelessWidget {
const _SectionCard({
required this.theme,
required this.isDark,
required this.icon,
required this.title,
required this.subtitle,
required this.child,
});
final ThemeData theme;
final bool isDark;
final IconData icon;
final String title;
final String subtitle;
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: theme.colorScheme.primary, size: 28),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 8),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withAlpha(153),
),
),
const SizedBox(height: 16),
child,
],
),
),
);
}
}