Add duplicate profile merge and share deep-link routing
This commit is contained in:
@@ -483,6 +483,179 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
]);
|
||||
}
|
||||
|
||||
/// Merge one person profile into another and rebind related records.
|
||||
Future<void> mergePersonProfiles({
|
||||
required String sourcePersonId,
|
||||
required String targetPersonId,
|
||||
}) async {
|
||||
if (sourcePersonId == targetPersonId) {
|
||||
throw ArgumentError('Source and target profile must be different');
|
||||
}
|
||||
|
||||
final LocalDataState current = _requireState();
|
||||
final PersonProfile? source = _findPersonById(
|
||||
current.people,
|
||||
sourcePersonId,
|
||||
);
|
||||
final PersonProfile? target = _findPersonById(
|
||||
current.people,
|
||||
targetPersonId,
|
||||
);
|
||||
if (source == null || target == null) {
|
||||
throw ArgumentError('Both source and target profiles must exist');
|
||||
}
|
||||
|
||||
final PersonProfile mergedTarget = target.copyWith(
|
||||
affinityScore: target.affinityScore > source.affinityScore
|
||||
? target.affinityScore
|
||||
: source.affinityScore,
|
||||
nextMoment: source.nextMoment.isBefore(target.nextMoment)
|
||||
? source.nextMoment
|
||||
: target.nextMoment,
|
||||
tags: _mergeTags(target.tags, source.tags),
|
||||
notes: _mergeNotes(
|
||||
targetNotes: target.notes,
|
||||
sourceName: source.name,
|
||||
sourceNotes: source.notes,
|
||||
),
|
||||
location: _effectiveLocation(target.location, source.location),
|
||||
);
|
||||
|
||||
final List<PersonProfile> nextPeople = current.people
|
||||
.where((PersonProfile person) => person.id != sourcePersonId)
|
||||
.map(
|
||||
(PersonProfile person) =>
|
||||
person.id == targetPersonId ? mergedTarget : person,
|
||||
)
|
||||
.toList(growable: false);
|
||||
|
||||
final List<RelationshipMoment> updatedMoments = <RelationshipMoment>[];
|
||||
final List<RelationshipMoment> nextMoments = current.moments
|
||||
.map((RelationshipMoment moment) {
|
||||
if (moment.personId != sourcePersonId) {
|
||||
return moment;
|
||||
}
|
||||
final RelationshipMoment merged = moment.copyWith(
|
||||
personId: targetPersonId,
|
||||
);
|
||||
updatedMoments.add(merged);
|
||||
return merged;
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
final List<RelationshipIdea> updatedIdeas = <RelationshipIdea>[];
|
||||
final List<RelationshipIdea> nextIdeas = current.ideas
|
||||
.map((RelationshipIdea idea) {
|
||||
if (idea.personId != sourcePersonId) {
|
||||
return idea;
|
||||
}
|
||||
final RelationshipIdea merged = idea.copyWith(
|
||||
personId: targetPersonId,
|
||||
);
|
||||
updatedIdeas.add(merged);
|
||||
return merged;
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
final List<ReminderRule> updatedReminders = <ReminderRule>[];
|
||||
final List<ReminderRule> nextReminders = current.reminders
|
||||
.map((ReminderRule reminder) {
|
||||
if (reminder.personId != sourcePersonId) {
|
||||
return reminder;
|
||||
}
|
||||
final ReminderRule merged = reminder.copyWith(
|
||||
personId: targetPersonId,
|
||||
);
|
||||
updatedReminders.add(merged);
|
||||
return merged;
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
final List<SourceProfileLink> nextLinks = current.sourceLinks
|
||||
.map((SourceProfileLink link) {
|
||||
if (link.profileId != sourcePersonId) {
|
||||
return link;
|
||||
}
|
||||
return link.copyWith(profileId: targetPersonId);
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
final List<SharedMessageEntry> nextMessages = current.sharedMessages
|
||||
.map((SharedMessageEntry entry) {
|
||||
if (entry.profileId != sourcePersonId) {
|
||||
return entry;
|
||||
}
|
||||
return entry.copyWith(profileId: targetPersonId);
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
final List<SharedInboxEntry> nextInbox = current.sharedInbox
|
||||
.map((SharedInboxEntry entry) {
|
||||
if (!entry.candidateProfileIds.contains(sourcePersonId)) {
|
||||
return entry;
|
||||
}
|
||||
final List<String> candidates = entry.candidateProfileIds
|
||||
.map(
|
||||
(String profileId) =>
|
||||
profileId == sourcePersonId ? targetPersonId : profileId,
|
||||
)
|
||||
.toSet()
|
||||
.toList(growable: false);
|
||||
return entry.copyWith(candidateProfileIds: candidates);
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
await _setState(
|
||||
current.copyWith(
|
||||
people: nextPeople,
|
||||
moments: nextMoments,
|
||||
ideas: nextIdeas,
|
||||
reminders: nextReminders,
|
||||
sourceLinks: nextLinks,
|
||||
sharedMessages: nextMessages,
|
||||
sharedInbox: nextInbox,
|
||||
),
|
||||
);
|
||||
|
||||
await _enqueueChanges(<ChangeEnvelope>[
|
||||
_buildEnvelope(
|
||||
entityType: 'person',
|
||||
entityId: mergedTarget.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _personPayload(mergedTarget),
|
||||
),
|
||||
_buildEnvelope(
|
||||
entityType: 'person',
|
||||
entityId: sourcePersonId,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
...updatedMoments.map(
|
||||
(RelationshipMoment moment) => _buildEnvelope(
|
||||
entityType: 'capture',
|
||||
entityId: moment.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _momentPayload(moment),
|
||||
),
|
||||
),
|
||||
...updatedIdeas.map(
|
||||
(RelationshipIdea idea) => _buildEnvelope(
|
||||
entityType: _ideaEntityType(idea.type),
|
||||
entityId: idea.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _ideaPayload(idea),
|
||||
),
|
||||
),
|
||||
...updatedReminders.map(
|
||||
(ReminderRule reminder) => _buildEnvelope(
|
||||
entityType: 'reminderRule',
|
||||
entityId: reminder.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _reminderPayload(reminder),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> addMoment({
|
||||
required String personId,
|
||||
required String summary,
|
||||
@@ -1264,6 +1437,49 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _mergeTags(List<String> primary, List<String> secondary) {
|
||||
final List<String> merged = <String>[];
|
||||
final Set<String> seen = <String>{};
|
||||
for (final String raw in <String>[...primary, ...secondary]) {
|
||||
final String tag = raw.trim();
|
||||
if (tag.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final String normalized = tag.toLowerCase();
|
||||
if (seen.add(normalized)) {
|
||||
merged.add(tag);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
String _mergeNotes({
|
||||
required String targetNotes,
|
||||
required String sourceName,
|
||||
required String sourceNotes,
|
||||
}) {
|
||||
final String target = targetNotes.trim();
|
||||
final String source = sourceNotes.trim();
|
||||
if (target.isEmpty) {
|
||||
return source;
|
||||
}
|
||||
if (source.isEmpty) {
|
||||
return target;
|
||||
}
|
||||
if (target == source) {
|
||||
return target;
|
||||
}
|
||||
return '$target\n\nMerged from $sourceName:\n$source';
|
||||
}
|
||||
|
||||
String? _effectiveLocation(String? target, String? source) {
|
||||
final String? targetValue = _trimToNull(target);
|
||||
if (targetValue != null) {
|
||||
return targetValue;
|
||||
}
|
||||
return _trimToNull(source);
|
||||
}
|
||||
|
||||
Future<SharedMessageIngestResult> _queueSharedInbox({
|
||||
required LocalDataState current,
|
||||
required String sourceApp,
|
||||
|
||||
Reference in New Issue
Block a user