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
+13 -53
View File
@@ -96,41 +96,15 @@ extension LocalRepositoryShareOperations on LocalRepository {
);
}
bool createdProfile = false;
final List<PersonProfile> nextPeople = current.people.toList(
growable: true,
);
if (resolvedPerson == null) {
final String? inferredName = _deriveAutoProfileName(
sourceDisplayName: sourceDisplayName,
sourceUserId: sourceUserId,
sourceThreadId: sourceThreadId,
return _queueSharedInbox(
current: current,
payload: payload,
sourceFingerprint: sourceFingerprint,
normalizedDisplayName: normalizedDisplayName,
reason: SharedInboxReason.missingIdentity,
candidateProfileIds: const <String>[],
);
if (inferredName == null) {
return _queueSharedInbox(
current: current,
payload: payload,
sourceFingerprint: sourceFingerprint,
normalizedDisplayName: normalizedDisplayName,
reason: SharedInboxReason.missingIdentity,
candidateProfileIds: const <String>[],
);
}
createdProfile = true;
resolvedPerson = PersonProfile(
id: 'p-${_uuid.v4()}',
name: inferredName,
relationship: 'WhatsApp Contact',
affinityScore: 70,
nextMoment: now.add(const Duration(days: 2)),
tags: const <String>['whatsapp'],
notes: 'Auto-created from shared message.',
aliases: sourceDisplayName == null || sourceDisplayName == inferredName
? const <String>[]
: <String>[sourceDisplayName],
lastUpdatedAt: now,
);
nextPeople.insert(0, resolvedPerson);
}
return _ingestIntoResolvedProfile(
@@ -141,8 +115,8 @@ extension LocalRepositoryShareOperations on LocalRepository {
normalizedDisplayName: normalizedDisplayName,
resolvedPerson: resolvedPerson,
matchedLink: matchedLink,
createdProfile: createdProfile,
people: nextPeople,
createdProfile: false,
people: current.people,
resolvedAutomatically: true,
draft: _defaultDraftForPayload(payload),
);
@@ -298,14 +272,10 @@ extension LocalRepositoryShareOperations on LocalRepository {
}) async {
final LocalDataState current = _requireState();
final DateTime now = DateTime.now();
final String profileName =
_trimToNull(name) ??
_deriveAutoProfileName(
sourceDisplayName: payload.sourceDisplayName,
sourceUserId: payload.sourceUserId,
sourceThreadId: payload.sourceThreadId,
) ??
'New Contact';
final String? profileName = _trimToNull(name);
if (profileName == null) {
throw ArgumentError('A person name is required to create from a share');
}
final String normalizedRelationship = relationship.trim().isEmpty
? 'Contact'
: relationship.trim();
@@ -842,16 +812,6 @@ SharedInboxEntry _requireSharedInboxEntry(
throw ArgumentError('Shared inbox entry not found');
}
String? _deriveAutoProfileName({
required String? sourceDisplayName,
required String? sourceUserId,
required String? sourceThreadId,
}) {
return _trimToNull(sourceDisplayName) ??
_trimToNull(sourceUserId) ??
_trimToNull(sourceThreadId);
}
String? _buildStableSourceFingerprint({
required String sourceApp,
required String? sourceUserId,
+2 -10
View File
@@ -2,14 +2,9 @@ import 'package:flutter/material.dart';
/// Typed shell destinations used by the app navigation shell.
enum AppDestination {
dashboard('Dashboard', Icons.dashboard_rounded),
shareInbox('Capture', Icons.inbox_rounded),
people('People', Icons.people_alt_rounded),
moments('Moments', Icons.auto_awesome_rounded),
signals('Signals', Icons.tips_and_updates_rounded),
aiReview('AI Review', Icons.rate_review_rounded),
ideas('Ideas', Icons.lightbulb_outline_rounded),
reminders('Reminders', Icons.notifications_active_outlined),
sync('Sync', Icons.sync_rounded),
aiReview('Digest', Icons.rate_review_rounded),
settings('Settings', Icons.settings_rounded);
const AppDestination(this.label, this.icon);
@@ -17,6 +12,3 @@ enum AppDestination {
final String label;
final IconData icon;
}
/// Secondary destinations exposed from the compact shell overflow menu.
enum CompactSecondaryDestination { aiReview, ideas, reminders, sync, settings }
+16 -115
View File
@@ -3,14 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:relationship_saver/app/navigation/app_destination.dart';
import 'package:relationship_saver/core/config/app_theme.dart';
import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_review_view.dart';
import 'package:relationship_saver/features/dashboard/presentation/dashboard_view.dart';
import 'package:relationship_saver/features/ideas/presentation/ideas_view.dart';
import 'package:relationship_saver/features/moments/presentation/moments_view.dart';
import 'package:relationship_saver/features/people/presentation/people_view.dart';
import 'package:relationship_saver/features/reminders/presentation/reminders_view.dart';
import 'package:relationship_saver/features/settings/presentation/settings_view.dart';
import 'package:relationship_saver/features/signals/presentation/signals_view.dart';
import 'package:relationship_saver/features/sync/presentation/sync_view.dart';
import 'package:relationship_saver/features/share_intake/presentation/share_inbox_view.dart';
/// Responsive shell that hosts the main feature slices.
class AppShell extends ConsumerStatefulWidget {
@@ -21,16 +16,11 @@ class AppShell extends ConsumerStatefulWidget {
}
class _AppShellState extends ConsumerState<AppShell> {
AppDestination _destination = AppDestination.dashboard;
AppDestination _destination = AppDestination.shareInbox;
static const List<AppDestination> _destinations = AppDestination.values;
static const List<AppDestination> _compactPrimaryDestinations =
<AppDestination>[
AppDestination.dashboard,
AppDestination.people,
AppDestination.moments,
AppDestination.signals,
];
AppDestination.values;
@override
Widget build(BuildContext context) {
@@ -65,6 +55,7 @@ class _AppShellState extends ConsumerState<AppShell> {
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
layoutBuilder: _expandedSwitcherLayout,
child: KeyedSubtree(
key: ValueKey<AppDestination>(_destination),
child: _screenFor(_destination),
@@ -84,31 +75,10 @@ class _AppShellState extends ConsumerState<AppShell> {
final int compactIndex = _compactSelectedIndex(_destination);
final AppDestination active = _compactPrimaryDestinations[compactIndex];
return _CompactShell(
title: active.label,
selected: active,
body: _screenFor(active),
destinations: _compactPrimaryDestinations,
onChange: _selectDestination,
onOpenSecondary: (CompactSecondaryDestination destination) {
final AppDestination screenDestination = switch (destination) {
CompactSecondaryDestination.ideas => AppDestination.ideas,
CompactSecondaryDestination.aiReview => AppDestination.aiReview,
CompactSecondaryDestination.reminders => AppDestination.reminders,
CompactSecondaryDestination.sync => AppDestination.sync,
CompactSecondaryDestination.settings => AppDestination.settings,
};
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return _CompactSecondaryScreen(
title: screenDestination.label,
child: _screenFor(screenDestination),
);
},
),
);
},
);
}
@@ -129,22 +99,12 @@ class _AppShellState extends ConsumerState<AppShell> {
Widget _screenFor(AppDestination destination) {
switch (destination) {
case AppDestination.dashboard:
return const DashboardView();
case AppDestination.shareInbox:
return const ShareInboxView();
case AppDestination.people:
return const PeopleView();
case AppDestination.moments:
return const MomentsView();
case AppDestination.signals:
return const SignalsView();
case AppDestination.aiReview:
return const AiDigestReviewView();
case AppDestination.ideas:
return const IdeasView();
case AppDestination.reminders:
return const RemindersView();
case AppDestination.sync:
return const SyncView();
case AppDestination.settings:
return const SettingsView();
}
@@ -255,7 +215,7 @@ class _Sidebar extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Tip: Use Signals daily and convert at least one into a concrete plan.',
'Share messages or paste text into Capture. Nothing is assigned until you choose a person.',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary),
@@ -269,69 +229,25 @@ class _Sidebar extends StatelessWidget {
class _CompactShell extends StatelessWidget {
const _CompactShell({
required this.title,
required this.selected,
required this.body,
required this.destinations,
required this.onChange,
required this.onOpenSecondary,
});
final String title;
final AppDestination selected;
final Widget body;
final List<AppDestination> destinations;
final ValueChanged<AppDestination> onChange;
final ValueChanged<CompactSecondaryDestination> onOpenSecondary;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Row(
children: <Widget>[
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
),
PopupMenuButton<CompactSecondaryDestination>(
tooltip: 'More',
icon: const Icon(Icons.more_horiz_rounded),
onSelected: onOpenSecondary,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<CompactSecondaryDestination>>[
const PopupMenuItem<CompactSecondaryDestination>(
value: CompactSecondaryDestination.aiReview,
child: Text('AI Review'),
),
const PopupMenuItem<CompactSecondaryDestination>(
value: CompactSecondaryDestination.ideas,
child: Text('Ideas'),
),
const PopupMenuItem<CompactSecondaryDestination>(
value: CompactSecondaryDestination.reminders,
child: Text('Reminders'),
),
const PopupMenuItem<CompactSecondaryDestination>(
value: CompactSecondaryDestination.sync,
child: Text('Sync'),
),
const PopupMenuItem<CompactSecondaryDestination>(
value: CompactSecondaryDestination.settings,
child: Text('Settings'),
),
],
),
],
),
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 220),
layoutBuilder: _expandedSwitcherLayout,
child: KeyedSubtree(
key: ValueKey<AppDestination>(selected),
child: body,
@@ -355,27 +271,12 @@ class _CompactShell extends StatelessWidget {
}
}
class _CompactSecondaryScreen extends StatelessWidget {
const _CompactSecondaryScreen({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Color(0xFFF3F8FB), Color(0xFFEAF1F8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(title: Text(title)),
body: child,
),
);
}
Widget _expandedSwitcherLayout(
Widget? currentChild,
List<Widget> previousChildren,
) {
return Stack(
fit: StackFit.expand,
children: <Widget>[...previousChildren, ?currentChild],
);
}
+9 -156
View File
@@ -219,162 +219,15 @@ class LocalDataState {
);
}
static LocalDataState seed() {
return LocalDataState(
people: <PersonProfile>[
PersonProfile(
id: 'p-ava',
name: 'Ava Hart',
relationship: 'Partner',
affinityScore: 92,
nextMoment: DateTime(2026, 2, 16, 19, 30),
tags: <String>['coffee', 'books', 'slow mornings'],
notes: 'Prefers thoughtful plans over expensive plans.',
aliases: <String>['Aves'],
location: 'Austin, TX',
lastUpdatedAt: DateTime(2026, 2, 14, 9, 0),
lastInteractedAt: DateTime(2026, 2, 12, 18, 30),
),
PersonProfile(
id: 'p-jordan',
name: 'Jordan Lee',
relationship: 'Close Friend',
affinityScore: 78,
nextMoment: DateTime(2026, 2, 18, 18, 0),
tags: <String>['running', 'street food'],
notes: 'Has a race on Sunday; ask how training is going.',
location: 'Chicago, IL',
lastUpdatedAt: DateTime(2026, 2, 13, 8, 30),
lastInteractedAt: DateTime(2026, 2, 11, 19, 0),
),
PersonProfile(
id: 'p-mila',
name: 'Mila Stone',
relationship: 'Sister',
affinityScore: 84,
nextMoment: DateTime(2026, 2, 20, 12, 0),
tags: <String>['plants', 'retro music'],
notes: 'Birthday prep should start this week.',
aliases: <String>['Millie'],
location: 'Seattle, WA',
lastUpdatedAt: DateTime(2026, 2, 13, 14, 20),
lastInteractedAt: DateTime(2026, 2, 12, 14, 20),
),
],
moments: <RelationshipMoment>[
RelationshipMoment(
id: 'm-1',
personId: 'p-ava',
title: 'Sunday Walk Tradition',
summary: 'Short walk + no-phone hour worked really well.',
at: DateTime(2026, 2, 9, 9, 30),
type: 'ritual',
),
RelationshipMoment(
id: 'm-2',
personId: 'p-jordan',
title: 'Post-workout Check-in',
summary: 'Shared training playlist and grabbed smoothies.',
at: DateTime(2026, 2, 11, 19, 0),
type: 'support',
),
RelationshipMoment(
id: 'm-3',
personId: 'p-mila',
title: 'Gift Idea Captured',
summary: 'Found a vintage lamp from her saved style board.',
at: DateTime(2026, 2, 12, 14, 20),
type: 'gift',
),
],
ideas: <RelationshipIdea>[
RelationshipIdea(
id: 'i-1',
personId: 'p-ava',
type: IdeaType.gift,
title: 'Handwritten recipe journal',
details: 'Collect 10 memories and recipes from this year.',
createdAt: DateTime(2026, 2, 12, 9),
),
RelationshipIdea(
id: 'i-2',
personId: 'p-mila',
type: IdeaType.event,
title: 'Plant market + brunch date',
details: 'Saturday morning slot; book nearby cafe in advance.',
createdAt: DateTime(2026, 2, 13, 11),
),
],
reminders: <ReminderRule>[
ReminderRule(
id: 'r-1',
personId: 'p-jordan',
title: 'Weekly check-in message',
cadence: ReminderCadence.weekly,
nextAt: DateTime(2026, 2, 18, 18),
),
ReminderRule(
id: 'r-2',
personId: 'p-ava',
title: 'Sunday ritual planning',
cadence: ReminderCadence.weekly,
nextAt: DateTime(2026, 2, 16, 10),
),
],
tasks: <DashboardTask>[
DashboardTask(
id: 't-1',
title: 'Plan Friday dinner with Ava',
description: 'Keep it low-key: ramen + bookstore stop.',
when: DateTime(2026, 2, 16, 17, 0),
),
DashboardTask(
id: 't-2',
title: 'Send race-day encouragement to Jordan',
description: 'Voice note before 8:00 AM.',
when: DateTime(2026, 2, 17, 7, 30),
),
DashboardTask(
id: 't-3',
title: 'Finalize Mila birthday shortlist',
description: 'Pick 1 meaningful gift and 1 backup.',
when: DateTime(2026, 2, 18, 20, 0),
),
],
personFacts: <PersonFact>[
PersonFact(
id: 'pf-1',
personId: 'p-ava',
type: CapturedFactType.like,
text: 'Quiet brunch spots and recipe-book stores',
label: 'Weekend preference',
sourceKind: CaptureSourceKind.manual,
createdAt: DateTime(2026, 2, 10, 8, 0),
updatedAt: DateTime(2026, 2, 10, 8, 0),
),
PersonFact(
id: 'pf-2',
personId: 'p-mila',
type: CapturedFactType.giftIdea,
text: 'Vintage ceramic planter in muted green',
label: 'Gift lead',
sourceKind: CaptureSourceKind.manual,
createdAt: DateTime(2026, 2, 12, 14, 25),
updatedAt: DateTime(2026, 2, 12, 14, 25),
),
],
importantDates: <PersonImportantDate>[
PersonImportantDate(
id: 'pd-1',
personId: 'p-mila',
label: 'Birthday',
date: DateTime(2026, 3, 3),
classification: 'birthday',
sourceKind: CaptureSourceKind.manual,
createdAt: DateTime(2026, 2, 12, 14, 30),
updatedAt: DateTime(2026, 2, 12, 14, 30),
),
],
static LocalDataState empty() {
return const LocalDataState(
people: <PersonProfile>[],
moments: <RelationshipMoment>[],
ideas: <RelationshipIdea>[],
reminders: <ReminderRule>[],
tasks: <DashboardTask>[],
);
}
static LocalDataState seed() => empty();
}