feat: add local-first private AI digest workflow
Migrate app code into canonical feature slices, add phone-only AI digest scheduling and review, wire local notification/background task support, and cover the flow with tests.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Share Intake Application
|
||||
|
||||
This layer coordinates share-entry routing and the review flow. If content comes
|
||||
in from outside the app, this folder decides how it enters the local workflow.
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:relationship_saver/features/local/local_models.dart';
|
||||
import 'package:relationship_saver/features/people/presentation/people_view.dart';
|
||||
import 'package:relationship_saver/features/people/presentation/providers/people_ui_state.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_inbox_view.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_payload_parser.dart';
|
||||
|
||||
SharedPayload? buildSharedPayloadFromRawText({
|
||||
required String rawText,
|
||||
required String platform,
|
||||
String? sourceAppHint,
|
||||
}) {
|
||||
final String trimmed = rawText.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final String sourceApp = SharePayloadParser.detectSourceApp(
|
||||
rawText: trimmed,
|
||||
sourceAppHint: sourceAppHint,
|
||||
);
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: trimmed,
|
||||
sourceApp: sourceApp,
|
||||
platform: platform,
|
||||
createdAt: DateTime.now(),
|
||||
receivedAt: DateTime.now(),
|
||||
);
|
||||
if (payload.rawText.trim().isEmpty && (payload.url?.trim().isEmpty ?? true)) {
|
||||
return null;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
Future<SharedMessageIngestResult?> startShareCaptureReview(
|
||||
BuildContext context, {
|
||||
required WidgetRef ref,
|
||||
required SharedPayload payload,
|
||||
}) {
|
||||
return showShareCaptureReviewSheet(
|
||||
context,
|
||||
ref: ref,
|
||||
payload: payload,
|
||||
initialPersonId: ref.read(selectedPersonIdProvider),
|
||||
);
|
||||
}
|
||||
|
||||
void openShareCaptureDestination(
|
||||
BuildContext context, {
|
||||
required WidgetRef ref,
|
||||
required SharedMessageIngestResult result,
|
||||
}) {
|
||||
if (result.isQueuedForResolution) {
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) => const ShareInboxView(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.profileId != null) {
|
||||
ref.read(selectedPersonIdProvider.notifier).select(result.profileId!);
|
||||
}
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) => const PeopleView(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String shareCaptureResultMessage(SharedMessageIngestResult result) {
|
||||
if (result.isQueuedForResolution) {
|
||||
return 'Saved shared capture to inbox.';
|
||||
}
|
||||
if (result.createdProfile) {
|
||||
return 'Saved shared capture and created ${result.profileName}.';
|
||||
}
|
||||
return 'Saved shared capture to ${result.profileName}.';
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
class SharedMessageIngestInput {
|
||||
const SharedMessageIngestInput({
|
||||
required this.sourceApp,
|
||||
required this.messageText,
|
||||
this.sourceDisplayName,
|
||||
this.sourceUserId,
|
||||
this.sourceThreadId,
|
||||
this.sharedAt,
|
||||
});
|
||||
|
||||
final String sourceApp;
|
||||
final String messageText;
|
||||
final String? sourceDisplayName;
|
||||
final String? sourceUserId;
|
||||
final String? sourceThreadId;
|
||||
final DateTime? sharedAt;
|
||||
}
|
||||
|
||||
enum SharedMessageIngestStatus { imported, queuedForResolution }
|
||||
|
||||
class SharedMessageIngestResult {
|
||||
const SharedMessageIngestResult({
|
||||
required this.status,
|
||||
required this.createdProfile,
|
||||
required this.createdSourceLink,
|
||||
this.profileId,
|
||||
this.profileName,
|
||||
this.momentId,
|
||||
this.inboxEntryId,
|
||||
});
|
||||
|
||||
const SharedMessageIngestResult.imported({
|
||||
required this.profileId,
|
||||
required this.profileName,
|
||||
required this.createdProfile,
|
||||
required this.createdSourceLink,
|
||||
required this.momentId,
|
||||
}) : status = SharedMessageIngestStatus.imported,
|
||||
inboxEntryId = null;
|
||||
|
||||
const SharedMessageIngestResult.queuedForResolution({
|
||||
required this.inboxEntryId,
|
||||
}) : status = SharedMessageIngestStatus.queuedForResolution,
|
||||
createdProfile = false,
|
||||
createdSourceLink = false,
|
||||
profileId = null,
|
||||
profileName = null,
|
||||
momentId = null;
|
||||
|
||||
final SharedMessageIngestStatus status;
|
||||
final String? profileId;
|
||||
final String? profileName;
|
||||
final bool createdProfile;
|
||||
final bool createdSourceLink;
|
||||
final String? momentId;
|
||||
final String? inboxEntryId;
|
||||
|
||||
bool get isQueuedForResolution =>
|
||||
status == SharedMessageIngestStatus.queuedForResolution;
|
||||
}
|
||||
Reference in New Issue
Block a user