feat: Phase 3 — E2EE calls, Jitsi conferencing, help tab, web security model
- LiveKit call E2EE: CallE2EEManager exchanges encryption keys via Matrix to-device events (m.rtc.encryption_keys) for interop with Element X - Olm bootstrapped in index.html before Flutter init; main.dart logs result - Encrypted messages shown with lock icon and informative fallback text - Profile screen: key restore dialog + security setup (cross-signing/backup) - Jitsi feature: welcome screen (public, no login), conference tab, full-screen embed via JitsiMeetExternalAPI, JitsiLink parser for all common link formats - Help tab: expandable cards for encryption, video calls, account management - Web security model: no session persistence — device ID only across visits - Media auth: MSC3916 authenticated endpoint for avatars (Synapse 1.120+) - Router: welcome route as public landing page; jitsi route as public - Manifest/index.html: M8Chat branding, dark theme colours Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
332
lib/features/help/presentation/help_tab.dart
Normal file
332
lib/features/help/presentation/help_tab.dart
Normal file
@@ -0,0 +1,332 @@
|
||||
// Version: 1.1.0 | Created: 2026-04-11 | Updated: 2026-04-11
|
||||
// Help tab with in-app guidance for common tasks and troubleshooting.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
import '../../../core/network/matrix_client.dart';
|
||||
import '../../profile/presentation/security_setup_dialog.dart';
|
||||
|
||||
void _launchUrl(BuildContext context, String url) {
|
||||
web.window.open(url, '_blank');
|
||||
}
|
||||
|
||||
class HelpTab extends ConsumerWidget {
|
||||
const HelpTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final client = ref.watch(matrixClientProvider);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
'Help & Troubleshooting',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// --- Encrypted messages ---
|
||||
_HelpCard(
|
||||
icon: Icons.lock_outline,
|
||||
title: 'I see "Encrypted — use Element to read"',
|
||||
children: [
|
||||
const _HelpParagraph(
|
||||
'This means your device does not have the keys needed to '
|
||||
'read those messages. This happens when:',
|
||||
),
|
||||
const _HelpBullet(
|
||||
'You are using M8Chat on a new device or browser.'),
|
||||
const _HelpBullet(
|
||||
'The messages were sent before you set up security.'),
|
||||
const _HelpBullet(
|
||||
'Your browser data was cleared.'),
|
||||
const SizedBox(height: 12),
|
||||
const _HelpSubheading('How to fix it'),
|
||||
const _HelpParagraph(
|
||||
'1. Go to the Profile tab.\n'
|
||||
'2. Tap Security & Privacy to expand it.\n'
|
||||
'3. Tap Security setup.\n'
|
||||
'4. If you have set up security before, enter your '
|
||||
'recovery key. If not, a new one will be created for you.\n'
|
||||
'5. Save the recovery key somewhere safe.\n'
|
||||
'6. After setup, go back to your rooms. Messages should '
|
||||
'now be readable.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _HelpSubheading('What is a recovery key?'),
|
||||
const _HelpParagraph(
|
||||
'A recovery key is a long code starting with "EsTc" that '
|
||||
'unlocks your encrypted messages. It was shown to you when '
|
||||
'you first set up security in Element or M8Chat. If you '
|
||||
'saved it, you can use it to restore your messages on any '
|
||||
'device.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Builder(
|
||||
builder: (context) => OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
showSecuritySetupDialog(context, client),
|
||||
icon: const Icon(Icons.shield_outlined),
|
||||
label: const Text('Open Security Setup'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// --- New to M8Chat ---
|
||||
const _HelpCard(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
title: 'Getting started with M8Chat',
|
||||
children: [
|
||||
_HelpParagraph(
|
||||
'M8Chat is a secure messaging app built on the Matrix '
|
||||
'protocol. All your messages are end-to-end encrypted, '
|
||||
'meaning only you and the people you chat with can read '
|
||||
'them.',
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
_HelpSubheading('First steps'),
|
||||
_HelpParagraph(
|
||||
'1. Set up security: go to Profile > Security & Privacy '
|
||||
'> Security setup. This creates your encryption keys and '
|
||||
'backs them up.\n'
|
||||
'2. Save your recovery key — you will need it if you ever '
|
||||
'log in on a new device.\n'
|
||||
'3. Start chatting. Tap the pencil icon at the top right '
|
||||
'to search for users and start a conversation.',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// --- Joining a conference ---
|
||||
const _HelpCard(
|
||||
icon: Icons.videocam_outlined,
|
||||
title: 'Joining a video conference',
|
||||
children: [
|
||||
_HelpParagraph(
|
||||
'M8Chat supports Jitsi video conferences. To join one:',
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_HelpParagraph(
|
||||
'1. Tap the Conference tab at the bottom.\n'
|
||||
'2. Paste the meeting link (e.g. conf.m8chat.au/MyMeeting).\n'
|
||||
'3. Tap Join Meeting.\n\n'
|
||||
'You can also make voice and video calls directly from any '
|
||||
'chat room using the phone and camera icons at the top of '
|
||||
'the conversation.',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// --- Video call issues ---
|
||||
const _HelpCard(
|
||||
icon: Icons.videocam_off_outlined,
|
||||
title: 'Video call problems',
|
||||
children: [
|
||||
_HelpSubheading('Scrambled or black video'),
|
||||
_HelpParagraph(
|
||||
'If video appears scrambled when calling someone on Element X, '
|
||||
'this is an encryption compatibility issue that is being '
|
||||
'worked on. Try calling from the same app on both ends.',
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_HelpSubheading('No audio or robotic sound'),
|
||||
_HelpParagraph(
|
||||
'This is usually caused by browser permissions. Make sure '
|
||||
'you have allowed microphone and camera access when '
|
||||
'prompted. On some devices (especially Sailfish OS), the '
|
||||
'audio codec support may be limited.',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// --- Account & privacy ---
|
||||
const _HelpCard(
|
||||
icon: Icons.security_outlined,
|
||||
title: 'Account & privacy',
|
||||
children: [
|
||||
_HelpSubheading('Who can read my messages?'),
|
||||
_HelpParagraph(
|
||||
'Only you and the people in the conversation. M8Chat uses '
|
||||
'end-to-end encryption — not even the server operator can '
|
||||
'read your messages.',
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_HelpSubheading('Can I use multiple devices?'),
|
||||
_HelpParagraph(
|
||||
'Yes. Set up security on each device using the same '
|
||||
'recovery key. Your messages will be available on all '
|
||||
'devices where security is set up.',
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_HelpSubheading('What happens if I lose my recovery key?'),
|
||||
_HelpParagraph(
|
||||
'You will not be able to read old messages on new devices. '
|
||||
'New messages will still work. You can create a new '
|
||||
'recovery key from the Security setup, but old messages '
|
||||
'encrypted with the previous key will remain unreadable.',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// --- Account management ---
|
||||
_HelpCard(
|
||||
icon: Icons.manage_accounts_outlined,
|
||||
title: 'Account management',
|
||||
children: [
|
||||
const _HelpParagraph(
|
||||
'All account changes need to be made through the M8Chat '
|
||||
'website. This includes:',
|
||||
),
|
||||
const _HelpBullet('Changing your password'),
|
||||
const _HelpBullet('Membership settings and billing'),
|
||||
const _HelpBullet('Updating your profile or email'),
|
||||
const _HelpBullet('Deleting your account'),
|
||||
const SizedBox(height: 12),
|
||||
Builder(
|
||||
builder: (context) => OutlinedButton.icon(
|
||||
onPressed: () => _launchUrl(context, 'https://m8chat.au'),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
label: const Text('Go to m8chat.au'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _HelpSubheading('Privacy'),
|
||||
const _HelpParagraph(
|
||||
'No account details or other privacy or user identifiers '
|
||||
'are saved or stored on this application. All data stays '
|
||||
'on the M8Chat server and is protected by end-to-end '
|
||||
'encryption.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _HelpParagraph(
|
||||
'For more information about what happens if things go '
|
||||
'wrong, including data recovery and account issues:',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Builder(
|
||||
builder: (context) => OutlinedButton.icon(
|
||||
onPressed: () => _launchUrl(
|
||||
context, 'https://m8chat.au/when-it-all-goes-wrong/'),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
label: const Text('When it all goes wrong'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: Text(
|
||||
'Need more help? Visit m8chat.au or contact the M8Chat team.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withAlpha(102),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reusable help widgets ---
|
||||
|
||||
class _HelpCard extends StatelessWidget {
|
||||
const _HelpCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ExpansionTile(
|
||||
leading: Icon(icon, color: theme.colorScheme.primary),
|
||||
title: Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
childrenPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HelpParagraph extends StatelessWidget {
|
||||
const _HelpParagraph(this.text);
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HelpSubheading extends StatelessWidget {
|
||||
const _HelpSubheading(this.text);
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HelpBullet extends StatelessWidget {
|
||||
const _HelpBullet(this.text);
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8, top: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('\u2022 '),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(height: 1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user