import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/local/local_repository.dart'; import 'package:relationship_saver/features/shared/frosted_card.dart'; /// Review unresolved shared messages and map them to a person profile. class ShareInboxView extends ConsumerStatefulWidget { const ShareInboxView({super.key}); @override ConsumerState createState() => _ShareInboxViewState(); } class _ShareInboxViewState extends ConsumerState { bool _submitting = false; @override Widget build(BuildContext context) { final AsyncValue state = ref.watch(localRepositoryProvider); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool compact = constraints.maxWidth < 760; return Padding( padding: EdgeInsets.fromLTRB( compact ? 16 : 28, compact ? 14 : 20, compact ? 16 : 28, compact ? 16 : 20, ), child: state.when( loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) { return Center( child: Text( 'Failed to load Share Inbox.', style: Theme.of(context).textTheme.titleMedium, ), ); }, data: (LocalDataState localState) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Share Inbox', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 6), Text( 'Resolve shared messages that could not be matched confidently.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), const SizedBox(height: 14), Expanded( child: localState.sharedInbox.isEmpty ? _EmptyInbox(compact: compact) : ListView.separated( itemCount: localState.sharedInbox.length, separatorBuilder: (_, _) => const SizedBox(height: 10), itemBuilder: (BuildContext context, int index) { final SharedInboxEntry entry = localState.sharedInbox[index]; final List options = _orderedPeopleForEntry(entry, localState); return _InboxEntryCard( entry: entry, compact: compact, submitting: _submitting, suggestedMatches: options, onResolveToExisting: () => _resolveToExisting(entry, options), onCreateProfile: () => _createProfile(entry), onDismiss: () => _dismiss(entry.id), ); }, ), ), ], ); }, ), ); }, ); } List _orderedPeopleForEntry( SharedInboxEntry entry, LocalDataState state, ) { final Map peopleById = { for (final PersonProfile person in state.people) person.id: person, }; final List candidates = []; final Set usedIds = {}; for (final String id in entry.candidateProfileIds) { final PersonProfile? person = peopleById[id]; if (person != null) { candidates.add(person); usedIds.add(person.id); } } final List others = state.people .where((PersonProfile person) => !usedIds.contains(person.id)) .toList(growable: false); return [...candidates, ...others]; } Future _resolveToExisting( SharedInboxEntry entry, List options, ) async { if (_submitting || options.isEmpty) { return; } final String? selectedId = await showModalBottomSheet( context: context, showDragHandle: true, builder: (BuildContext context) { return SafeArea( child: ListView( children: options .map((PersonProfile person) { final bool suggested = entry.candidateProfileIds.contains( person.id, ); return ListTile( title: Text(person.name), subtitle: Text( suggested ? '${person.relationship} ยท Suggested' : person.relationship, ), leading: CircleAvatar(child: Text(_initials(person.name))), onTap: () => Navigator.of(context).pop(person.id), ); }) .toList(growable: false), ), ); }, ); if (selectedId == null || !mounted) { return; } await _runRepositoryAction(() async { final SharedMessageIngestResult result = await ref .read(localRepositoryProvider.notifier) .resolveSharedInboxToExistingProfile( inboxEntryId: entry.id, profileId: selectedId, ); if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Imported to ${result.profileName ?? 'selected profile'}.', ), ), ); }); } Future _createProfile(SharedInboxEntry entry) async { if (_submitting) { return; } final _CreateProfileDraft? draft = await showDialog<_CreateProfileDraft>( context: context, builder: (BuildContext context) => _CreateProfileDialog( initialName: entry.sourceDisplayName ?? entry.sourceUserId ?? entry.sourceThreadId ?? '', ), ); if (draft == null || !mounted) { return; } await _runRepositoryAction(() async { final SharedMessageIngestResult result = await ref .read(localRepositoryProvider.notifier) .resolveSharedInboxByCreatingProfile( inboxEntryId: entry.id, name: draft.name, relationship: draft.relationship, location: draft.location, ); if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Created and imported to ${result.profileName ?? 'profile'}.', ), ), ); }); } Future _dismiss(String inboxEntryId) async { if (_submitting) { return; } await _runRepositoryAction(() async { await ref .read(localRepositoryProvider.notifier) .dismissSharedInboxEntry(inboxEntryId); if (!mounted) { return; } ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Inbox item dismissed.'))); }); } Future _runRepositoryAction(Future Function() action) async { setState(() { _submitting = true; }); try { await action(); } catch (_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Action failed. Please try again.')), ); } } finally { if (mounted) { setState(() { _submitting = false; }); } } } String _initials(String name) { final List parts = name .split(RegExp(r'\s+')) .where((String value) => value.trim().isNotEmpty) .toList(growable: false); if (parts.isEmpty) { return '?'; } if (parts.length == 1) { return parts.first.substring(0, 1).toUpperCase(); } return '${parts.first.substring(0, 1).toUpperCase()}${parts[1].substring(0, 1).toUpperCase()}'; } } class _InboxEntryCard extends StatelessWidget { const _InboxEntryCard({ required this.entry, required this.compact, required this.submitting, required this.suggestedMatches, required this.onResolveToExisting, required this.onCreateProfile, required this.onDismiss, }); final SharedInboxEntry entry; final bool compact; final bool submitting; final List suggestedMatches; final VoidCallback onResolveToExisting; final VoidCallback onCreateProfile; final VoidCallback onDismiss; @override Widget build(BuildContext context) { final String sender = entry.sourceDisplayName ?? entry.sourceUserId ?? 'Unknown sender'; final bool hasCandidates = suggestedMatches.isNotEmpty; final String reasonLabel = switch (entry.reason) { SharedInboxReason.ambiguousProfileMatch => 'Ambiguous match', SharedInboxReason.nearProfileConflict => 'Near match conflict', SharedInboxReason.missingIdentity => 'Missing identity', }; return FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( sender, style: Theme.of(context).textTheme.titleLarge, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(999), color: const Color(0xFFEFF5FA), ), child: Text( reasonLabel, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.textSecondary, ), ), ), ], ), const SizedBox(height: 8), Text(entry.messageText, style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: 10), Text( 'Shared ${_formatDateTime(entry.sharedAt)}', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ FilledButton.icon( onPressed: submitting || !hasCandidates ? null : onResolveToExisting, icon: const Icon(Icons.person_search_rounded), label: Text(hasCandidates ? 'Match Existing' : 'No Matches'), ), OutlinedButton.icon( onPressed: submitting ? null : onCreateProfile, icon: const Icon(Icons.person_add_alt_1_rounded), label: const Text('Create Profile'), ), TextButton.icon( onPressed: submitting ? null : onDismiss, icon: const Icon(Icons.close_rounded), label: Text(compact ? 'Dismiss' : 'Dismiss Item'), ), ], ), ], ), ); } } class _EmptyInbox extends StatelessWidget { const _EmptyInbox({required this.compact}); final bool compact; @override Widget build(BuildContext context) { return FrostedCard( child: SizedBox( width: double.infinity, child: Padding( padding: EdgeInsets.symmetric( vertical: compact ? 18 : 30, horizontal: compact ? 6 : 14, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.inbox_rounded, size: 40, color: AppTheme.primary, ), const SizedBox(height: 10), Text( 'No unresolved shares.', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 6), Text( 'Incoming WhatsApp shares that need manual profile mapping will appear here.', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), textAlign: TextAlign.center, ), ], ), ), ), ); } } class _CreateProfileDraft { const _CreateProfileDraft({ required this.name, required this.relationship, this.location, }); final String name; final String relationship; final String? location; } class _CreateProfileDialog extends StatefulWidget { const _CreateProfileDialog({required this.initialName}); final String initialName; @override State<_CreateProfileDialog> createState() => _CreateProfileDialogState(); } class _CreateProfileDialogState extends State<_CreateProfileDialog> { late final TextEditingController _nameController; late final TextEditingController _relationshipController; late final TextEditingController _locationController; @override void initState() { super.initState(); _nameController = TextEditingController(text: widget.initialName); _relationshipController = TextEditingController(text: 'WhatsApp Contact'); _locationController = TextEditingController(); } @override void dispose() { _nameController.dispose(); _relationshipController.dispose(); _locationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Create Profile'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), ), const SizedBox(height: 8), TextField( controller: _relationshipController, decoration: const InputDecoration(labelText: 'Relationship'), ), const SizedBox(height: 8), TextField( controller: _locationController, decoration: const InputDecoration( labelText: 'Location (optional)', ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { Navigator.of(context).pop( _CreateProfileDraft( name: _nameController.text, relationship: _relationshipController.text, location: _locationController.text, ), ); }, child: const Text('Create'), ), ], ); } } String _formatDateTime(DateTime value) { String twoDigits(int number) => number.toString().padLeft(2, '0'); return '${value.year}-${twoDigits(value.month)}-${twoDigits(value.day)} ${twoDigits(value.hour)}:${twoDigits(value.minute)}'; }