Files
2026-05-19 19:17:35 +02:00

467 lines
16 KiB
Dart

// ignore_for_file: sort_constructors_first
import 'package:flutter/foundation.dart';
/// Supported raw payload shapes entering the app from the share pipeline.
enum SharedPayloadType { text, url, textWithUrl }
/// User-facing assignment outcome for a shared payload.
enum ShareAssignmentStatus {
needsPersonSelection,
assignedDirectly,
savedToInbox,
discarded,
}
/// Structured capture types that shared or manual content can resolve into.
enum CapturedFactType {
note,
like,
dislike,
importantDate,
giftIdea,
placeIdea,
activityIdea,
misc,
}
/// Tracks how a fact or date entered local storage.
enum CaptureSourceKind { manual, shareSheet, shareInbox }
/// Normalized input payload shared from another app.
@immutable
class SharedPayload {
const SharedPayload({
required this.sourceApp,
required this.payloadType,
required this.rawText,
required this.createdAt,
required this.receivedAt,
required this.platform,
this.sharedMessageDateTime,
this.url,
this.sourceDisplayName,
this.sourceUserId,
this.sourceThreadId,
this.parsingHints = const <String>[],
this.metadata = const <String, String>{},
this.dedupeKey,
});
final String sourceApp;
final SharedPayloadType payloadType;
final String rawText;
final String? url;
final DateTime createdAt;
final DateTime receivedAt;
final DateTime? sharedMessageDateTime;
final String platform;
final String? sourceDisplayName;
final String? sourceUserId;
final String? sourceThreadId;
final List<String> parsingHints;
final Map<String, String> metadata;
final String? dedupeKey;
SharedPayload copyWith({
String? sourceApp,
SharedPayloadType? payloadType,
String? rawText,
String? url,
DateTime? createdAt,
DateTime? receivedAt,
DateTime? sharedMessageDateTime,
String? platform,
String? sourceDisplayName,
String? sourceUserId,
String? sourceThreadId,
List<String>? parsingHints,
Map<String, String>? metadata,
String? dedupeKey,
}) {
return SharedPayload(
sourceApp: sourceApp ?? this.sourceApp,
payloadType: payloadType ?? this.payloadType,
rawText: rawText ?? this.rawText,
url: url ?? this.url,
createdAt: createdAt ?? this.createdAt,
receivedAt: receivedAt ?? this.receivedAt,
sharedMessageDateTime:
sharedMessageDateTime ?? this.sharedMessageDateTime,
platform: platform ?? this.platform,
sourceDisplayName: sourceDisplayName ?? this.sourceDisplayName,
sourceUserId: sourceUserId ?? this.sourceUserId,
sourceThreadId: sourceThreadId ?? this.sourceThreadId,
parsingHints: parsingHints ?? this.parsingHints,
metadata: metadata ?? this.metadata,
dedupeKey: dedupeKey ?? this.dedupeKey,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'sourceApp': sourceApp,
'payloadType': payloadType.name,
'rawText': rawText,
'url': url,
'createdAt': createdAt.toUtc().toIso8601String(),
'receivedAt': receivedAt.toUtc().toIso8601String(),
'sharedMessageDateTime': sharedMessageDateTime?.toUtc().toIso8601String(),
'platform': platform,
'sourceDisplayName': sourceDisplayName,
'sourceUserId': sourceUserId,
'sourceThreadId': sourceThreadId,
'parsingHints': parsingHints,
'metadata': metadata,
'dedupeKey': dedupeKey,
};
}
factory SharedPayload.fromJson(Map<String, dynamic> json) {
final String payloadTypeName =
json['payloadType'] as String? ?? SharedPayloadType.text.name;
return SharedPayload(
sourceApp: json['sourceApp'] as String? ?? 'share_sheet',
payloadType: SharedPayloadType.values.firstWhere(
(SharedPayloadType value) => value.name == payloadTypeName,
orElse: () => SharedPayloadType.text,
),
rawText: json['rawText'] as String? ?? '',
url: json['url'] as String?,
createdAt: DateTime.parse(
json['createdAt'] as String? ??
DateTime.now().toUtc().toIso8601String(),
).toLocal(),
receivedAt: DateTime.parse(
json['receivedAt'] as String? ??
DateTime.now().toUtc().toIso8601String(),
).toLocal(),
sharedMessageDateTime: json['sharedMessageDateTime'] == null
? null
: DateTime.parse(json['sharedMessageDateTime'] as String).toLocal(),
platform: json['platform'] as String? ?? 'unknown',
sourceDisplayName: json['sourceDisplayName'] as String?,
sourceUserId: json['sourceUserId'] as String?,
sourceThreadId: json['sourceThreadId'] as String?,
parsingHints: (json['parsingHints'] as List<dynamic>? ?? <dynamic>[])
.map((dynamic item) => '$item')
.toList(growable: false),
metadata:
(json['metadata'] as Map<dynamic, dynamic>? ?? <dynamic, dynamic>{})
.map(
(dynamic key, dynamic value) =>
MapEntry<String, String>('$key', '$value'),
),
dedupeKey: json['dedupeKey'] as String?,
);
}
}
/// Draft extracted from a shared payload before it becomes a persistent fact.
@immutable
class CapturedFactDraft {
const CapturedFactDraft({
required this.type,
required this.text,
this.label,
this.dateValue,
this.confidence,
this.isSensitive = false,
this.needsReview = false,
});
final CapturedFactType type;
final String text;
final String? label;
final DateTime? dateValue;
final double? confidence;
final bool isSensitive;
final bool needsReview;
CapturedFactDraft copyWith({
CapturedFactType? type,
String? text,
String? label,
DateTime? dateValue,
double? confidence,
bool? isSensitive,
bool? needsReview,
}) {
return CapturedFactDraft(
type: type ?? this.type,
text: text ?? this.text,
label: label ?? this.label,
dateValue: dateValue ?? this.dateValue,
confidence: confidence ?? this.confidence,
isSensitive: isSensitive ?? this.isSensitive,
needsReview: needsReview ?? this.needsReview,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'type': type.name,
'text': text,
'label': label,
'dateValue': dateValue?.toUtc().toIso8601String(),
'confidence': confidence,
'isSensitive': isSensitive,
'needsReview': needsReview,
};
}
factory CapturedFactDraft.fromJson(Map<String, dynamic> json) {
final String typeName =
json['type'] as String? ?? CapturedFactType.note.name;
return CapturedFactDraft(
type: CapturedFactType.values.firstWhere(
(CapturedFactType value) => value.name == typeName,
orElse: () => CapturedFactType.note,
),
text: json['text'] as String? ?? '',
label: json['label'] as String?,
dateValue: json['dateValue'] == null
? null
: DateTime.parse(json['dateValue'] as String).toLocal(),
confidence: (json['confidence'] as num?)?.toDouble(),
isSensitive: json['isSensitive'] as bool? ?? false,
needsReview: json['needsReview'] as bool? ?? false,
);
}
}
/// Durable record of a share that was already attached to a person.
@immutable
class SharedMessageEntry {
const SharedMessageEntry({
required this.id,
required this.profileId,
required this.payload,
required this.importedAt,
required this.resolvedAutomatically,
this.assignmentStatus = ShareAssignmentStatus.assignedDirectly,
this.draft,
});
final String id;
final String profileId;
final SharedPayload payload;
final DateTime importedAt;
final bool resolvedAutomatically;
final ShareAssignmentStatus assignmentStatus;
final CapturedFactDraft? draft;
String get sourceApp => payload.sourceApp;
String get messageText => payload.rawText;
DateTime get sharedAt => payload.sharedMessageDateTime ?? payload.createdAt;
DateTime? get sharedMessageDateTime => payload.sharedMessageDateTime;
SharedPayloadType get payloadType => payload.payloadType;
String? get url => payload.url;
String? get sourceDisplayName => payload.sourceDisplayName;
String? get sourceUserId => payload.sourceUserId;
String? get sourceThreadId => payload.sourceThreadId;
SharedMessageEntry copyWith({
String? id,
String? profileId,
SharedPayload? payload,
DateTime? importedAt,
bool? resolvedAutomatically,
ShareAssignmentStatus? assignmentStatus,
CapturedFactDraft? draft,
}) {
return SharedMessageEntry(
id: id ?? this.id,
profileId: profileId ?? this.profileId,
payload: payload ?? this.payload,
importedAt: importedAt ?? this.importedAt,
resolvedAutomatically:
resolvedAutomatically ?? this.resolvedAutomatically,
assignmentStatus: assignmentStatus ?? this.assignmentStatus,
draft: draft ?? this.draft,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'profileId': profileId,
'payload': payload.toJson(),
'importedAt': importedAt.toUtc().toIso8601String(),
'resolvedAutomatically': resolvedAutomatically,
'assignmentStatus': assignmentStatus.name,
'draft': draft?.toJson(),
};
}
factory SharedMessageEntry.fromJson(Map<String, dynamic> json) {
final String assignmentStatusName =
json['assignmentStatus'] as String? ??
ShareAssignmentStatus.assignedDirectly.name;
final SharedPayload payload = json['payload'] is Map<String, dynamic>
? SharedPayload.fromJson(json['payload'] as Map<String, dynamic>)
: SharedPayload(
sourceApp: json['sourceApp'] as String? ?? 'share_sheet',
payloadType: SharedPayloadType.text,
rawText: json['messageText'] as String? ?? '',
createdAt: DateTime.parse(
json['sharedAt'] as String? ??
DateTime.now().toUtc().toIso8601String(),
).toLocal(),
receivedAt: DateTime.parse(
json['importedAt'] as String? ??
DateTime.now().toUtc().toIso8601String(),
).toLocal(),
platform: 'legacy',
sourceDisplayName: json['sourceDisplayName'] as String?,
sourceUserId: json['sourceUserId'] as String?,
sourceThreadId: json['sourceThreadId'] as String?,
);
return SharedMessageEntry(
id: json['id'] as String,
profileId: json['profileId'] as String,
payload: payload,
importedAt: DateTime.parse(json['importedAt'] as String).toLocal(),
resolvedAutomatically: json['resolvedAutomatically'] as bool? ?? true,
assignmentStatus: ShareAssignmentStatus.values.firstWhere(
(ShareAssignmentStatus value) => value.name == assignmentStatusName,
orElse: () => ShareAssignmentStatus.assignedDirectly,
),
draft: json['draft'] is Map<String, dynamic>
? CapturedFactDraft.fromJson(json['draft'] as Map<String, dynamic>)
: null,
);
}
}
/// Why the share flow parked a payload in the inbox instead of saving directly.
enum SharedInboxReason {
ambiguousProfileMatch,
nearProfileConflict,
missingIdentity,
}
/// Unresolved share payload waiting for manual triage.
@immutable
class SharedInboxEntry {
const SharedInboxEntry({
required this.id,
required this.payload,
required this.reason,
required this.candidateProfileIds,
required this.normalizedDisplayName,
this.assignmentStatus = ShareAssignmentStatus.needsPersonSelection,
this.targetPersonId,
this.sourceFingerprint,
this.draft,
});
final String id;
final SharedPayload payload;
final SharedInboxReason reason;
final List<String> candidateProfileIds;
final String normalizedDisplayName;
final ShareAssignmentStatus assignmentStatus;
final String? targetPersonId;
final String? sourceFingerprint;
final CapturedFactDraft? draft;
String get sourceApp => payload.sourceApp;
String get messageText => payload.rawText;
DateTime get sharedAt => payload.createdAt;
DateTime get receivedAt => payload.receivedAt;
SharedPayloadType get payloadType => payload.payloadType;
String? get url => payload.url;
String? get sourceDisplayName => payload.sourceDisplayName;
String? get sourceUserId => payload.sourceUserId;
String? get sourceThreadId => payload.sourceThreadId;
SharedInboxEntry copyWith({
String? id,
SharedPayload? payload,
SharedInboxReason? reason,
List<String>? candidateProfileIds,
String? normalizedDisplayName,
ShareAssignmentStatus? assignmentStatus,
String? targetPersonId,
String? sourceFingerprint,
CapturedFactDraft? draft,
}) {
return SharedInboxEntry(
id: id ?? this.id,
payload: payload ?? this.payload,
reason: reason ?? this.reason,
candidateProfileIds: candidateProfileIds ?? this.candidateProfileIds,
normalizedDisplayName:
normalizedDisplayName ?? this.normalizedDisplayName,
assignmentStatus: assignmentStatus ?? this.assignmentStatus,
targetPersonId: targetPersonId ?? this.targetPersonId,
sourceFingerprint: sourceFingerprint ?? this.sourceFingerprint,
draft: draft ?? this.draft,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'payload': payload.toJson(),
'reason': reason.name,
'candidateProfileIds': candidateProfileIds,
'normalizedDisplayName': normalizedDisplayName,
'assignmentStatus': assignmentStatus.name,
'targetPersonId': targetPersonId,
'sourceFingerprint': sourceFingerprint,
'draft': draft?.toJson(),
};
}
factory SharedInboxEntry.fromJson(Map<String, dynamic> json) {
final String reasonName =
json['reason'] as String? ?? SharedInboxReason.missingIdentity.name;
final String assignmentStatusName =
json['assignmentStatus'] as String? ??
ShareAssignmentStatus.needsPersonSelection.name;
final SharedPayload payload = json['payload'] is Map<String, dynamic>
? SharedPayload.fromJson(json['payload'] as Map<String, dynamic>)
: SharedPayload(
sourceApp: json['sourceApp'] as String? ?? 'share_sheet',
payloadType: SharedPayloadType.text,
rawText: json['messageText'] as String? ?? '',
createdAt: DateTime.parse(
json['sharedAt'] as String? ??
DateTime.now().toUtc().toIso8601String(),
).toLocal(),
receivedAt: DateTime.parse(
json['receivedAt'] as String? ??
DateTime.now().toUtc().toIso8601String(),
).toLocal(),
platform: 'legacy',
sourceDisplayName: json['sourceDisplayName'] as String?,
sourceUserId: json['sourceUserId'] as String?,
sourceThreadId: json['sourceThreadId'] as String?,
);
return SharedInboxEntry(
id: json['id'] as String,
payload: payload,
reason: SharedInboxReason.values.firstWhere(
(SharedInboxReason item) => item.name == reasonName,
orElse: () => SharedInboxReason.missingIdentity,
),
candidateProfileIds:
(json['candidateProfileIds'] as List<dynamic>? ?? <dynamic>[])
.map((dynamic id) => '$id')
.toList(growable: false),
normalizedDisplayName: json['normalizedDisplayName'] as String? ?? '',
assignmentStatus: ShareAssignmentStatus.values.firstWhere(
(ShareAssignmentStatus value) => value.name == assignmentStatusName,
orElse: () => ShareAssignmentStatus.needsPersonSelection,
),
targetPersonId: json['targetPersonId'] as String?,
sourceFingerprint: json['sourceFingerprint'] as String?,
draft: json['draft'] is Map<String, dynamic>
? CapturedFactDraft.fromJson(json['draft'] as Map<String, dynamic>)
: null,
);
}
}