From 804f479cb0615f351942e5c928603e9834cffa86 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Wed, 26 Nov 2025 11:31:21 -0500 Subject: [PATCH] Add various fixes for pinned messages. --- .../conversation/ConversationUpdateItem.java | 3 +- .../ConversationListItem.java | 2 + .../securesms/database/MessageTable.kt | 60 ++++++++++++++++++- .../messages/SyncMessageProcessor.kt | 59 ++++++++++++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 83d1275912..01e3930156 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -685,11 +685,10 @@ public final class ConversationUpdateItem extends FrameLayout passthroughClickListener.onClick(v); } }); - } else if (MessageRecordUtil.hasPinnedMessageUpdate(conversationMessage.getMessageRecord())) { + } else if (MessageRecordUtil.hasPinnedMessageUpdate(conversationMessage.getMessageRecord()) && conversationMessage.getMessageRecord().getMessageExtras().pinnedMessage.pinnedMessageId != -1) { actionButton.setText(R.string.PinnedMessage__go_to_message); actionButton.setVisibility(VISIBLE); actionButton.setOnClickListener(v -> { - // TODO(michelle): Handle when a message gets deleted if (batchSelected.isEmpty() && eventListener != null && MessageRecordUtil.hasPinnedMessageUpdate(conversationMessage.getMessageRecord())) { eventListener.onViewPinnedMessage(conversationMessage.getMessageRecord().getMessageExtras().pinnedMessage.pinnedMessageId); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 2a13c0c912..b67a168c0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -682,6 +682,8 @@ public final class ConversationListItem extends ConstraintLayout implements Bind } } else if (MessageTypes.isPollTerminate(thread.getType())) { return emphasisAdded(context, thread.getBody(), Glyph.POLL, defaultTint); + } else if (MessageTypes.isPinnedMessageUpdate(thread.getType())) { + return emphasisAdded(context, thread.getBody(), Glyph.PIN, defaultTint); } else { ThreadTable.Extra extra = thread.getExtra(); if (extra != null && extra.isViewOnce()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 0110af591d..63b0cccf03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -2270,6 +2270,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat disassociateStoryQuotes(messageId) polls.deletePoll(messageId) disassociatePollFromPollTerminate(polls.getPollTerminateMessageId(messageId)) + disassociatePinnedMessage(messageId) val threadId = getThreadIdForMessage(messageId) threads.update(threadId, false) @@ -2820,7 +2821,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat retrieved.type == MessageType.IDENTITY_VERIFIED || retrieved.type == MessageType.IDENTITY_UPDATE - val read = silent || retrieved.type == MessageType.EXPIRATION_UPDATE + val read = silent || retrieved.type == MessageType.EXPIRATION_UPDATE || MessageTypes.isPinnedMessageUpdate(type) val contentValues = contentValuesOf( DATE_SENT to retrieved.sentTimeMillis, @@ -3669,6 +3670,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat groupReceipts.deleteRowsForMessage(messageId) mentions.deleteMentionsForMessage(messageId) disassociatePollFromPollTerminate(polls.getPollTerminateMessageId(messageId)) + disassociatePinnedMessage(messageId) writableDatabase .delete(TABLE_NAME) @@ -3764,6 +3766,62 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } } + /** + * When a message gets deleted, clear the pinned record and remove any references + */ + fun disassociatePinnedMessage(messageId: Long) { + if (messageId == -1L) { + return + } + + writableDatabase.withinTransaction { db -> + // Clear pinned message info + val updated = db.update(TABLE_NAME) + .values( + PINNED_AT to 0, + PINNED_UNTIL to 0 + ) + .where("$ID = ? AND $PINNED_UNTIL > 0", messageId) + .run() > 0 + + if (!updated) { + return@withinTransaction + } + + // Find the pinned message chat update + val pinningMessageId = db + .select(PINNING_MESSAGE_ID) + .from(TABLE_NAME) + .where("$ID = ?", messageId) + .run() + .readToSingleInt(-1) + + if (pinningMessageId == -1) { + return@withinTransaction + } + + // Disassociate chat update from pinned message + val messageExtras = db + .select(MESSAGE_EXTRAS) + .from(TABLE_NAME) + .where("$ID = ?", pinningMessageId) + .run() + .readToSingleObject { cursor -> + val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS) + messageExtraBytes?.let { MessageExtras.ADAPTER.decode(it) } + } + + if (messageExtras?.pinnedMessage != null) { + val updatedMessageExtras = messageExtras.newBuilder().pinnedMessage(pinnedMessage = messageExtras.pinnedMessage.copy(pinnedMessageId = -1)).build() + db + .update(TABLE_NAME) + .values(MESSAGE_EXTRAS to updatedMessageExtras.encode()) + .where("$ID = ?", pinningMessageId) + .run() + } + } + } + fun getSerializedSharedContacts(insertedAttachmentIds: Map, contacts: List): String? { if (contacts.isEmpty()) { return null diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index 5f5de9877f..a839cf8101 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.model.StoryType import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras +import org.thoughtcrime.securesms.database.model.databaseprotos.PinnedMessage import org.thoughtcrime.securesms.database.model.databaseprotos.PollTerminate import org.thoughtcrime.securesms.database.model.toBodyRangeList import org.thoughtcrime.securesms.dependencies.AppDependencies @@ -251,6 +252,11 @@ object SyncMessageProcessor { threadId = SignalDatabase.threads.getOrCreateThreadIdFor(getSyncMessageDestination(sent)) } dataMessage.pollTerminate != null -> threadId = handleSynchronizedPollEnd(envelope, dataMessage, sent, senderRecipient, earlyMessageCacheEntry) + dataMessage.pinMessage != null -> threadId = handleSynchronizedPinMessage(envelope, dataMessage, sent, senderRecipient, earlyMessageCacheEntry) + dataMessage.unpinMessage != null -> { + DataMessageProcessor.handleUnpinMessage(envelope, dataMessage, senderRecipient, threadRecipient, earlyMessageCacheEntry) + threadId = SignalDatabase.threads.getOrCreateThreadIdFor(getSyncMessageDestination(sent)) + } else -> threadId = handleSynchronizeSentTextMessage(sent, envelope.timestamp!!) } @@ -1857,6 +1863,59 @@ object SyncMessageProcessor { return threadId } + private fun handleSynchronizedPinMessage( + envelope: Envelope, + message: DataMessage, + sent: Sent, + senderRecipient: Recipient, + earlyMessageCacheEntry: EarlyMessageCacheEntry? + ): Long { + if (!RemoteConfig.receivePinnedMessages) { + log(envelope.timestamp!!, "Sync pinned messages not allowed due to remote config.") + } + + log(envelope.timestamp!!, "Synchronize pinned message") + + val recipient = getSyncMessageDestination(sent) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + + val expiresInMillis = message.expireTimerDuration.inWholeMilliseconds + if (recipient.expiresInSeconds != message.expireTimerDuration.inWholeSeconds.toInt() || ((message.expireTimerVersion ?: -1) > recipient.expireTimerVersion)) { + handleSynchronizeSentExpirationUpdate(sent, sideEffect = true) + } + + val pinMessage = message.pinMessage!! + val targetMessage = SignalDatabase.messages.getMessageFor(pinMessage.targetSentTimestamp!!, Recipient.self().id) + if (targetMessage == null) { + warn(envelope.timestamp!!, "Unable to find target message for sync message. Putting in early message cache.") + if (earlyMessageCacheEntry != null) { + AppDependencies.earlyMessageCache.store(senderRecipient.id, pinMessage.targetSentTimestamp!!, earlyMessageCacheEntry) + PushProcessEarlyMessagesJob.enqueue() + } + return -1 + } + + val duration = if (pinMessage.pinDurationForever == true) MessageTable.PIN_FOREVER else pinMessage.pinDurationSeconds!!.toLong() + val outgoingMessage = OutgoingMessage.pinMessage( + threadRecipient = recipient, + sentTimeMillis = sent.timestamp!!, + expiresIn = recipient.expiresInSeconds.seconds.inWholeMilliseconds, + messageExtras = MessageExtras(pinnedMessage = PinnedMessage(pinnedMessageId = targetMessage.id, targetAuthorAci = pinMessage.targetAuthorAciBinary!!, targetTimestamp = pinMessage.targetSentTimestamp!!, pinDurationInSeconds = duration)) + ) + + val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId + SignalDatabase.messages.markAsSent(messageId, true) + + log(envelope.timestamp!!, "Inserted sync pin message as messageId $messageId") + + if (expiresInMillis > 0) { + SignalDatabase.messages.markExpireStarted(messageId, sent.expirationStartTimestamp ?: 0) + AppDependencies.expiringMessageManager.scheduleDeletion(messageId, recipient.isGroup, sent.expirationStartTimestamp ?: 0, expiresInMillis) + } + + return threadId + } + private fun ConversationIdentifier.toRecipientId(): RecipientId? { val threadServiceId = ServiceId.parseOrNull(this.threadServiceId, this.threadServiceIdBinary) return when {