mirror of
https://github.com/BlueBubblesApp/bluebubbles-app.git
synced 2025-08-06 19:44:08 +08:00
656 lines
25 KiB
Dart
656 lines
25 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:bluebubbles/helpers/helpers.dart';
|
|
import 'package:bluebubbles/database/html/attachment.dart';
|
|
import 'package:bluebubbles/database/html/chat.dart';
|
|
import 'package:bluebubbles/database/html/handle.dart';
|
|
import 'package:bluebubbles/database/html/objectbox.dart';
|
|
import 'package:bluebubbles/database/models.dart' show AttributedBody, MessageSummaryInfo, PayloadData;
|
|
import 'package:bluebubbles/services/services.dart';
|
|
import 'package:bluebubbles/utils/logger/logger.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:metadata_fetch/metadata_fetch.dart';
|
|
|
|
enum LineType { meToMe, otherToMe, meToOther, otherToOther }
|
|
|
|
class Message {
|
|
int? id;
|
|
int? originalROWID;
|
|
String? guid;
|
|
int? handleId;
|
|
int? otherHandle;
|
|
String? text;
|
|
String? subject;
|
|
String? country;
|
|
DateTime? dateCreated;
|
|
bool? isFromMe;
|
|
// Data detector results
|
|
bool? hasDdResults;
|
|
DateTime? datePlayed;
|
|
int? itemType;
|
|
String? groupTitle;
|
|
int? groupActionType;
|
|
String? balloonBundleId;
|
|
String? associatedMessageGuid;
|
|
int? associatedMessagePart;
|
|
String? associatedMessageType;
|
|
String? expressiveSendStyleId;
|
|
Handle? handle;
|
|
bool hasAttachments;
|
|
bool hasReactions;
|
|
DateTime? dateDeleted;
|
|
Map<String, dynamic>? metadata;
|
|
String? threadOriginatorGuid;
|
|
String? threadOriginatorPart;
|
|
List<Attachment?> attachments = [];
|
|
List<Message> associatedMessages = [];
|
|
bool? bigEmoji;
|
|
List<AttributedBody> attributedBody;
|
|
List<MessageSummaryInfo> messageSummaryInfo;
|
|
PayloadData? payloadData;
|
|
bool hasApplePayloadData;
|
|
bool wasDeliveredQuietly;
|
|
bool didNotifyRecipient;
|
|
bool isBookmarked;
|
|
|
|
final RxInt _error = RxInt(0);
|
|
int get error => _error.value;
|
|
set error(int i) => _error.value = i;
|
|
|
|
final Rxn<DateTime> _dateRead = Rxn<DateTime>();
|
|
DateTime? get dateRead => _dateRead.value;
|
|
set dateRead(DateTime? d) => _dateRead.value = d;
|
|
|
|
final Rxn<DateTime> _dateDelivered = Rxn<DateTime>();
|
|
DateTime? get dateDelivered => _dateDelivered.value;
|
|
set dateDelivered(DateTime? d) => _dateDelivered.value = d;
|
|
|
|
final Rxn<DateTime> _dateEdited = Rxn<DateTime>();
|
|
DateTime? get dateEdited => _dateEdited.value;
|
|
set dateEdited(DateTime? d) => _dateEdited.value = d;
|
|
|
|
final chat = ToOne<Chat>();
|
|
final dbAttachments = <Attachment>[];
|
|
|
|
Message({
|
|
this.id,
|
|
this.originalROWID,
|
|
this.guid,
|
|
this.handleId,
|
|
this.otherHandle,
|
|
this.text,
|
|
this.subject,
|
|
this.country,
|
|
int? error,
|
|
this.dateCreated,
|
|
DateTime? dateRead,
|
|
DateTime? dateDelivered,
|
|
this.isFromMe = true,
|
|
this.hasDdResults = false,
|
|
this.datePlayed,
|
|
this.itemType = 0,
|
|
this.groupTitle,
|
|
this.groupActionType = 0,
|
|
this.balloonBundleId,
|
|
this.associatedMessageGuid,
|
|
this.associatedMessagePart,
|
|
this.associatedMessageType,
|
|
this.expressiveSendStyleId,
|
|
this.handle,
|
|
this.hasAttachments = false,
|
|
this.hasReactions = false,
|
|
this.attachments = const [],
|
|
this.associatedMessages = const [],
|
|
this.dateDeleted,
|
|
this.metadata,
|
|
this.threadOriginatorGuid,
|
|
this.threadOriginatorPart,
|
|
this.attributedBody = const [],
|
|
this.messageSummaryInfo = const [],
|
|
this.payloadData,
|
|
this.hasApplePayloadData = false,
|
|
DateTime? dateEdited,
|
|
this.wasDeliveredQuietly = false,
|
|
this.didNotifyRecipient = false,
|
|
this.isBookmarked = false,
|
|
}) {
|
|
if (error != null) _error.value = error;
|
|
if (dateRead != null) _dateRead.value = dateRead;
|
|
if (dateDelivered != null) _dateDelivered.value = dateDelivered;
|
|
if (dateEdited != null) _dateEdited.value = dateEdited;
|
|
if (attachments.isEmpty) attachments = [];
|
|
if (associatedMessages.isEmpty) associatedMessages = [];
|
|
if (attributedBody.isEmpty) attributedBody = [];
|
|
if (messageSummaryInfo.isEmpty) messageSummaryInfo = [];
|
|
}
|
|
|
|
factory Message.fromMap(Map<String, dynamic> json) {
|
|
final attachments = (json['attachments'] as List? ?? []).map((a) => Attachment.fromMap(a)).toList();
|
|
|
|
List<AttributedBody> attributedBody = [];
|
|
if (json["attributedBody"] != null) {
|
|
if (json['attributedBody'] is Map) {
|
|
json['attributedBody'] = [json['attributedBody']];
|
|
}
|
|
try {
|
|
attributedBody = (json['attributedBody'] as List).map((a) => AttributedBody.fromMap(a)).toList();
|
|
} catch (e, stack) {
|
|
Logger.error('Failed to parse attributed body!', error: e, trace: stack);
|
|
}
|
|
}
|
|
|
|
Map<String, dynamic> metadata = {};
|
|
if (!isNullOrEmpty(json["metadata"])) {
|
|
if (json["metadata"] is String) {
|
|
try {
|
|
metadata = jsonDecode(json["metadata"]);
|
|
} catch (_) {}
|
|
} else {
|
|
metadata = json["metadata"];
|
|
}
|
|
}
|
|
|
|
List<MessageSummaryInfo> msi = [];
|
|
try {
|
|
msi = (json['messageSummaryInfo'] as List? ?? []).map((e) => MessageSummaryInfo.fromJson(e)).toList();
|
|
} catch (e, stack) {
|
|
Logger.error('Failed to parse summary info!', error: e, trace: stack);
|
|
}
|
|
|
|
PayloadData? payloadData;
|
|
try {
|
|
payloadData = json['payloadData'] == null ? null : PayloadData.fromJson(json['payloadData']);
|
|
} catch (e, stack) {
|
|
Logger.error('Failed to parse payload data!', error: e, trace: stack);
|
|
}
|
|
|
|
return Message(
|
|
id: json["ROWID"] ?? json['id'],
|
|
originalROWID: json["originalROWID"],
|
|
guid: json["guid"],
|
|
handleId: json["handleId"] ?? 0,
|
|
otherHandle: json["otherHandle"],
|
|
text: sanitizeString(json["text"] ?? attributedBody.firstOrNull?.string),
|
|
subject: json["subject"],
|
|
country: json["country"],
|
|
error: json["_error"] ?? 0,
|
|
dateCreated: parseDate(json["dateCreated"]),
|
|
dateRead: parseDate(json["dateRead"]),
|
|
dateDelivered: parseDate(json["dateDelivered"]),
|
|
isFromMe: json['isFromMe'] == true,
|
|
hasDdResults: json['hasDdResults'] == true,
|
|
datePlayed: parseDate(json["datePlayed"]),
|
|
itemType: json["itemType"],
|
|
groupTitle: json["groupTitle"],
|
|
groupActionType: json["groupActionType"] ?? 0,
|
|
balloonBundleId: json["balloonBundleId"],
|
|
associatedMessageGuid: json["associatedMessageGuid"]?.toString().replaceAll("bp:", "").split("/").last,
|
|
associatedMessagePart: json["associatedMessagePart"] ?? int.tryParse(json["associatedMessageGuid"].toString().replaceAll("p:", "").split("/").first),
|
|
associatedMessageType: json["associatedMessageType"],
|
|
expressiveSendStyleId: json["expressiveSendStyleId"],
|
|
handle: json['handle'] != null ? Handle.fromMap(json['handle']) : null,
|
|
hasAttachments: attachments.isNotEmpty || json['hasAttachments'] == true,
|
|
attachments: (json['attachments'] as List? ?? []).map((a) => Attachment.fromMap(a)).toList(),
|
|
hasReactions: json['hasReactions'] == true,
|
|
dateDeleted: parseDate(json["dateDeleted"]),
|
|
metadata: metadata is String ? null : metadata,
|
|
threadOriginatorGuid: json['threadOriginatorGuid'],
|
|
threadOriginatorPart: json['threadOriginatorPart'],
|
|
attributedBody: attributedBody,
|
|
messageSummaryInfo: msi,
|
|
payloadData: payloadData,
|
|
hasApplePayloadData: json['hasApplePayloadData'] == true || payloadData != null,
|
|
dateEdited: parseDate(json["dateEdited"]),
|
|
wasDeliveredQuietly: json['wasDeliveredQuietly'] ?? false,
|
|
didNotifyRecipient: json['didNotifyRecipient'] ?? false,
|
|
isBookmarked: json['isBookmarked'] ?? false,
|
|
);
|
|
}
|
|
|
|
Message save({Chat? chat, bool updateIsBookmarked = false}) {
|
|
// Save the participant & set the handle ID to the new participant
|
|
if (handle == null && handleId != null) {
|
|
handle = Handle.findOne(originalROWID: handleId);
|
|
}
|
|
// ignore: argument_type_not_assignable, return_of_invalid_type, invalid_assignment, for_in_of_invalid_element_type
|
|
WebListeners.notifyMessage(this, chat: chat);
|
|
return this;
|
|
}
|
|
|
|
static Future<List<Message>> bulkSaveNewMessages(Chat chat, List<Message> messages) async {
|
|
for (Message m in messages) {
|
|
// Save the participant & set the handle ID to the new participant
|
|
if (m.handle == null && m.handleId != null) {
|
|
m.handle = Handle.findOne(originalROWID: m.handleId);
|
|
}
|
|
// ignore: argument_type_not_assignable, return_of_invalid_type, invalid_assignment, for_in_of_invalid_element_type
|
|
WebListeners.notifyMessage(m, chat: chat);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
static List<Message> bulkSave(List<Message> messages) {
|
|
for (Message m in messages) {
|
|
// Save the participant & set the handle ID to the new participant
|
|
if (m.handle == null && m.handleId != null) {
|
|
m.handle = Handle.findOne(originalROWID: m.handleId);
|
|
}
|
|
// ignore: argument_type_not_assignable, return_of_invalid_type, invalid_assignment, for_in_of_invalid_element_type
|
|
WebListeners.notifyMessage(m);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
static Future<Message> replaceMessage(String? oldGuid, Message newMessage, {bool awaitNewMessageEvent = true, Chat? chat}) async {
|
|
if (newMessage.handle == null && newMessage.handleId != null) {
|
|
newMessage.handle = Handle.findOne(originalROWID: newMessage.handleId);
|
|
}
|
|
// ignore: argument_type_not_assignable, return_of_invalid_type, invalid_assignment, for_in_of_invalid_element_type
|
|
WebListeners.notifyMessage(newMessage, tempGuid: oldGuid, chat: chat);
|
|
return newMessage;
|
|
}
|
|
|
|
Message updateMetadata(Metadata? metadata) {
|
|
return this;
|
|
}
|
|
|
|
Message setPlayedDate({ DateTime? timestamp }) {
|
|
datePlayed = timestamp ?? DateTime.now().toUtc();
|
|
return this;
|
|
}
|
|
|
|
List<Attachment?>? fetchAttachments({ChatLifecycleManager? currentChat}) {
|
|
return attachments;
|
|
}
|
|
|
|
Chat? getChat() {
|
|
return null;
|
|
}
|
|
|
|
Message fetchAssociatedMessages({MessagesService? service, bool shouldRefresh = false}) {
|
|
associatedMessages = (service?.struct.reactions.where((element) => element.associatedMessageGuid == guid).toList() ?? []).cast<Message>();
|
|
if (threadOriginatorGuid != null) {
|
|
final existing = service?.struct.getMessage(threadOriginatorGuid!);
|
|
final threadOriginator = existing;
|
|
// ignore: argument_type_not_assignable, return_of_invalid_type, invalid_assignment, for_in_of_invalid_element_type
|
|
threadOriginator?.handle ??= Handle.findOne(originalROWID: threadOriginator.handleId);
|
|
// ignore: argument_type_not_assignable, return_of_invalid_type, invalid_assignment, for_in_of_invalid_element_type
|
|
if (threadOriginator != null) associatedMessages.add(threadOriginator);
|
|
if (existing == null && threadOriginator != null) service?.struct.addThreadOriginator(threadOriginator);
|
|
}
|
|
associatedMessages.sort((a, b) => a.originalROWID!.compareTo(b.originalROWID!));
|
|
return this;
|
|
}
|
|
|
|
Handle? getHandle() {
|
|
// ignore: argument_type_not_assignable, return_of_invalid_type, invalid_assignment, for_in_of_invalid_element_type
|
|
return chats.webCachedHandles.firstWhereOrNull((element) => element.originalROWID == handleId);
|
|
}
|
|
|
|
static Message? findOne({String? guid, String? associatedMessageGuid}) {
|
|
return null;
|
|
}
|
|
|
|
static List<Message> find() {
|
|
return [];
|
|
}
|
|
|
|
static void delete(String guid) {
|
|
return;
|
|
}
|
|
|
|
static void softDelete(String guid) {
|
|
return;
|
|
}
|
|
|
|
String get fullText => sanitizeString([subject, text].where((e) => !isNullOrEmpty(e)).join("\n"));
|
|
|
|
// first condition is for macOS < 11 and second condition is for macOS >= 11
|
|
bool get isLegacyUrlPreview => (balloonBundleId == "com.apple.messages.URLBalloonProvider" && hasDdResults!)
|
|
|| (hasDdResults! && (text ?? "").trim().isURL);
|
|
|
|
String? get url => text?.replaceAll("\n", " ").split(" ").firstWhereOrNull((String e) => e.hasUrl);
|
|
|
|
bool get isInteractive => balloonBundleId != null && !isLegacyUrlPreview;
|
|
|
|
String get interactiveText {
|
|
String text = "";
|
|
final temp = balloonBundleIdMap[balloonBundleId?.split(":").first] ?? (balloonBundleId?.split(":").first ?? "Unknown");
|
|
if (temp is Map) {
|
|
text = temp[balloonBundleId?.split(":").last] ?? ((balloonBundleId?.split(":").last ?? "Unknown"));
|
|
} else {
|
|
text = temp.toString();
|
|
}
|
|
return text;
|
|
}
|
|
|
|
String? get interactiveMediaPath {
|
|
return null;
|
|
}
|
|
|
|
bool get isGroupEvent => groupTitle != null || (itemType ?? 0) > 0 || (groupActionType ?? 0) > 0;
|
|
|
|
String get groupEventText {
|
|
String text = "Unknown group event";
|
|
handle ??= getHandle();
|
|
String name = handle?.displayName ?? 'You';
|
|
|
|
String? other = "someone";
|
|
if (otherHandle != null && isParticipantEvent) {
|
|
other = Handle.findOne(originalROWID: otherHandle)?.displayName ?? other;
|
|
}
|
|
|
|
if (itemType == 1 && groupActionType == 1) {
|
|
text = "$name removed $other from the conversation";
|
|
} else if (itemType == 1 && groupActionType == 0) {
|
|
text = "$name added $other to the conversation";
|
|
} else if (itemType == 3 && (groupActionType ?? 0) > 0) {
|
|
text = "$name changed the group photo";
|
|
} else if (itemType == 3) {
|
|
text = "$name left the conversation";
|
|
} else if (itemType == 2 && groupTitle != null) {
|
|
text = "$name named the conversation \"$groupTitle\"";
|
|
} else if (itemType == 6) {
|
|
text = "$name started a FaceTime call";
|
|
} else if (itemType == 4 && groupActionType == 0) {
|
|
text = "$name shared ${name == "You" ? "your" : "their"} location";
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
bool get isParticipantEvent => isGroupEvent && ((itemType == 1 && [0, 1].contains(groupActionType)) || [2, 3].contains(itemType));
|
|
|
|
bool get isBigEmoji => bigEmoji ?? MessageHelper.shouldShowBigEmoji(fullText);
|
|
|
|
List<Attachment> get realAttachments => attachments.where((e) => e != null && e.mimeType != null).cast<Attachment>().toList();
|
|
|
|
List<Attachment> get previewAttachments => attachments.where((e) => e != null && e.mimeType == null).cast<Attachment>().toList();
|
|
|
|
List<Message> get reactions => associatedMessages.where((item) =>
|
|
ReactionTypes.toList().contains(item.associatedMessageType?.replaceAll("-", ""))).toList();
|
|
|
|
Indicator get indicatorToShow {
|
|
if (!isFromMe!) return Indicator.NONE;
|
|
if (dateRead != null) return Indicator.READ;
|
|
if (dateDelivered != null) return Indicator.DELIVERED;
|
|
if (dateCreated != null) return Indicator.SENT;
|
|
return Indicator.NONE;
|
|
}
|
|
|
|
bool showTail(Message? newer) {
|
|
// if there is no newer, or if the newer is a different sender
|
|
if (newer == null || !sameSender(newer) || newer.isGroupEvent) return true;
|
|
// if newer is over a minute newer
|
|
return newer.dateCreated!.difference(dateCreated!).inMinutes.abs() > 1;
|
|
}
|
|
|
|
bool sameSender(Message? other) {
|
|
return (isFromMe! && isFromMe == other?.isFromMe) || (!isFromMe! && !(other?.isFromMe ?? true) && handleId == other?.handleId);
|
|
}
|
|
|
|
void generateTempGuid() {
|
|
guid = "temp-${randomString(8)}";
|
|
}
|
|
|
|
static int? countForChat(Chat? chat) {
|
|
return 0;
|
|
}
|
|
|
|
Message mergeWith(Message otherMessage) {
|
|
return Message.merge(this, otherMessage);
|
|
}
|
|
|
|
/// Get what shape the reply line should be
|
|
LineType getLineType(Message? olderMessage, Message threadOriginator) {
|
|
if (olderMessage?.threadOriginatorGuid != threadOriginatorGuid) olderMessage = threadOriginator;
|
|
if (isFromMe! && (olderMessage?.isFromMe ?? false)) {
|
|
return LineType.meToMe;
|
|
} else if (!isFromMe! && (olderMessage?.isFromMe ?? false)) {
|
|
return LineType.meToOther;
|
|
} else if (isFromMe! && !(olderMessage?.isFromMe ?? false)) {
|
|
return LineType.otherToMe;
|
|
} else {
|
|
return LineType.otherToOther;
|
|
}
|
|
}
|
|
|
|
/// Get whether the reply line from the message should connect to the message below
|
|
bool shouldConnectLower(Message? olderMessage, Message? newerMessage, Message threadOriginator) {
|
|
// if theres no newer message or it isn't part of the thread, don't connect
|
|
if (newerMessage == null || newerMessage.threadOriginatorGuid != threadOriginatorGuid) return false;
|
|
// if the line is from me to other or from other to other, don't connect lower.
|
|
// we only want lines ending at messages to me to connect downwards (this
|
|
// helps simplify some things and prevent rendering mistakes)
|
|
if (getLineType(olderMessage, threadOriginator) == LineType.meToOther ||
|
|
getLineType(olderMessage, threadOriginator) == LineType.otherToOther) return false;
|
|
// if the lower message isn't from me, then draw the connecting line
|
|
// (if the message is from me, that message will draw a connecting line up
|
|
// rather than this message drawing one downwards).
|
|
return isFromMe != newerMessage.isFromMe;
|
|
}
|
|
|
|
int get normalizedThreadPart => threadOriginatorPart == null ? 0 : int.parse(threadOriginatorPart![0]);
|
|
|
|
bool connectToUpper() => threadOriginatorGuid != null;
|
|
|
|
bool showUpperMessage(Message olderMessage) {
|
|
// find the part count of the older message
|
|
final olderPartCount = getActiveMwc(olderMessage.guid!)?.parts.length ?? 1;
|
|
// make sure the older message is none of the following:
|
|
// 1) thread originator
|
|
// 2) part of the thread
|
|
// OR
|
|
// 1) It is the thread originator but the part is not the last part of the older message
|
|
// 2) It is part of the thread but has multiple parts
|
|
return (olderMessage.guid != threadOriginatorGuid && olderMessage.threadOriginatorGuid != threadOriginatorGuid)
|
|
|| (olderMessage.guid == threadOriginatorGuid && normalizedThreadPart != olderPartCount - 1)
|
|
|| (olderMessage.threadOriginatorGuid == threadOriginatorGuid && olderPartCount > 1);
|
|
}
|
|
|
|
bool connectToLower(Message newerMessage) {
|
|
final thisPartCount = getActiveMwc(guid!)?.parts.length ?? 1;
|
|
if (newerMessage.isFromMe != isFromMe) return false;
|
|
if (newerMessage.normalizedThreadPart != thisPartCount - 1) return false;
|
|
if (threadOriginatorGuid != null) {
|
|
return newerMessage.threadOriginatorGuid == threadOriginatorGuid;
|
|
} else {
|
|
return newerMessage.threadOriginatorGuid == guid;
|
|
}
|
|
}
|
|
|
|
/// Get whether the reply line from the message should connect to the message above
|
|
bool shouldConnectUpper(Message? olderMessage, Message threadOriginator) {
|
|
// if theres no older message, or it isn't a part of the thread (make sure
|
|
// to check that it isn't actually an outlined bubble representing the
|
|
// thread originator), don't connect
|
|
if (olderMessage == null ||
|
|
(olderMessage.threadOriginatorGuid != threadOriginatorGuid && !upperIsThreadOriginatorBubble(olderMessage))) {
|
|
return false;
|
|
}
|
|
// if the older message is the outlined bubble, or the originator is from
|
|
// someone else and the message is from me, then draw the connecting line
|
|
// (the second condition might be redundant / unnecessary but I left it in
|
|
// just in case)
|
|
if (upperIsThreadOriginatorBubble(olderMessage) ||
|
|
(!threadOriginator.isFromMe! && isFromMe!) ||
|
|
getLineType(olderMessage, threadOriginator) == LineType.meToMe ||
|
|
getLineType(olderMessage, threadOriginator) == LineType.otherToMe) return true;
|
|
// if the upper message is from me, then draw the connecting line
|
|
// (if the message is not from me, that message will draw a connecting line
|
|
// down rather than this message drawing one upwards).
|
|
return isFromMe == olderMessage.isFromMe;
|
|
}
|
|
|
|
/// Get whether the upper bubble is actually the thread originator as the
|
|
/// outlined bubble
|
|
bool upperIsThreadOriginatorBubble(Message? olderMessage) {
|
|
return olderMessage?.threadOriginatorGuid != threadOriginatorGuid;
|
|
}
|
|
|
|
static Message merge(Message existing, Message newMessage) {
|
|
existing.id ??= newMessage.id;
|
|
existing.guid ??= newMessage.guid;
|
|
|
|
// Update date created
|
|
if ((existing.dateCreated == null && newMessage.dateCreated != null) ||
|
|
(existing.dateCreated != null &&
|
|
newMessage.dateCreated != null &&
|
|
existing.dateCreated!.millisecondsSinceEpoch < newMessage.dateCreated!.millisecondsSinceEpoch)) {
|
|
existing.dateCreated = newMessage.dateCreated;
|
|
}
|
|
|
|
// Update date delivered
|
|
if ((existing._dateDelivered.value == null && newMessage._dateDelivered.value != null) ||
|
|
(existing._dateDelivered.value != null &&
|
|
newMessage.dateDelivered != null &&
|
|
existing._dateDelivered.value!.millisecondsSinceEpoch <
|
|
newMessage._dateDelivered.value!.millisecondsSinceEpoch)) {
|
|
existing._dateDelivered.value = newMessage.dateDelivered;
|
|
}
|
|
|
|
// Update date delivered
|
|
if ((existing._dateRead.value == null && newMessage._dateRead.value != null) ||
|
|
(existing._dateRead.value != null &&
|
|
newMessage._dateRead.value != null &&
|
|
existing._dateRead.value!.millisecondsSinceEpoch < newMessage._dateRead.value!.millisecondsSinceEpoch)) {
|
|
existing._dateRead.value = newMessage.dateRead;
|
|
}
|
|
|
|
// Update date played
|
|
if ((existing.datePlayed == null && newMessage.datePlayed != null) ||
|
|
(existing.datePlayed != null &&
|
|
newMessage.datePlayed != null &&
|
|
existing.datePlayed!.millisecondsSinceEpoch < newMessage.datePlayed!.millisecondsSinceEpoch)) {
|
|
existing.datePlayed = newMessage.datePlayed;
|
|
}
|
|
|
|
// Update date deleted
|
|
if ((existing.dateDeleted == null && newMessage.dateDeleted != null) ||
|
|
(existing.dateDeleted != null &&
|
|
newMessage.dateDeleted != null &&
|
|
existing.dateDeleted!.millisecondsSinceEpoch < newMessage.dateDeleted!.millisecondsSinceEpoch)) {
|
|
existing.dateDeleted = newMessage.dateDeleted;
|
|
}
|
|
|
|
// Update date edited (and attr body & message summary info)
|
|
if ((existing.dateEdited == null && newMessage.dateEdited != null) ||
|
|
(existing.dateEdited != null &&
|
|
newMessage.dateEdited != null &&
|
|
existing.dateEdited!.millisecondsSinceEpoch < newMessage.dateEdited!.millisecondsSinceEpoch)) {
|
|
existing.dateEdited = newMessage.dateEdited;
|
|
if (!isNullOrEmpty(newMessage.attributedBody)) {
|
|
existing.attributedBody = newMessage.attributedBody;
|
|
}
|
|
if (!isNullOrEmpty(newMessage.messageSummaryInfo)) {
|
|
existing.messageSummaryInfo = newMessage.messageSummaryInfo;
|
|
}
|
|
}
|
|
|
|
// Update error
|
|
if (existing._error.value != newMessage._error.value) {
|
|
existing._error.value = newMessage._error.value;
|
|
}
|
|
|
|
// Update has Dd results
|
|
if ((existing.hasDdResults == null && newMessage.hasDdResults != null) ||
|
|
(!existing.hasDdResults! && newMessage.hasDdResults!)) {
|
|
existing.hasDdResults = newMessage.hasDdResults;
|
|
}
|
|
|
|
// Update metadata
|
|
existing.metadata = mergeTopLevelDicts(existing.metadata, newMessage.metadata);
|
|
|
|
// Update original ROWID
|
|
if (existing.originalROWID == null && newMessage.originalROWID != null) {
|
|
existing.originalROWID = newMessage.originalROWID;
|
|
}
|
|
|
|
// Update attachments flag
|
|
if (!existing.hasAttachments && newMessage.hasAttachments) {
|
|
existing.hasAttachments = newMessage.hasAttachments;
|
|
}
|
|
|
|
// Update has reactions flag
|
|
if (!existing.hasReactions && newMessage.hasReactions) {
|
|
existing.hasReactions = newMessage.hasReactions;
|
|
}
|
|
|
|
// Update handle
|
|
if (existing.handle?.id == null && newMessage.handle?.id != null) {
|
|
existing.handle = newMessage.handle;
|
|
}
|
|
|
|
// Update attachments
|
|
if (existing.dbAttachments.isEmpty && newMessage.dbAttachments.isNotEmpty) {
|
|
existing.dbAttachments.addAll(newMessage.dbAttachments);
|
|
}
|
|
|
|
if (existing.payloadData == null && newMessage.payloadData != null) {
|
|
existing.payloadData = newMessage.payloadData;
|
|
}
|
|
|
|
if (!existing.wasDeliveredQuietly && newMessage.wasDeliveredQuietly) {
|
|
existing.wasDeliveredQuietly = newMessage.wasDeliveredQuietly;
|
|
}
|
|
|
|
if (!existing.didNotifyRecipient && newMessage.didNotifyRecipient) {
|
|
existing.didNotifyRecipient = newMessage.didNotifyRecipient;
|
|
}
|
|
|
|
existing.isBookmarked = newMessage.isBookmarked;
|
|
|
|
return existing;
|
|
}
|
|
|
|
Map<String, dynamic> toMap({bool includeObjects = false}) {
|
|
final map = {
|
|
"ROWID": id,
|
|
"originalROWID": originalROWID,
|
|
"guid": guid,
|
|
"handleId": handleId,
|
|
"otherHandle": otherHandle,
|
|
"text": sanitizeString(text),
|
|
"subject": subject,
|
|
"country": country,
|
|
"_error": _error.value,
|
|
"dateCreated": dateCreated?.millisecondsSinceEpoch,
|
|
"dateRead": _dateRead.value?.millisecondsSinceEpoch,
|
|
"dateDelivered": _dateDelivered.value?.millisecondsSinceEpoch,
|
|
"isFromMe": isFromMe!,
|
|
"hasDdResults": hasDdResults!,
|
|
"datePlayed": datePlayed?.millisecondsSinceEpoch,
|
|
"itemType": itemType,
|
|
"groupTitle": groupTitle,
|
|
"groupActionType": groupActionType,
|
|
"balloonBundleId": balloonBundleId,
|
|
"associatedMessageGuid": associatedMessageGuid,
|
|
"associatedMessagePart": associatedMessagePart,
|
|
"associatedMessageType": associatedMessageType,
|
|
"expressiveSendStyleId": expressiveSendStyleId,
|
|
"handle": handle?.toMap(),
|
|
"hasAttachments": hasAttachments,
|
|
"hasReactions": hasReactions,
|
|
"dateDeleted": dateDeleted?.millisecondsSinceEpoch,
|
|
"metadata": jsonEncode(metadata),
|
|
"threadOriginatorGuid": threadOriginatorGuid,
|
|
"threadOriginatorPart": threadOriginatorPart,
|
|
"hasApplePayloadData": hasApplePayloadData,
|
|
"dateEdited": dateEdited,
|
|
"wasDeliveredQuietly": wasDeliveredQuietly,
|
|
"didNotifyRecipient": didNotifyRecipient,
|
|
"isBookmarked": isBookmarked,
|
|
};
|
|
if (includeObjects) {
|
|
map['attachments'] = (attachments).map((e) => e!.toMap()).toList();
|
|
map['handle'] = handle?.toMap();
|
|
map['attributedBody'] = attributedBody.map((e) => e.toMap()).toList();
|
|
map['messageSummaryInfo'] = messageSummaryInfo.map((e) => e.toJson()).toList();
|
|
map['payloadData'] = payloadData?.toJson();
|
|
}
|
|
return map;
|
|
}
|
|
}
|