Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions lib/database/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,57 @@ class Database {
Logger.error("Failed to perform database migrations!", error: e, trace: s);
}

try {
if (ss.prefs.getBool('cloudReactionRepair-1') != true) {
// Repair reactions restored from iCloud that kept Apple's "p:<part>/<guid>"
// prefix in associatedMessageGuid — locally reactions link by the bare guid
final query = Database.messages.query(Message_.associatedMessageGuid.contains("/")).build();
final broken = query.find();
query.close();
for (Message m in broken) {
final amg = m.associatedMessageGuid!;
final prefix = amg.substring(0, amg.indexOf("/"));
m.associatedMessageGuid = amg.substring(amg.indexOf("/") + 1);
if (prefix.startsWith("p:")) {
m.associatedMessagePart ??= int.tryParse(prefix.substring(2));
}
}
if (broken.isNotEmpty) Database.messages.putMany(broken);
// Cloud restore used to drop messages whose chat hadn't synced yet while still
// advancing the checkpoint; reset the checkpoints so the next sync re-pages
// from the start and picks those messages up
await ss.prefs.remove("chatSyncToken");
await ss.prefs.remove("attachmentSyncToken");
await ss.prefs.remove("messageSyncToken");
await ss.prefs.setBool('cloudReactionRepair-1', true);
Logger.info("Repaired ${broken.length} cloud-restored reactions and reset cloud sync checkpoints");
}
} catch (e, s) {
Logger.error("Failed to repair cloud-restored reactions!", error: e, trace: s);
}

try {
if (ss.prefs.getBool('cloudChatIdentifierRepair-1') != true) {
// Live-created chats never set chatIdentifier, which cloud sync uses to route
// DM messages — backfill it so the next sync can attach orphaned messages
final allChats = Database.chats.getAll();
final List<Chat> repaired = [];
for (Chat c in allChats) {
if (c.chatIdentifier == null && c.handles.length == 1) {
c.chatIdentifier = c.handles.first.address;
repaired.add(c);
}
}
if (repaired.isNotEmpty) Database.chats.putMany(repaired);
// re-page messages so previously-orphaned ones get attached
await ss.prefs.remove("messageSyncToken");
await ss.prefs.setBool('cloudChatIdentifierRepair-1', true);
Logger.info("Backfilled chatIdentifier on ${repaired.length} chats and reset the message sync checkpoint");
}
} catch (e, s) {
Logger.error("Failed to backfill chat identifiers!", error: e, trace: s);
}

initComplete.complete();
}

Expand Down
45 changes: 42 additions & 3 deletions lib/database/io/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,35 @@ class Message {
chat ??= Chat.findByRustGuid(c.chatId);
}

if (chat?.isRpSms ?? true) return;
final chatIdParts = c.chatId.split(";");
if (chat == null && chatIdParts.length >= 3) {
// older chats may lack chatIdentifier, so fall back to matching a DM by participant address
final ident = chatIdParts[2];
if (ident.isNotEmpty && ident != "null") {
final query = (Database.chats.query(Chat_.dateDeleted.isNull())
..linkMany(Chat_.handles, Handle_.address.equals(ident)))
.build();
final results = query.find();
query.close();
chat = results.firstWhereOrNull((ch) => !ch.isGroup);
}
}
if (chat == null) {
// last resort: route by the original chat guid carried in proto4
try {
if (c.msgProto4 != null) {
final proto4 = api.decodeMessageproto4(wrapped: c.msgProto4!);
if (proto4.groupId != null) {
chat = Chat.findByRustGuid(proto4.groupId!);
}
}
} catch (_) {}
}
if (chat == null) {
Logger.warn("Cloud message ${c.guid} references unknown chat ${c.chatId}; skipping");
return;
}
if (chat.isRpSms) return;

Logger.info("Syncing new message");

Expand Down Expand Up @@ -1164,8 +1192,19 @@ class Message {
associatedMessageType = "-${ReactionTypes.toList()[proto1.associatedMessageType! - 3000]}";
}
}
associatedMessageGuid = proto1.associatedMessageGuid;
associatedMessagePart = attributedBody.firstOrNull?.runs.firstWhereOrNull((b) => b.range[0] == proto1.associatedMessageRangeLocation && b.range[1] == proto1.associatedMessageRangeLength)?.attributes?.messagePart;
// the cloud stores the target in Apple's prefixed form ("p:<part>/<guid>"),
// but locally reactions are linked by the bare guid
var amg = proto1.associatedMessageGuid;
int? amgPart;
if (amg != null && amg.contains("/")) {
var prefix = amg.substring(0, amg.indexOf("/"));
amg = amg.substring(amg.indexOf("/") + 1);
if (prefix.startsWith("p:")) {
amgPart = int.tryParse(prefix.substring(2));
}
}
associatedMessageGuid = amg;
associatedMessagePart = amgPart ?? attributedBody.firstOrNull?.runs.firstWhereOrNull((b) => b.range[0] == proto1.associatedMessageRangeLocation && b.range[1] == proto1.associatedMessageRangeLength)?.attributes?.messagePart;
guid = c.guid;
var bits = c.flags.bits();
isFromMe = (bits & IS_FROM_ME) != 0;
Expand Down
2 changes: 2 additions & 0 deletions lib/services/rustpush/rustpush_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ class RustPushBackend implements BackendService {
var formattedHandles = addresses.map((e) => RustPushBBUtils.rustHandleToBB(e)).toList();
var chat = Chat(
guid: existingGuid ?? uuid.v4(),
// cloud sync routes DM messages by chatIdentifier; leaving it null orphans them
chatIdentifier: formattedHandles.length == 1 ? formattedHandles[0].address : null,
participants: formattedHandles,
usingHandle: handle,
isRpSms: service == "SMS",
Expand Down