Add support for starring messages.

This commit is contained in:
Greyson Parrelli
2026-03-20 21:24:10 -04:00
committed by Cody Henthorne
parent 6496f236ea
commit 48374e6950
48 changed files with 1149 additions and 49 deletions
@@ -48,6 +48,7 @@ public class DatabaseObserver {
private static final String KEY_CALL_LINK_UPDATES = "CallLinkUpdates";
private static final String KEY_IN_APP_PAYMENTS = "InAppPayments";
private static final String KEY_CHAT_FOLDER = "ChatFolder";
private static final String KEY_STARRED_MESSAGES = "StarredMessages";
private final Executor executor;
@@ -71,6 +72,7 @@ public class DatabaseObserver {
private final Map<CallLinkRoomId, Set<Observer>> callLinkObservers;
private final Set<InAppPaymentObserver> inAppPaymentObservers;
private final Set<Observer> chatFolderObservers;
private final Set<Observer> starredMessageObservers;
public DatabaseObserver() {
this.executor = new SerialExecutor(SignalExecutors.BOUNDED);
@@ -94,6 +96,7 @@ public class DatabaseObserver {
this.callLinkObservers = new HashMap<>();
this.inAppPaymentObservers = new HashSet<>();
this.chatFolderObservers = new HashSet<>();
this.starredMessageObservers = new HashSet<>();
}
public void registerConversationListObserver(@NonNull Observer listener) {
@@ -213,6 +216,10 @@ public class DatabaseObserver {
executor.execute(() -> chatFolderObservers.add(observer));
}
public void registerStarredMessageObserver(@NonNull Observer observer) {
executor.execute(() -> starredMessageObservers.add(observer));
}
public void unregisterObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.remove(listener);
@@ -231,6 +238,7 @@ public class DatabaseObserver {
callUpdateObservers.remove(listener);
unregisterMapped(callLinkObservers, listener);
chatFolderObservers.remove(listener);
starredMessageObservers.remove(listener);
});
}
@@ -399,6 +407,10 @@ public class DatabaseObserver {
runPostSuccessfulTransaction(KEY_CHAT_FOLDER, () -> notifySet(chatFolderObservers));
}
public void notifyStarredMessageObservers() {
runPostSuccessfulTransaction(KEY_STARRED_MESSAGES, () -> notifySet(starredMessageObservers));
}
private void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable runnable) {
SignalDatabase.runPostSuccessfulTransaction(dedupeKey, () -> {
executor.execute(runnable);
@@ -227,6 +227,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
const val PINNED_AT = "pinned_at"
const val DELETED_BY = "deleted_by"
const val STORY_ARCHIVED = "story_archived"
const val STARRED = "starred"
const val QUOTE_NOT_PRESENT_ID = 0L
const val QUOTE_TARGET_MISSING_ID = -1L
@@ -299,7 +300,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
$PINNING_MESSAGE_ID INTEGER DEFAULT 0,
$PINNED_AT INTEGER DEFAULT 0,
$DELETED_BY INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$STORY_ARCHIVED INTEGER DEFAULT 0
$STORY_ARCHIVED INTEGER DEFAULT 0,
$STARRED INTEGER DEFAULT 0
)
"""
@@ -334,7 +336,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
"CREATE INDEX IF NOT EXISTS message_pinned_until_index ON $TABLE_NAME ($PINNED_UNTIL)",
"CREATE INDEX IF NOT EXISTS message_pinned_at_index ON $TABLE_NAME ($PINNED_AT)",
"CREATE INDEX IF NOT EXISTS message_deleted_by_index ON $TABLE_NAME ($DELETED_BY)",
"CREATE INDEX IF NOT EXISTS message_story_archived_index ON $TABLE_NAME ($STORY_ARCHIVED, $STORY_TYPE, $DATE_SENT) WHERE $STORY_TYPE > 0 AND $STORY_ARCHIVED > 0"
"CREATE INDEX IF NOT EXISTS message_story_archived_index ON $TABLE_NAME ($STORY_ARCHIVED, $STORY_TYPE, $DATE_SENT) WHERE $STORY_TYPE > 0 AND $STORY_ARCHIVED > 0",
"CREATE INDEX IF NOT EXISTS message_starred_index ON $TABLE_NAME ($STARRED) WHERE $STARRED > 0"
)
private val MMS_PROJECTION_BASE = arrayOf(
@@ -390,7 +393,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
VOTES_UNREAD,
VOTES_LAST_SEEN,
PINNED_UNTIL,
DELETED_BY
DELETED_BY,
STARRED
)
private val MMS_PROJECTION: Array<String> = MMS_PROJECTION_BASE
@@ -2150,6 +2154,47 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
}
fun setStarred(messageId: Long, starred: Boolean) {
setStarred(setOf(messageId), starred)
}
fun setStarred(messageIds: Set<Long>, starred: Boolean) {
writableDatabase.withinTransaction { db ->
for (messageId in messageIds) {
db.update(TABLE_NAME)
.values(STARRED to if (starred) 1 else 0)
.where("$ID = ?", messageId)
.run()
}
}
val threadIds = messageIds.map { getThreadIdForMessage(it) }.toSet()
for (threadId in threadIds) {
notifyConversationListeners(threadId)
}
for (messageId in messageIds) {
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
}
AppDependencies.databaseObserver.notifyStarredMessageObservers()
}
fun getStarredMessages(threadId: Long? = null): List<MessageRecord> {
val where: String
val args: Array<String>?
if (threadId != null) {
where = "$STARRED > 0 AND $THREAD_ID = ? AND $LATEST_REVISION_ID IS NULL"
args = buildArgs(threadId)
} else {
where = "$STARRED > 0 AND $LATEST_REVISION_ID IS NULL"
args = null
}
return mmsReaderFor(queryMessages(where, args, reverse = true)).use { reader ->
reader.mapNotNull { it }
}.withAttachments()
}
fun getRecentPendingMessages(): MmsReader {
val now = System.currentTimeMillis()
val oneDayAgo = now.milliseconds - 1.days
@@ -2301,7 +2346,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
LINK_PREVIEWS to null,
SHARED_CONTACTS to null,
ORIGINAL_MESSAGE_ID to null,
LATEST_REVISION_ID to null
LATEST_REVISION_ID to null,
STARRED to 0
)
.where("$ID = ?", messageId)
.run()
@@ -2925,6 +2971,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.readToSingleInt(0)
contentValues.put(NOTIFIED, notified.toInt())
contentValues.put(STARRED, if (editedMessage.isStarred) 1 else 0)
} else if (MessageTypes.isPinnedMessageUpdate(type)) {
contentValues.put(NOTIFIED, 1)
}
@@ -3343,6 +3390,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
contentValues.put(ORIGINAL_MESSAGE_ID, editedMessage.getOriginalOrOwnMessageId().id)
contentValues.put(REVISION_NUMBER, editedMessage.revisionNumber + 1)
contentValues.put(EXPIRE_STARTED, editedMessage.expireStarted)
contentValues.put(STARRED, if (editedMessage.isStarred) 1 else 0)
} else {
contentValues.putNull(ORIGINAL_MESSAGE_ID)
}
@@ -6402,6 +6450,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val isRead = cursor.requireBoolean(READ)
val pinnedUntil = cursor.requireLong(PINNED_UNTIL)
val deletedBy = cursor.requireLongOrNull(DELETED_BY)?.let { RecipientId.from(it) }
val isStarred = cursor.requireBoolean(STARRED)
val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS)
val messageExtras = messageExtraBytes?.let { MessageExtras.ADAPTER.decode(it) }
@@ -6497,7 +6546,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
isRead,
pinnedUntil,
deletedBy,
messageExtras
messageExtras,
isStarred
)
}
@@ -16,6 +16,7 @@ object RxDatabaseObserver {
val conversationList: Flowable<Unit> by lazy { conversationListFlowable() }
val notificationProfiles: Flowable<Unit> by lazy { notificationProfilesFlowable() }
val chatFolders: Flowable<Unit> by lazy { chatFoldersFlowable() }
val starredMessages: Flowable<Unit> by lazy { starredMessagesFlowable() }
private fun conversationListFlowable(): Flowable<Unit> {
return databaseFlowable { listener ->
@@ -43,6 +44,12 @@ object RxDatabaseObserver {
}
}
private fun starredMessagesFlowable(): Flowable<Unit> {
return databaseFlowable { listener ->
AppDependencies.databaseObserver.registerStarredMessageObserver(listener)
}
}
private fun databaseFlowable(registerObserver: (RxObserver) -> Unit): Flowable<Unit> {
val flowable = Flowable.create(
{
@@ -162,6 +162,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V305_AddStoryArchiv
import org.thoughtcrime.securesms.database.helpers.migration.V306_AddRemoteDeletedColumn
import org.thoughtcrime.securesms.database.helpers.migration.V308_AddBackRemoteDeletedColumn
import org.thoughtcrime.securesms.database.helpers.migration.V309_GroupTerminatedColumnMigration
import org.thoughtcrime.securesms.database.helpers.migration.V310_AddStarredColumn
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -331,10 +332,11 @@ object SignalDatabaseMigrations {
306 to V306_AddRemoteDeletedColumn,
// 307 to V307_RemoveRemoteDeletedColumn - Removed due to unsolvable OOM crashes. [TODO]: Attempt to fix in the future
308 to V308_AddBackRemoteDeletedColumn,
309 to V309_GroupTerminatedColumnMigration
309 to V309_GroupTerminatedColumnMigration,
310 to V310_AddStarredColumn
)
const val DATABASE_VERSION = 309
const val DATABASE_VERSION = 310
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* Adds a column for tracking the starred status of a message.
*/
@Suppress("ClassName")
object V310_AddStarredColumn : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE message ADD COLUMN starred INTEGER DEFAULT 0")
db.execSQL("CREATE INDEX IF NOT EXISTS message_starred_index ON message (starred) WHERE starred > 0")
}
}
@@ -60,7 +60,8 @@ public class InMemoryMessageRecord extends MessageRecord {
0,
0,
null,
null);
null,
false);
}
@Override
@@ -116,6 +116,7 @@ public abstract class MessageRecord extends DisplayRecord {
private final long pinnedUntil;
private final RecipientId deletedBy;
private final MessageExtras messageExtras;
private final boolean starred;
protected Boolean isJumboji = null;
@@ -138,7 +139,8 @@ public abstract class MessageRecord extends DisplayRecord {
int revisionNumber,
long pinnedUntil,
@Nullable RecipientId deletedBy,
@Nullable MessageExtras messageExtras)
@Nullable MessageExtras messageExtras,
boolean starred)
{
super(body, fromRecipient, toRecipient, dateSent, dateReceived,
threadId, deliveryStatus, hasDeliveryReceipt, type,
@@ -161,6 +163,7 @@ public abstract class MessageRecord extends DisplayRecord {
this.pinnedUntil = pinnedUntil;
this.deletedBy = deletedBy;
this.messageExtras = messageExtras;
this.starred = starred;
}
public abstract boolean isMms();
@@ -799,6 +802,10 @@ public abstract class MessageRecord extends DisplayRecord {
return deletedBy;
}
public boolean isStarred() {
return starred;
}
public boolean isPendingAdminDelete() {
return messageExtras != null &&
messageExtras.adminDeleteStatus != null &&
@@ -121,12 +121,13 @@ public class MmsMessageRecord extends MessageRecord {
boolean isRead,
long pinnedUntil,
@Nullable RecipientId deletedBy,
@Nullable MessageExtras messageExtras)
@Nullable MessageExtras messageExtras,
boolean starred)
{
super(id, body, fromRecipient, fromDeviceId, toRecipient,
dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, hasDeliveryReceipt,
mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, expireTimerVersion, hasReadReceipt,
unidentified, reactions, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, pinnedUntil, deletedBy, messageExtras);
unidentified, reactions, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, pinnedUntil, deletedBy, messageExtras, starred);
this.slideDeck = slideDeck;
this.quote = quote;
@@ -334,12 +335,21 @@ public class MmsMessageRecord extends MessageRecord {
(parentStoryId == null || parentStoryId.isDirectReply());
}
public @NonNull MmsMessageRecord withIncomingType() {
long incomingType = (getType() & ~MessageTypes.BASE_TYPE_MASK) | MessageTypes.BASE_INBOX_TYPE;
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
incomingType, getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
}
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
}
public @NonNull MmsMessageRecord withoutQuote() {
@@ -347,7 +357,7 @@ public class MmsMessageRecord extends MessageRecord {
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
}
public @NonNull MmsMessageRecord withAttachments(@NonNull List<DatabaseAttachment> attachments) {
@@ -369,7 +379,7 @@ public class MmsMessageRecord extends MessageRecord {
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
}
public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) {
@@ -377,7 +387,7 @@ public class MmsMessageRecord extends MessageRecord {
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
}
@@ -386,7 +396,7 @@ public class MmsMessageRecord extends MessageRecord {
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getPoll(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
}
public @NonNull MmsMessageRecord withPoll(@Nullable PollRecord poll) {
@@ -394,7 +404,7 @@ public class MmsMessageRecord extends MessageRecord {
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), mentionsSelf,
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), poll, getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras());
getOriginalMessageId(), getRevisionNumber(), isRead(), getPinnedUntil(), getDeletedBy(), getMessageExtras(), isStarred());
}
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {