Generate thumbnails for quote attachments.

This commit is contained in:
Greyson Parrelli
2025-08-26 12:54:16 -04:00
parent 71dd1d9d8b
commit d4c1c39179
22 changed files with 276 additions and 148 deletions

View File

@@ -309,7 +309,7 @@ public class InputPanel extends ConstraintLayout
quoteView.getAuthor().getId(),
quoteView.getBody().toString(),
false,
quoteView.getAttachments(),
quoteView.getAttachment(),
quoteView.getMentions(),
quoteView.getQuoteType(),
quoteView.getBodyRanges()));

View File

@@ -464,8 +464,13 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
return body;
}
public List<Attachment> getAttachments() {
return attachments.asAttachments();
public @Nullable Attachment getAttachment() {
List<Attachment> converted = attachments.asAttachments();
if (converted.size() > 0) {
return converted.get(0);
} else {
return null;
}
}
public @NonNull QuoteModel.Type getQuoteType() {

View File

@@ -185,7 +185,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
val id = SignalDatabase.messages.insertMessageOutbox(
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i"),
threadId = targetThread
)
).messageId
SignalDatabase.messages.markAsSent(id, true)
} else {
SignalDatabase.messages.insertMessageInbox(
@@ -215,7 +215,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
val id = SignalDatabase.messages.insertMessageOutbox(
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i", attachments = listOf(attachment)),
threadId = targetThread
)
).messageId
SignalDatabase.messages.markAsSent(id, true)
SignalDatabase.attachments.getAttachmentsForMessage(id).forEach {
SignalDatabase.attachments.debugMakeValidForArchive(it.attachmentId)
@@ -249,7 +249,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
splitThreadId,
false,
null
)
).messageId
SignalDatabase.messages.markAsSent(messageId, true)
SignalDatabase.threads.update(splitThreadId, true)

View File

@@ -95,12 +95,15 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.jobs.GenerateAudioWaveFormJob
import org.thoughtcrime.securesms.mms.DecryptableUri
import org.thoughtcrime.securesms.mms.MediaStream
import org.thoughtcrime.securesms.mms.MmsException
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.FileUtils
import org.thoughtcrime.securesms.util.ImageCompressionUtil
import org.thoughtcrime.securesms.util.JsonUtils.SaneJSONObject
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.RemoteConfig
@@ -299,6 +302,9 @@ class AttachmentTable(
ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP, ARCHIVE_CDN, ARCHIVE_TRANSFER_STATE, THUMBNAIL_FILE, THUMBNAIL_RESTORE_STATE, THUMBNAIL_RANDOM
)
private const val QUOTE_THUMBNAIL_DIMEN = 150
private const val QUOTE_IMAGE_QUALITY = 80
@JvmStatic
@Throws(IOException::class)
fun newDataFile(context: Context): File {
@@ -1789,7 +1795,7 @@ class AttachmentTable(
try {
for (attachment in quoteAttachment) {
val attachmentId = when {
attachment.uri != null -> insertAttachmentWithData(mmsId, attachment, true)
attachment.uri != null -> insertQuoteAttachment(mmsId, attachment)
attachment is ArchivedAttachment -> insertArchivedAttachment(mmsId, attachment, true)
else -> insertUndownloadedAttachment(mmsId, attachment, true)
}
@@ -2409,6 +2415,104 @@ class AttachmentTable(
return attachmentId
}
/**
* When inserting a quote attachment, it looks a lot like a normal attachment insert, but rather than insert the actual data pointed at by the attachment's
* URI, we instead want to generate a thumbnail of that attachment and use that instead.
*/
@Throws(MmsException::class)
private fun insertQuoteAttachment(messageId: Long, attachment: Attachment): AttachmentId {
Log.d(TAG, "[insertQuoteAttachment] Inserting quote attachment for messageId $messageId.")
val thumbnail = generateQuoteThumbnail(DecryptableUri(attachment.uri!!), attachment.contentType)
if (thumbnail != null) {
Log.d(TAG, "[insertQuoteAttachment] Successfully generated quote thumbnail for messageId $messageId.")
return insertAttachmentWithData(
messageId = messageId,
dataStream = thumbnail.data.inputStream(),
attachment = attachment,
quote = true
)
}
Log.d(TAG, "[insertQuoteAttachment] Unable to generate quote thumbnail for messageId $messageId. Content type: ${attachment.contentType}")
val attachmentId: AttachmentId = writableDatabase.withinTransaction { db ->
val contentValues = ContentValues().apply {
put(MESSAGE_ID, messageId)
put(CONTENT_TYPE, attachment.contentType)
put(VOICE_NOTE, attachment.voiceNote.toInt())
put(BORDERLESS, attachment.borderless.toInt())
put(VIDEO_GIF, attachment.videoGif.toInt())
put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE)
put(DATA_SIZE, 0)
put(WIDTH, attachment.width)
put(HEIGHT, attachment.height)
put(QUOTE, 1)
put(BLUR_HASH, attachment.blurHash?.hash)
put(FILE_NAME, attachment.fileName)
attachment.stickerLocator?.let { sticker ->
put(STICKER_PACK_ID, sticker.packId)
put(STICKER_PACK_KEY, sticker.packKey)
put(STICKER_ID, sticker.stickerId)
put(STICKER_EMOJI, sticker.emoji)
}
}
val rowId = db.insert(TABLE_NAME, null, contentValues)
AttachmentId(rowId)
}
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
return attachmentId
}
private fun generateQuoteThumbnail(uri: DecryptableUri, contentType: String?): ImageCompressionUtil.Result? {
return try {
when {
MediaUtil.isImageType(contentType) -> {
val hasTransparency = MediaUtil.isPngType(contentType) || MediaUtil.isWebpType(contentType)
val outputFormat = if (hasTransparency) MediaUtil.IMAGE_WEBP else MediaUtil.IMAGE_JPEG
ImageCompressionUtil.compress(
context,
contentType,
outputFormat,
uri,
QUOTE_THUMBNAIL_DIMEN,
QUOTE_IMAGE_QUALITY
)
}
MediaUtil.isVideoType(contentType) -> {
val videoThumbnail = MediaUtil.getVideoThumbnail(context, uri.uri)
if (videoThumbnail != null) {
ImageCompressionUtil.compress(
context,
MediaUtil.IMAGE_JPEG,
MediaUtil.IMAGE_JPEG,
uri,
QUOTE_THUMBNAIL_DIMEN,
QUOTE_IMAGE_QUALITY
)
} else {
Log.w(TAG, "[generateQuoteThumbnail] Failed to extract video thumbnail")
null
}
}
else -> {
Log.w(TAG, "[generateQuoteThumbnail] Unsupported content type for thumbnail generation: $contentType")
null
}
}
} catch (e: BitmapDecodingException) {
Log.w(TAG, "[generateQuoteThumbnail] Failed to decode image for thumbnail", e)
null
} catch (e: Exception) {
Log.w(TAG, "[generateQuoteThumbnail] Failed to generate thumbnail", e)
null
}
}
/**
* Attachments need records in the database even if they haven't been downloaded yet. That allows us to store the info we need to download it, what message
* it's associated with, etc. We treat this case separately from attachments with data (see [insertAttachmentWithData]) because it's much simpler,

View File

@@ -2548,11 +2548,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val quoteText = cursor.requireString(QUOTE_BODY)
val quoteType = cursor.requireInt(QUOTE_TYPE)
val quoteMissing = cursor.requireBoolean(QUOTE_MISSING)
val quoteAttachments: List<Attachment> = associatedAttachments.filter { it.quote }.toList()
val quoteAttachment: Attachment? = associatedAttachments.filter { it.quote }.firstOrNull()
val quoteMentions: List<Mention> = parseQuoteMentions(cursor)
val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor)
val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) {
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachment != null)) {
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachment, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
} else {
null
}
@@ -2776,7 +2776,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().encode())
}
quoteAttachments += retrieved.quote.attachments
retrieved.quote.attachment?.let { quoteAttachments += it }
} else {
contentValues.put(QUOTE_ID, 0)
contentValues.put(QUOTE_AUTHOR, 0)
@@ -2869,7 +2869,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
messageId = messageId,
threadId = threadId,
threadWasNewlyCreated = threadIdResult.newlyCreated,
insertedAttachments = insertedAttachments
insertedAttachments = insertedAttachments,
quoteAttachmentId = quoteAttachments.firstOrNull()?.let { insertedAttachments?.get(it) }
)
)
}
@@ -2982,7 +2983,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
threadId: Long,
forceSms: Boolean = false,
insertListener: InsertListener? = null
): Long {
): InsertResult {
return insertMessageOutbox(
message = message,
threadId = threadId,
@@ -2999,7 +3000,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
forceSms: Boolean,
defaultReceiptStatus: Int,
insertListener: InsertListener?
): Long {
): InsertResult {
var type = MessageTypes.BASE_SENDING_TYPE
var hasSpecialType = false
@@ -3218,7 +3219,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
if (editedMessage == null) {
quoteAttachments += message.outgoingQuote.attachments
message.outgoingQuote.attachment?.let { quoteAttachments += it }
}
} else {
contentValues.put(QUOTE_ID, 0)
@@ -3320,7 +3321,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
TrimThreadJob.enqueueAsync(threadId)
return messageId
return InsertResult(
messageId = messageId,
threadId = threadId,
threadWasNewlyCreated = false,
insertedAttachments = insertedAttachments,
quoteAttachmentId = quoteAttachments.firstOrNull()?.let { insertedAttachments?.get(it) }
)
}
private fun hasAudioAttachment(attachments: List<Attachment>): Boolean {
@@ -5255,7 +5262,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
timetamp = this.requireLong(DATE_SENT)
),
expirationInfo = null,
storyType = StoryType.fromCode(this.requireInt(STORY_TYPE)),
storyType = fromCode(this.requireInt(STORY_TYPE)),
dateReceived = this.requireLong(DATE_RECEIVED)
)
}
@@ -5406,7 +5413,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val messageId: Long,
val threadId: Long,
val threadWasNewlyCreated: Boolean,
val insertedAttachments: Map<Attachment, AttachmentId>? = null
val insertedAttachments: Map<Attachment, AttachmentId>? = null,
val quoteAttachmentId: AttachmentId? = null
)
data class MessageReceiptUpdate(

View File

@@ -1291,7 +1291,7 @@ final class GroupManagerV2 {
} else {
long threadId = SignalDatabase.threads().getOrCreateValidThreadId(outgoingMessage.getThreadRecipient(), -1, outgoingMessage.getDistributionType());
try {
long messageId = SignalDatabase.messages().insertMessageOutbox(outgoingMessage, threadId, false, null);
long messageId = SignalDatabase.messages().insertMessageOutbox(outgoingMessage, threadId, false, null).getMessageId();
SignalDatabase.messages().markAsSent(messageId, true);
SignalDatabase.threads().update(threadId, true, true);
} catch (MmsException e) {

View File

@@ -676,7 +676,7 @@ class GroupsV2StateProcessor private constructor(
try {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val id = SignalDatabase.messages.insertMessageOutbox(leaveMessage, threadId, false, null)
val id = SignalDatabase.messages.insertMessageOutbox(leaveMessage, threadId, false, null).messageId
SignalDatabase.messages.markAsSent(id, true)
SignalDatabase.drafts.clearDrafts(threadId)
SignalDatabase.threads.update(threadId, unarchive = false, allowDeletion = false)
@@ -733,7 +733,7 @@ class GroupsV2StateProcessor private constructor(
val recipient = Recipient.resolved(recipientId)
val outgoingMessage = OutgoingMessage.groupUpdateMessage(recipient, updateDescription, timestamp)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null).messageId
SignalDatabase.messages.markAsSent(messageId, true)
SignalDatabase.threads.update(threadId, unarchive = false, allowDeletion = false)

View File

@@ -77,6 +77,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulRespons
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import org.whispersystems.signalservice.internal.push.AttachmentPointer;
import org.whispersystems.signalservice.internal.push.BodyRange;
import java.io.ByteArrayInputStream;
@@ -363,48 +364,19 @@ public abstract class PushSendJob extends SendJob {
List<BodyRange> bodyRanges = getBodyRanges(message.getOutgoingQuote().getBodyRanges());
QuoteModel.Type quoteType = message.getOutgoingQuote().getType();
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
Optional<Attachment> localQuoteAttachment = message.getOutgoingQuote()
.getAttachments()
.stream()
.filter(a -> !MediaUtil.isViewOnceType(a.contentType))
.findFirst();
Optional<Attachment> localQuoteAttachment = Optional.ofNullable(message.getOutgoingQuote()).map(QuoteModel::getAttachment);
if (localQuoteAttachment.isPresent() && MediaUtil.isViewOnceType(localQuoteAttachment.get().contentType)) {
localQuoteAttachment = Optional.empty();
}
if (localQuoteAttachment.isPresent()) {
Attachment attachment = localQuoteAttachment.get();
Attachment attachment = localQuoteAttachment.get();
SignalServiceAttachment quoteAttachmentPointer = getAttachmentPointerFor(localQuoteAttachment.get());
ImageCompressionUtil.Result thumbnailData = null;
SignalServiceAttachment thumbnail = null;
try {
if (MediaUtil.isImageType(attachment.contentType) && attachment.getUri() != null) {
thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50);
} else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.contentType) && attachment.getUri() != null) {
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getUri(), 1000);
if (bitmap != null) {
thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50);
}
}
if (thumbnailData != null) {
SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder()
.withContentType(thumbnailData.getMimeType())
.withWidth(thumbnailData.getWidth())
.withHeight(thumbnailData.getHeight())
.withLength(thumbnailData.getData().length)
.withStream(new ByteArrayInputStream(thumbnailData.getData()))
.withResumableUploadSpec(AppDependencies.getSignalServiceMessageSender().getResumableUploadSpec())
.withUuid(UUID.randomUUID());
thumbnail = builder.build();
}
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.videoGif ? MediaUtil.IMAGE_GIF : attachment.contentType,
attachment.fileName,
thumbnail));
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
}
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.videoGif ? MediaUtil.IMAGE_GIF : attachment.contentType,
attachment.fileName,
quoteAttachmentPointer));
}
Recipient quoteAuthorRecipient = Recipient.resolved(quoteAuthor);

View File

@@ -52,8 +52,8 @@ public abstract class SendJob extends BaseJob {
attachments.addAll(Stream.of(message.getLinkPreviews()).map(lp -> lp.getThumbnail().orElse(null)).withoutNulls().toList());
attachments.addAll(Stream.of(message.getSharedContacts()).map(Contact::getAvatarAttachment).withoutNulls().toList());
if (message.getOutgoingQuote() != null) {
attachments.addAll(message.getOutgoingQuote().getAttachments());
if (message.getOutgoingQuote() != null && message.getOutgoingQuote().getAttachment() != null) {
attachments.add(message.getOutgoingQuote().getAttachment());
}
AttachmentTable database = SignalDatabase.attachments();

View File

@@ -443,7 +443,7 @@ object DataMessageProcessor {
}
parentStoryId = DirectReply(storyId)
quoteModel = QuoteModel(sentTimestamp, authorRecipientId, displayText, false, story.slideDeck.asAttachments(), emptyList(), QuoteModel.Type.NORMAL, bodyRanges)
quoteModel = QuoteModel(sentTimestamp, authorRecipientId, displayText, false, story.slideDeck.asAttachments().firstOrNull(), emptyList(), QuoteModel.Type.NORMAL, bodyRanges)
expiresIn = message.expireTimerDuration
} else {
warn(envelope.timestamp!!, "Story has reactions disabled. Dropping reaction.")
@@ -769,7 +769,7 @@ object DataMessageProcessor {
bodyRanges = story.messageRanges
}
quoteModel = QuoteModel(sentTimestamp, storyAuthorRecipientId, displayText, false, story.slideDeck.asAttachments(), emptyList(), QuoteModel.Type.NORMAL, bodyRanges)
quoteModel = QuoteModel(sentTimestamp, storyAuthorRecipientId, displayText, false, story.slideDeck.asAttachments().firstOrNull(), emptyList(), QuoteModel.Type.NORMAL, bodyRanges)
expiresInMillis = message.expireTimerDuration
} else {
warn(envelope.timestamp!!, "Story has replies disabled. Dropping reply.")
@@ -898,7 +898,7 @@ object DataMessageProcessor {
SignalDatabase.messages.beginTransaction()
try {
val quote: QuoteModel? = getValidatedQuote(context, envelope.timestamp!!, message, senderRecipient, threadRecipient)
val quoteModel: QuoteModel? = getValidatedQuote(context, envelope.timestamp!!, message, senderRecipient, threadRecipient)
val contacts: List<Contact> = getContacts(message)
val linkPreviews: List<LinkPreview> = getLinkPreviews(message.preview, message.body ?: "", false)
val mentions: List<Mention> = getMentions(message.bodyRanges.take(BODY_RANGE_PROCESSING_LIMIT))
@@ -920,7 +920,7 @@ object DataMessageProcessor {
body = message.body?.ifEmpty { null },
groupId = groupId,
attachments = attachments + if (sticker != null) listOf(sticker) else emptyList(),
quote = quote,
quote = quoteModel,
sharedContacts = contacts,
linkPreviews = linkPreviews,
mentions = mentions,
@@ -1092,24 +1092,31 @@ object DataMessageProcessor {
if (quotedMessage != null && isSenderValid(quotedMessage, timestamp, senderRecipient, threadRecipient) && !quotedMessage.isRemoteDelete) {
log(timestamp, "Found matching message record...")
val attachments: MutableList<Attachment> = mutableListOf()
val mentions: MutableList<Mention> = mutableListOf()
var thumbnailAttachment: Attachment? = null
val targetMessageAttachments = SignalDatabase.attachments.getAttachmentsForMessage(quotedMessage.id)
val mentions: List<Mention> = SignalDatabase.mentions.getMentionsForMessage(quotedMessage.id)
quotedMessage = quotedMessage.withAttachments(SignalDatabase.attachments.getAttachmentsForMessage(quotedMessage.id))
mentions.addAll(SignalDatabase.mentions.getMentionsForMessage(quotedMessage.id))
// We want our thumbnail attachment to be the first "thumbnailable" item from the target message.
// That means we want to pick the earliest image/video that has data.
thumbnailAttachment = targetMessageAttachments
.sortedBy { it.displayOrder }
.sortedBy {
if (MediaUtil.isImageType(it.contentType) || MediaUtil.isVideoType(it.contentType)) {
0
} else {
1
}
}
.firstOrNull { it.hasData }
if (quotedMessage.isViewOnce) {
attachments.add(TombstoneAttachment(MediaUtil.VIEW_ONCE, true))
} else {
attachments += quotedMessage.slideDeck.asAttachments()
if (attachments.isEmpty()) {
attachments += quotedMessage
.linkPreviews
.filter { it.thumbnail.isPresent }
.map { it.thumbnail.get() }
}
thumbnailAttachment = TombstoneAttachment(MediaUtil.VIEW_ONCE, true)
} else if (thumbnailAttachment == null) {
thumbnailAttachment = quotedMessage
.linkPreviews
.filter { it.thumbnail.isPresent }
.map { it.thumbnail.get() }
.firstOrNull()
}
if (quotedMessage.isPaymentNotification) {
@@ -1119,14 +1126,14 @@ object DataMessageProcessor {
val body = if (quotedMessage.isPaymentNotification) quotedMessage.getDisplayBody(context).toString() else quotedMessage.body
return QuoteModel(
quote.id!!,
authorId,
body,
false,
attachments,
mentions,
QuoteModel.Type.fromProto(quote.type),
quotedMessage.messageRanges
id = quote.id!!,
author = authorId,
text = body,
isOriginalMissing = false,
attachment = thumbnailAttachment,
mentions = mentions,
type = QuoteModel.Type.fromProto(quote.type),
bodyRanges = quotedMessage.messageRanges
)
} else if (quotedMessage != null && quotedMessage.isRemoteDelete) {
warn(timestamp, "Found the target for the quote, but it's flagged as remotely deleted.")
@@ -1134,14 +1141,14 @@ object DataMessageProcessor {
warn(timestamp, "Didn't find matching message record...")
return QuoteModel(
quote.id!!,
authorId,
quote.text ?: "",
true,
quote.attachments.mapNotNull { PointerAttachment.forPointer(it).orNull() },
getMentions(quote.bodyRanges),
QuoteModel.Type.fromProto(quote.type),
quote.bodyRanges.filter { it.mentionAci == null }.toBodyRangeList()
id = quote.id!!,
author = authorId,
text = quote.text ?: "",
isOriginalMissing = true,
attachment = quote.attachments.firstNotNullOfOrNull { PointerAttachment.forPointer(it).orNull() },
mentions = getMentions(quote.bodyRanges),
type = QuoteModel.Type.fromProto(quote.type),
bodyRanges = quote.bodyRanges.filter { it.mentionAci == null }.toBodyRangeList()
)
}

View File

@@ -128,7 +128,7 @@ object EditMessageProcessor {
targetQuote.author,
targetQuote.displayText.toString(),
targetQuote.isOriginalMissing,
emptyList(),
null,
null,
targetQuote.quoteType,
null

View File

@@ -372,7 +372,7 @@ object SyncMessageProcessor {
messageToEdit = targetMessage.id
)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
updateGroupReceiptStatus(sent, messageId, toRecipient.requireGroupId())
} else {
val outgoingTextMessage = OutgoingMessage(
@@ -386,7 +386,7 @@ object SyncMessageProcessor {
bodyRanges = bodyRanges,
messageToEdit = targetMessage.id
)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingTextMessage, threadId, false, null)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingTextMessage, threadId, false, null).messageId
SignalDatabase.messages.markUnidentified(messageId, sent.isUnidentified(toRecipient.serviceId.orNull()))
}
@@ -414,14 +414,14 @@ object SyncMessageProcessor {
val targetQuote = (targetMessage as? MmsMessageRecord)?.quote
val quote: QuoteModel? = if (targetQuote != null && message.quote != null) {
QuoteModel(
targetQuote.id,
targetQuote.author,
targetQuote.displayText.toString(),
targetQuote.isOriginalMissing,
emptyList(),
null,
targetQuote.quoteType,
null
id = targetQuote.id,
author = targetQuote.author,
text = targetQuote.displayText.toString(),
isOriginalMissing = targetQuote.isOriginalMissing,
attachment = null,
mentions = null,
type = targetQuote.quoteType,
bodyRanges = null
)
} else {
null
@@ -455,7 +455,7 @@ object SyncMessageProcessor {
messageToEdit = targetMessage.id
)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
if (toRecipient.isGroup) {
updateGroupReceiptStatus(sent, messageId, toRecipient.requireGroupId())
@@ -558,7 +558,7 @@ object SyncMessageProcessor {
)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNDELIVERED, null)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNDELIVERED, null).messageId
if (groupId != null) {
updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId())
@@ -655,7 +655,7 @@ object SyncMessageProcessor {
threadId,
false,
null
)
).messageId
SignalDatabase.messages.markAsSent(messageId, true)
}
@@ -703,14 +703,14 @@ object SyncMessageProcessor {
if (sent.message?.expireTimerVersion == null) {
// TODO [expireVersion] After unsupported builds expire, we can remove this branch
SignalDatabase.recipients.setExpireMessagesWithoutIncrementingVersion(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt())
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null).messageId
SignalDatabase.messages.markAsSent(messageId, true)
} else if (sent.message!!.expireTimerVersion!! >= recipient.expireTimerVersion) {
SignalDatabase.recipients.setExpireMessages(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt(), sent.message!!.expireTimerVersion!!)
if (sent.message!!.expireTimerDuration != recipient.expiresInSeconds.seconds) {
log(sent.timestamp!!, "Not inserted update message as timer value did not change")
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null).messageId
SignalDatabase.messages.markAsSent(messageId, true)
}
} else {
@@ -762,7 +762,7 @@ object SyncMessageProcessor {
quoteBody = story.body
bodyBodyRanges = story.messageRanges
}
quoteModel = QuoteModel(sentTimestamp, storyAuthorRecipient, quoteBody, false, story.slideDeck.asAttachments(), emptyList(), QuoteModel.Type.NORMAL, bodyBodyRanges)
quoteModel = QuoteModel(sentTimestamp, storyAuthorRecipient, quoteBody, false, story.slideDeck.asAttachments().firstOrNull(), emptyList(), QuoteModel.Type.NORMAL, bodyBodyRanges)
expiresInMillis = dataMessage.expireTimerDuration.inWholeMilliseconds
} else {
warn(envelopeTimestamp, "Story has replies disabled. Dropping reply.")
@@ -787,7 +787,7 @@ object SyncMessageProcessor {
}
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
if (recipient.isGroup) {
updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId())
@@ -821,7 +821,7 @@ object SyncMessageProcessor {
val recipient: Recipient = getSyncMessageDestination(sent)
val dataMessage: DataMessage = sent.message!!
val quote: QuoteModel? = DataMessageProcessor.getValidatedQuote(context, envelopeTimestamp, dataMessage, senderRecipient, threadRecipient)
val quoteModel: QuoteModel? = DataMessageProcessor.getValidatedQuote(context, envelopeTimestamp, dataMessage, senderRecipient, threadRecipient)
val sticker: Attachment? = DataMessageProcessor.getStickerAttachment(envelopeTimestamp, dataMessage)
val sharedContacts: List<Contact> = DataMessageProcessor.getContacts(dataMessage)
val previews: List<LinkPreview> = DataMessageProcessor.getLinkPreviews(dataMessage.preview, dataMessage.body ?: "", false)
@@ -838,7 +838,7 @@ object SyncMessageProcessor {
timestamp = sent.timestamp!!,
expiresIn = dataMessage.expireTimerDuration.inWholeMilliseconds,
viewOnce = viewOnce,
quote = quote,
quote = quoteModel,
contacts = sharedContacts,
previews = previews,
mentions = mentions,
@@ -852,7 +852,7 @@ object SyncMessageProcessor {
}
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
log(envelopeTimestamp, "Inserted sync message as messageId $messageId")
if (recipient.isGroup) {
@@ -913,11 +913,11 @@ object SyncMessageProcessor {
bodyRanges = bodyRanges
)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId())
} else {
val outgoingTextMessage = OutgoingMessage.text(threadRecipient = recipient, body = body, expiresIn = expiresInMillis, sentTimeMillis = sent.timestamp!!, bodyRanges = bodyRanges)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingTextMessage, threadId, false, null)
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingTextMessage, threadId, false, null).messageId
SignalDatabase.messages.markUnidentified(messageId, sent.isUnidentified(recipient.serviceId.orNull()))
}

View File

@@ -12,7 +12,7 @@ class QuoteModel(
val author: RecipientId,
val text: String,
val isOriginalMissing: Boolean,
val attachments: List<Attachment>,
val attachment: Attachment?,
mentions: List<Mention>?,
val type: Type,
val bodyRanges: BodyRangeList?

View File

@@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.MessageTable.InsertResult;
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.RecipientTable;
@@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
@@ -76,6 +78,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -114,7 +117,7 @@ public class MessageSender {
for (OutgoingMessage message : messages) {
long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), -1L, message.getDistributionType());
long messageId = database.insertMessageOutbox(message.stripAttachments(), allocatedThreadId, false, insertListener);
long messageId = database.insertMessageOutbox(message.stripAttachments(), allocatedThreadId, false, insertListener).getMessageId();
messageIds.add(messageId);
threads.add(allocatedThreadId);
@@ -199,6 +202,7 @@ public class MessageSender {
recipient,
SendType.SIGNAL,
messageId,
null,
jobDependencyIds
);
}
@@ -222,9 +226,11 @@ public class MessageSender {
ThreadTable threadTable = SignalDatabase.threads();
MessageTable database = SignalDatabase.messages();
long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), threadId, message.getDistributionType());
Recipient recipient = message.getThreadRecipient();
long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, sendType != SendType.SIGNAL, insertListener);
long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), threadId, message.getDistributionType());
Recipient recipient = message.getThreadRecipient();
InsertResult insertResult = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, sendType != SendType.SIGNAL, insertListener);
long messageId = insertResult.getMessageId();
if (message.getThreadRecipient().isGroup()) {
if (message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) {
@@ -236,7 +242,7 @@ public class MessageSender {
SignalLocalMetrics.IndividualMessageSend.onInsertedIntoDatabase(messageId, metricId);
}
sendMessageInternal(context, recipient, sendType, messageId, Collections.emptyList());
sendMessageInternal(context, recipient, sendType, messageId, insertResult.getQuoteAttachmentId(), Collections.emptyList());
onMessageSent();
threadTable.update(allocatedThreadId, true, true);
@@ -271,10 +277,11 @@ public class MessageSender {
return false;
}
long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId),
allocatedThreadId,
false,
insertListener);
InsertResult insertResult = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId),
allocatedThreadId,
false,
insertListener);
long messageId = insertResult.getMessageId();
for (AttachmentId attachmentId: attachmentIds) {
boolean wasPreuploaded = SignalDatabase.attachments().getMessageId(attachmentId) == AttachmentTable.PREUPLOAD_MESSAGE_ID;
@@ -286,7 +293,7 @@ public class MessageSender {
attachmentDatabase.updateMessageId(attachmentIds, messageId, message.getStoryType().isStory());
sendMessageInternal(context, recipient, SendType.SIGNAL, messageId, jobIds);
sendMessageInternal(context, recipient, SendType.SIGNAL, messageId, insertResult.getQuoteAttachmentId(), jobIds);
onMessageSent();
threadTable.update(allocatedThreadId, true, true);
@@ -324,7 +331,7 @@ public class MessageSender {
long primaryMessageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, primaryMessage.getThreadRecipient(), primaryMessage, primaryThreadId),
primaryThreadId,
false,
null);
null).getMessageId();
attachmentDatabase.updateMessageId(preUploadAttachmentIds, primaryMessageId, primaryMessage.getStoryType().isStory());
if (primaryMessage.getStoryType() != StoryType.NONE) {
@@ -352,7 +359,7 @@ public class MessageSender {
long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, secondaryMessage.getThreadRecipient(), secondaryMessage, allocatedThreadId),
allocatedThreadId,
false,
null);
null).getMessageId();
List<AttachmentId> attachmentIds = new ArrayList<>(preUploadAttachmentIds.size());
for (int i = 0; i < preUploadAttachments.size(); i++) {
@@ -517,7 +524,15 @@ public class MessageSender {
sendType = SendType.SIGNAL;
}
sendMessageInternal(context, recipient, sendType, messageId, Collections.emptyList());
AttachmentId quoteAttachmentId = SignalDatabase.attachments()
.getAttachmentsForMessage(messageId)
.stream()
.filter(it -> it.quote)
.findFirst()
.map(it -> it.attachmentId)
.orElse(null);
sendMessageInternal(context, recipient, sendType, messageId, quoteAttachmentId, Collections.emptyList());
onMessageSent();
}
@@ -542,14 +557,23 @@ public class MessageSender {
Recipient recipient,
SendType sendType,
long messageId,
@Nullable AttachmentId quoteAttachmentId,
@NonNull Collection<String> uploadJobIds)
{
Set<String> finalUploadJobIds = new HashSet<>(uploadJobIds);
if (quoteAttachmentId != null) {
Job uploadJob = new AttachmentUploadJob(quoteAttachmentId);
AppDependencies.getJobManager().add(uploadJob);
finalUploadJobIds.add(uploadJob.getId());
}
if (recipient.isPushGroup()) {
sendGroupPush(context, recipient, messageId, Collections.emptySet(), uploadJobIds);
sendGroupPush(context, recipient, messageId, Collections.emptySet(), finalUploadJobIds);
} else if (recipient.isDistributionList()) {
sendDistributionList(context, recipient, messageId, Collections.emptySet(), uploadJobIds);
sendDistributionList(context, recipient, messageId, Collections.emptySet(), finalUploadJobIds);
} else if (sendType == SendType.SIGNAL && isPushMediaSend(context, recipient)) {
sendMediaPush(context, recipient, messageId, uploadJobIds);
sendMediaPush(context, recipient, messageId, finalUploadJobIds);
} else {
Log.w(TAG, "Unknown send type!");
}

View File

@@ -47,7 +47,7 @@ class StoryDirectReplyRepository(context: Context) {
expiresIn = TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong()),
parentStoryId = ParentStoryId.DirectReply(storyId),
isStoryReaction = isReaction,
outgoingQuote = QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL, message.messageRanges),
outgoingQuote = QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments().firstOrNull(), null, QuoteModel.Type.NORMAL, message.messageRanges),
bodyRanges = bodyRangeList,
isSecure = true
),

View File

@@ -351,6 +351,10 @@ public class MediaUtil {
return !TextUtils.isEmpty(contentType) && contentType.trim().equals(IMAGE_WEBP);
}
public static boolean isPngType(String contentType) {
return !TextUtils.isEmpty(contentType) && contentType.trim().equals(IMAGE_PNG);
}
public static boolean isFile(Attachment attachment) {
return !isGif(attachment) && !isImage(attachment) && !isAudio(attachment) && !isVideo(attachment);
}