Refine backendless share intake

This commit is contained in:
Rijad Zuzo
2026-05-18 20:33:54 +02:00
parent f655adfbea
commit 42a59e959f
37 changed files with 1467 additions and 824 deletions
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math' as math;
import 'package:relationship_saver/app/state/local_data_state.dart';
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
import 'package:relationship_saver/features/people/domain/person_models.dart';
class AnonymizedLlmDigestContext {
@@ -22,11 +23,13 @@ class AnonymizedLlmContextBuilder {
const AnonymizedLlmContextBuilder({
this.maxPeople = 20,
this.maxSignalsPerPerson = 8,
this.maxPriorSuggestionsPerPerson = 6,
DateTime Function()? now,
}) : _now = now ?? DateTime.now;
final int maxPeople;
final int maxSignalsPerPerson;
final int maxPriorSuggestionsPerPerson;
final DateTime Function() _now;
AnonymizedLlmDigestContext build(LocalDataState state) {
@@ -66,6 +69,7 @@ class AnonymizedLlmContextBuilder {
'Do not infer or ask for names.',
'Return JSON only.',
'Prefer practical suggestions that can be reviewed before saving.',
'Do not repeat priorSuggestions for the same personToken.',
],
'limits': <String, dynamic>{
'maxItems': 10,
@@ -184,9 +188,27 @@ class AnonymizedLlmContextBuilder {
.map((PersonPreferenceSignal signal) => 'avoid ${signal.label}')
.toList(growable: false),
).take(maxSignalsPerPerson).toList(growable: false),
'priorSuggestions': _priorSuggestions(state, person),
};
}
List<Map<String, String>> _priorSuggestions(
LocalDataState state,
PersonProfile person,
) {
return state.aiSuggestionDrafts
.where((AiSuggestionDraft draft) => draft.personId == person.id)
.take(maxPriorSuggestionsPerPerson)
.map(
(AiSuggestionDraft draft) => <String, String>{
'kind': draft.kind.name,
'status': draft.status.name,
'title': _safeCategory(draft.title),
},
)
.toList(growable: false);
}
String _relationshipCategory(String value) {
final String normalized = value.toLowerCase();
if (_containsAny(normalized, <String>[
@@ -391,11 +391,11 @@ class _PersonDetail extends ConsumerWidget {
]
: sharedMessages
.take(compact ? 3 : 5)
.map(
.map<Widget>(
(SharedMessageEntry entry) => _PeopleInsightRow(
title:
entry.sourceDisplayName?.trim().isNotEmpty == true
? entry.sourceDisplayName!.trim()
? 'Detected in text: ${entry.sourceDisplayName!.trim()}'
: 'Shared from ${entry.sourceApp}',
subtitle: entry.messageText.trim().isEmpty
? 'Imported message content is empty'
@@ -404,6 +404,23 @@ class _PersonDetail extends ConsumerWidget {
'${entry.resolvedAutomatically ? 'AUTO-LINKED' : 'MANUAL-LINK'}${_shortDateTimeLabel(entry.sharedAt)}',
),
)
.followedBy(<Widget>[
if (sharedMessages.length > (compact ? 3 : 5))
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () => _showSharedMessageHistory(
context,
person: person,
sharedMessages: sharedMessages,
),
icon: const Icon(Icons.list_alt_rounded),
label: Text(
'View all ${sharedMessages.length} shared texts',
),
),
),
])
.toList(growable: false),
),
],
@@ -411,6 +428,70 @@ class _PersonDetail extends ConsumerWidget {
}
}
Future<void> _showSharedMessageHistory(
BuildContext context, {
required PersonProfile person,
required List<SharedMessageEntry> sharedMessages,
}) {
return showModalBottomSheet<void>(
context: context,
useSafeArea: true,
isScrollControlled: true,
showDragHandle: true,
builder: (BuildContext context) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.82,
minChildSize: 0.45,
maxChildSize: 0.95,
builder: (BuildContext context, ScrollController scrollController) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Shared texts',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
'${person.name}${sharedMessages.length} item${sharedMessages.length == 1 ? '' : 's'}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 12),
Expanded(
child: ListView.separated(
controller: scrollController,
itemCount: sharedMessages.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (BuildContext context, int index) {
final SharedMessageEntry entry = sharedMessages[index];
return _PeopleInsightRow(
title:
entry.sourceDisplayName?.trim().isNotEmpty == true
? 'Detected in text: ${entry.sourceDisplayName!.trim()}'
: 'Shared from ${entry.sourceApp}',
subtitle: entry.messageText.trim().isEmpty
? entry.url ?? 'Imported message content is empty'
: entry.messageText.trim(),
meta:
'${entry.sourceApp.toUpperCase()}${entry.resolvedAutomatically ? 'AUTO-LINKED' : 'MANUAL-LINK'}${_shortDateTimeLabel(entry.sharedAt)}',
);
},
),
),
],
),
);
},
);
},
);
}
Future<void> _showPeoplePreferenceEvidence(
BuildContext context, {
required PersonPreferenceSignal signal,
@@ -10,6 +10,7 @@ import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_rev
import 'package:relationship_saver/features/auth/session_controller.dart';
import 'package:relationship_saver/features/local/local_models.dart';
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart';
import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart';
import 'package:relationship_saver/features/share_intake/share_capture_flow.dart';
import 'package:relationship_saver/features/share_intake/share_capture_models.dart';
import 'package:relationship_saver/features/share_intake/share_inbox_view.dart';
@@ -210,6 +211,11 @@ class SettingsView extends ConsumerWidget {
icon: const Icon(Icons.inbox_rounded),
label: const Text('Open Share Inbox'),
),
OutlinedButton.icon(
onPressed: () => _showShareDiagnostics(context),
icon: const Icon(Icons.bug_report_rounded),
label: const Text('Share Diagnostics'),
),
],
),
],
@@ -411,6 +417,26 @@ class SettingsView extends ConsumerWidget {
),
);
}
Future<void> _showShareDiagnostics(BuildContext context) async {
final ShareIntakeDebugLog debugLog = const ShareIntakeDebugLog();
final List<ShareIntakeDebugEvent> events = await debugLog.read();
if (!context.mounted) {
return;
}
await showDialog<void>(
context: context,
builder: (BuildContext context) => _ShareDiagnosticsDialog(
events: events,
onClear: () async {
await debugLog.clear();
if (context.mounted) {
Navigator.of(context).pop();
}
},
),
);
}
}
class _SettingRow extends StatelessWidget {
@@ -462,6 +488,57 @@ class _SettingRow extends StatelessWidget {
}
}
class _ShareDiagnosticsDialog extends StatelessWidget {
const _ShareDiagnosticsDialog({required this.events, required this.onClear});
final List<ShareIntakeDebugEvent> events;
final Future<void> Function() onClear;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Share Diagnostics'),
content: SizedBox(
width: 520,
child: events.isEmpty
? const Text(
'No Flutter share events recorded yet. If you tried sharing, the native share extension likely did not open the app or the plugin did not receive the URL callback.',
)
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: events
.map(
(ShareIntakeDebugEvent event) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: SelectableText(
'${_debugTimeLabel(event.at)} ${event.stage}\n${event.message}',
),
),
)
.toList(growable: false),
),
),
),
actions: <Widget>[
TextButton(
onPressed: events.isEmpty ? null : onClear,
child: const Text('Clear'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
}
}
String _debugTimeLabel(DateTime value) {
return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:${value.second.toString().padLeft(2, '0')}';
}
class _LlmConfigDraft {
const _LlmConfigDraft({
required this.provider,
@@ -39,12 +39,7 @@ Future<SharedMessageIngestResult?> startShareCaptureReview(
required WidgetRef ref,
required SharedPayload payload,
}) {
return showShareCaptureReviewSheet(
context,
ref: ref,
payload: payload,
initialPersonId: ref.read(selectedPersonIdProvider),
);
return showShareCaptureReviewSheet(context, ref: ref, payload: payload);
}
void openShareCaptureDestination(
@@ -73,10 +68,10 @@ void openShareCaptureDestination(
String shareCaptureResultMessage(SharedMessageIngestResult result) {
if (result.isQueuedForResolution) {
return 'Saved shared capture to inbox.';
return 'Saved shared text to Capture inbox.';
}
if (result.createdProfile) {
return 'Saved shared capture and created ${result.profileName}.';
return 'Saved shared text and created ${result.profileName}.';
}
return 'Saved shared capture to ${result.profileName}.';
return 'Saved shared text to ${result.profileName}.';
}
@@ -0,0 +1,79 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
const String _shareIntakeDebugLogKey = 'share_intake_debug_log_v1';
const int _maxShareIntakeDebugEvents = 40;
class ShareIntakeDebugEvent {
const ShareIntakeDebugEvent({
required this.at,
required this.stage,
required this.message,
});
factory ShareIntakeDebugEvent.fromJson(Map<String, dynamic> json) {
return ShareIntakeDebugEvent(
at: DateTime.parse(
json['at'] as String? ?? DateTime.now().toUtc().toIso8601String(),
).toLocal(),
stage: json['stage'] as String? ?? 'unknown',
message: json['message'] as String? ?? '',
);
}
final DateTime at;
final String stage;
final String message;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'at': at.toUtc().toIso8601String(),
'stage': stage,
'message': message,
};
}
}
class ShareIntakeDebugLog {
const ShareIntakeDebugLog();
Future<List<ShareIntakeDebugEvent>> read() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String? raw = prefs.getString(_shareIntakeDebugLogKey);
if (raw == null || raw.isEmpty) {
return const <ShareIntakeDebugEvent>[];
}
try {
final List<dynamic> items = jsonDecode(raw) as List<dynamic>;
return items
.whereType<Map<String, dynamic>>()
.map(ShareIntakeDebugEvent.fromJson)
.toList(growable: false);
} on FormatException {
return const <ShareIntakeDebugEvent>[];
}
}
Future<void> record(String stage, String message) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final List<ShareIntakeDebugEvent> current = await read();
final List<ShareIntakeDebugEvent> next = <ShareIntakeDebugEvent>[
ShareIntakeDebugEvent(at: DateTime.now(), stage: stage, message: message),
...current,
].take(_maxShareIntakeDebugEvents).toList(growable: false);
await prefs.setString(
_shareIntakeDebugLogKey,
jsonEncode(
next
.map((ShareIntakeDebugEvent event) => event.toJson())
.toList(growable: false),
),
);
}
Future<void> clear() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove(_shareIntakeDebugLogKey);
}
}
@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:relationship_saver/core/config/app_config.dart';
import 'package:relationship_saver/features/local/local_models.dart';
import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart';
import 'package:relationship_saver/features/share_intake/share_capture_flow.dart';
import 'package:relationship_saver/features/share_intake/share_capture_models.dart';
@@ -23,6 +24,7 @@ class IncomingShareListener extends ConsumerStatefulWidget {
class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
StreamSubscription<List<SharedMediaFile>>? _mediaSubscription;
String? _lastToken;
final ShareIntakeDebugLog _debugLog = const ShareIntakeDebugLog();
@override
void initState() {
@@ -41,25 +43,44 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
Future<void> _start() async {
if (!AppConfig.enableWhatsAppShareIntake || kIsWeb) {
await _debugLog.record(
'listener_skipped',
'enabled=${AppConfig.enableWhatsAppShareIntake}, kIsWeb=$kIsWeb',
);
return;
}
try {
await _debugLog.record('listener_start', 'Subscribing to share intents.');
_mediaSubscription = ReceiveSharingIntent.instance
.getMediaStream()
.listen(
(List<SharedMediaFile> files) =>
_handleMediaList(files, autoOpenDestination: false),
onError: (_) {},
(List<SharedMediaFile> files) {
unawaited(
_debugLog.record(
'media_stream',
'Received ${files.length} shared item(s).',
),
);
unawaited(_handleMediaList(files, autoOpenDestination: false));
},
onError: (Object error) {
unawaited(_debugLog.record('media_stream_error', '$error'));
},
);
final List<SharedMediaFile> initialMedia = await ReceiveSharingIntent
.instance
.getInitialMedia();
await _debugLog.record(
'initial_media',
'Received ${initialMedia.length} initial shared item(s).',
);
if (initialMedia.isNotEmpty) {
await _handleMediaList(initialMedia, autoOpenDestination: true);
}
} catch (_) {
} catch (error) {
await _debugLog.record('listener_error', '$error');
// Unsupported platform/plugin state should not break app usage.
}
}
@@ -69,8 +90,16 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
required bool autoOpenDestination,
}) async {
for (final SharedMediaFile file in files) {
await _debugLog.record(
'media_item',
'type=${file.type.name}, mime=${file.mimeType ?? 'none'}, pathLength=${file.path.length}, messageLength=${file.message?.length ?? 0}',
);
final String? rawText = _extractText(file);
if (rawText == null) {
await _debugLog.record(
'extract_skip',
'No usable text for type=${file.type.name}.',
);
continue;
}
await _handleRawText(
@@ -129,12 +158,21 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
sourceAppHint: sourceAppHint,
);
if (payload == null) {
await _debugLog.record(
'payload_skip',
'Parser returned null for rawLength=${trimmed.length}.',
);
return;
}
await _debugLog.record(
'payload_parsed',
'source=${payload.sourceApp}, type=${payload.payloadType.name}, rawLength=${payload.rawText.length}, hasUrl=${payload.url != null}, detectedSender=${payload.sourceDisplayName != null}',
);
final String token =
'${payload.sourceApp}:${payload.sourceUserId ?? payload.sourceDisplayName ?? ''}:${payload.rawText}:${payload.url ?? ''}';
if (_lastToken == token) {
await _debugLog.record('payload_duplicate', 'Ignored duplicate share.');
return;
}
_lastToken = token;
@@ -149,8 +187,17 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
);
if (!mounted || result == null) {
await _debugLog.record('review_cancelled', 'Share review closed.');
return;
}
unawaited(
_debugLog.record(
'review_result',
result.isQueuedForResolution
? 'Queued for inbox.'
: 'Saved to profile; createdProfile=${result.createdProfile}.',
),
);
if (autoOpenDestination) {
openShareCaptureDestination(context, ref: ref, result: result);
@@ -88,12 +88,12 @@ class _ShareCaptureReviewSheetState
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Save Shared Capture',
'Save Shared Text',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 6),
Text(
'Review the incoming text or link, then assign it safely.',
'Choose who this belongs to, or keep it in the inbox for later review.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppTheme.textSecondary,
),
@@ -101,10 +101,7 @@ class _ShareCaptureReviewSheetState
const SizedBox(height: 14),
_PayloadPreview(payload: widget.payload),
const SizedBox(height: 14),
Text(
'What kind of info is this?',
style: Theme.of(context).textTheme.titleMedium,
),
Text('Type', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Wrap(
spacing: 8,
@@ -344,9 +341,7 @@ class _ShareCaptureReviewSheetState
await showDialog<_CreatePersonFromShareDraft>(
context: context,
builder: (BuildContext context) => _CreatePersonFromShareDialog(
initialName:
widget.payload.sourceDisplayName ??
widget.payload.rawText.split(RegExp(r'\s+')).take(2).join(' '),
detectedName: widget.payload.sourceDisplayName,
),
);
if (draft == null) {
@@ -445,7 +440,7 @@ class _PayloadPreview extends StatelessWidget {
if (payload.sourceDisplayName != null) ...<Widget>[
const SizedBox(height: 4),
Text(
'Sender: ${payload.sourceDisplayName}',
'Detected in text: ${payload.sourceDisplayName}',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
@@ -484,9 +479,9 @@ class _CreatePersonFromShareDraft {
}
class _CreatePersonFromShareDialog extends StatefulWidget {
const _CreatePersonFromShareDialog({required this.initialName});
const _CreatePersonFromShareDialog({this.detectedName});
final String initialName;
final String? detectedName;
@override
State<_CreatePersonFromShareDialog> createState() =>
@@ -504,7 +499,7 @@ class _CreatePersonFromShareDialogState
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.initialName);
_nameController = TextEditingController();
}
@override
@@ -523,9 +518,21 @@ class _CreatePersonFromShareDialogState
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.detectedName?.trim().isNotEmpty == true) ...<Widget>[
Text(
'Detected message author: ${widget.detectedName!.trim()}',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
),
const SizedBox(height: 10),
],
TextField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
decoration: const InputDecoration(
labelText: 'Person name',
hintText: 'Enter the real contact',
),
),
const SizedBox(height: 10),
TextField(
@@ -4,6 +4,8 @@ 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/share_intake/share_capture_models.dart';
import 'package:relationship_saver/features/share_intake/share_capture_review_sheet.dart';
import 'package:relationship_saver/features/share_intake/share_payload_parser.dart';
import 'package:relationship_saver/features/shared/frosted_card.dart';
/// Review unresolved shared messages and map them to a person profile.
@@ -45,13 +47,25 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Share Inbox',
style: Theme.of(context).textTheme.headlineMedium,
Wrap(
spacing: 12,
runSpacing: 10,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
Text(
'Capture',
style: Theme.of(context).textTheme.headlineMedium,
),
FilledButton.icon(
onPressed: _submitting ? null : _addTextCapture,
icon: const Icon(Icons.edit_note_rounded),
label: const Text('Add Text'),
),
],
),
const SizedBox(height: 6),
Text(
'Resolve shared messages that could not be matched confidently.',
'Share messages here, paste copied text, then choose or create the right person.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: AppTheme.textSecondary,
),
@@ -59,7 +73,9 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
const SizedBox(height: 14),
Expanded(
child: localState.sharedInbox.isEmpty
? _EmptyInbox(compact: compact)
? SingleChildScrollView(
child: _EmptyInbox(compact: compact),
)
: ListView.separated(
itemCount: localState.sharedInbox.length,
separatorBuilder: (_, _) =>
@@ -178,13 +194,8 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
}
final _CreateProfileDraft? draft = await showDialog<_CreateProfileDraft>(
context: context,
builder: (BuildContext context) => _CreateProfileDialog(
initialName:
entry.sourceDisplayName ??
entry.sourceUserId ??
entry.sourceThreadId ??
'',
),
builder: (BuildContext context) =>
_CreateProfileDialog(detectedName: entry.sourceDisplayName),
);
if (draft == null || !mounted) {
return;
@@ -232,6 +243,38 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
});
}
Future<void> _addTextCapture() async {
if (_submitting) {
return;
}
final String? rawText = await showDialog<String>(
context: context,
builder: (BuildContext context) => const _AddTextDialog(),
);
if (rawText == null || rawText.trim().isEmpty || !mounted) {
return;
}
final SharedPayload payload = SharePayloadParser.parse(
rawText: rawText,
sourceApp: 'manual',
platform: 'manual',
createdAt: DateTime.now(),
receivedAt: DateTime.now(),
);
final SharedMessageIngestResult? result = await showShareCaptureReviewSheet(
context,
ref: ref,
payload: payload,
);
if (result == null || !mounted) {
return;
}
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(_captureResultMessage(result))));
}
Future<void> _runRepositoryAction(Future<void> Function() action) async {
setState(() {
_submitting = true;
@@ -453,6 +496,60 @@ class _ShareInboxViewState extends ConsumerState<ShareInboxView> {
}
}
class _AddTextDialog extends StatefulWidget {
const _AddTextDialog();
@override
State<_AddTextDialog> createState() => _AddTextDialogState();
}
class _AddTextDialogState extends State<_AddTextDialog> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Add Text'),
content: SizedBox(
width: 520,
child: TextField(
controller: _controller,
autofocus: true,
minLines: 5,
maxLines: 10,
textInputAction: TextInputAction.newline,
decoration: const InputDecoration(
labelText: 'Shared or copied text',
hintText: 'Paste a message, note, link, preference, or date...',
),
),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(_controller.text),
child: const Text('Review'),
),
],
);
}
}
class _CandidateReview {
const _CandidateReview({
required this.person,
@@ -1073,6 +1170,16 @@ String _factTypeLabel(CapturedFactType type) {
};
}
String _captureResultMessage(SharedMessageIngestResult result) {
if (result.isQueuedForResolution) {
return 'Saved to Capture inbox.';
}
if (result.createdProfile) {
return 'Saved and created ${result.profileName}.';
}
return 'Saved to ${result.profileName}.';
}
class _EmptyInbox extends StatelessWidget {
const _EmptyInbox({required this.compact});
@@ -1103,7 +1210,7 @@ class _EmptyInbox extends StatelessWidget {
),
const SizedBox(height: 6),
Text(
'Incoming shared captures that need manual person mapping or fact triage will appear here.',
'Share a message into the app or use Add Text to start building your private relationship notes.',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary),
@@ -1132,9 +1239,9 @@ class _CreateProfileDraft {
}
class _CreateProfileDialog extends StatefulWidget {
const _CreateProfileDialog({required this.initialName});
const _CreateProfileDialog({this.detectedName});
final String initialName;
final String? detectedName;
@override
State<_CreateProfileDialog> createState() => _CreateProfileDialogState();
@@ -1149,7 +1256,7 @@ class _CreateProfileDialogState extends State<_CreateProfileDialog> {
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.initialName);
_nameController = TextEditingController();
_relationshipController = TextEditingController(text: 'Contact');
_locationController = TextEditingController();
_aliasesController = TextEditingController();
@@ -1172,9 +1279,21 @@ class _CreateProfileDialogState extends State<_CreateProfileDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.detectedName?.trim().isNotEmpty == true) ...<Widget>[
Text(
'Detected message author: ${widget.detectedName!.trim()}',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
),
const SizedBox(height: 8),
],
TextField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
decoration: const InputDecoration(
labelText: 'Person name',
hintText: 'Enter the real contact',
),
),
const SizedBox(height: 8),
TextField(