From 2d04bb32fd96402bdf723432fe0c92bbca11ada7 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 22 Jul 2026 12:04:09 -0400 Subject: [PATCH] Add notifications for scheduled notes to self. --- .../securesms/database/MessageTable.kt | 21 ++++++++++++------- .../notifications/v2/NotificationBuilder.kt | 5 +++-- .../v2/NotificationConversation.kt | 8 ++++++- .../notifications/v2/NotificationFactory.kt | 3 ++- .../notifications/v2/NotificationItem.kt | 19 +++++++++++++---- .../v2/NotificationStateProvider.kt | 5 +++-- .../service/ScheduledMessageManager.kt | 8 ++++++- .../securesms/util/AvatarUtil.java | 13 ++++++++---- 8 files changed, 60 insertions(+), 22 deletions(-) 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 251d443d5f..6b0dd2aad7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -2494,15 +2494,22 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId)) } - fun clearScheduledStatus(threadId: Long, messageId: Long, expiresIn: Long): Boolean { + fun clearScheduledStatus(threadId: Long, messageId: Long, expiresIn: Long, markUnread: Boolean = false): Boolean { + val values = contentValuesOf( + SCHEDULED_DATE to -1, + DATE_SENT to System.currentTimeMillis(), + DATE_RECEIVED to System.currentTimeMillis(), + EXPIRES_IN to expiresIn + ) + + if (markUnread) { + values.put(READ, 0) + values.put(NOTIFIED, 0) + } + val rowsUpdated = writableDatabase .update(TABLE_NAME) - .values( - SCHEDULED_DATE to -1, - DATE_SENT to System.currentTimeMillis(), - DATE_RECEIVED to System.currentTimeMillis(), - EXPIRES_IN to expiresIn - ) + .values(values) .where("$ID = ? AND $SCHEDULED_DATE != ?", messageId, -1) .run() diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt index 2cdc08cac9..92551b91f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt @@ -301,15 +301,16 @@ sealed class NotificationBuilder(protected val context: Context) { conversation.notificationItems.forEach { notificationItem -> var person: PersonCompat? = null + val isNoteToSelf = notificationItem.isPersonSelf && conversation.recipient.isSelf - if (!notificationItem.isPersonSelf) { + if (!notificationItem.isPersonSelf || isNoteToSelf) { val personBuilder: PersonCompat.Builder = PersonCompat.Builder() .setBot(false) .setName(notificationItem.getPersonName(context)) .setUri(notificationItem.getPersonUri(context)) .setIcon(notificationItem.getPersonIcon(context)) - if (includeShortcut) { + if (includeShortcut && !isNoteToSelf) { personBuilder.setKey(ConversationUtil.getShortcutId(notificationItem.authorRecipient)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt index 0c7ea10f94..835e12cb78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt @@ -55,7 +55,11 @@ data class NotificationConversation( fun getContactLargeIcon(context: Context): Drawable? { return if (SignalStore.settings.messageNotificationsPrivacy.isDisplayContact) { - recipient.getContactDrawable(context) + if (recipient.isSelf) { + FallbackAvatarDrawable(context, FallbackAvatar.Resource.NoteToSelf(recipient.avatarColor)).circleCrop() + } else { + recipient.getContactDrawable(context) + } } else { FallbackAvatarDrawable(context, FallbackAvatar.forTextOrDefault("Unknown", AvatarColor.UNKNOWN)).circleCrop() } @@ -207,6 +211,8 @@ data class NotificationConversation( private fun getDisplayName(context: Context): String { return if (thread.groupStoryId != null) { context.getString(R.string.SingleRecipientNotificationBuilder__s_dot_story, recipient.getDisplayName(context)) + } else if (recipient.isSelf) { + context.getString(R.string.note_to_self) } else { recipient.getDisplayName(context) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt index 74efb9190e..fed94ae4e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt @@ -213,8 +213,9 @@ object NotificationFactory { else -> 0.seconds } val canAlertBasedOnTime: Boolean = lastNotificationTimestamp < System.currentTimeMillis() - throttle.inWholeMilliseconds || lastNotificationTimestamp > System.currentTimeMillis() + val isUnreadNoteToSelf: Boolean = conversation.recipient.isSelf && (conversation.mostRecentNotification as? MessageNotification)?.isUnread == true - return ((conversation.hasNewNotifications() && canAlertBasedOnTime) || alertOverride) && !conversation.mostRecentNotification.authorRecipient.isSelf + return ((conversation.hasNewNotifications() && canAlertBasedOnTime) || alertOverride) && (!conversation.mostRecentNotification.authorRecipient.isSelf || isUnreadNoteToSelf) } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt index 4ac173ecf7..1f4292dd58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt @@ -58,6 +58,9 @@ sealed class NotificationItem(val threadRecipient: Recipient, protected val reco val isPersonSelf: Boolean get() = authorRecipient.isSelf + protected val isNoteToSelf: Boolean + get() = authorRecipient.isSelf && threadRecipient.isSelf + protected val notifiedTimestamp: Long = record.notifiedTimestamp abstract val timestamp: Long @@ -99,7 +102,7 @@ sealed class NotificationItem(val threadRecipient: Recipient, protected val reco context.getString(R.string.SingleRecipientNotificationBuilder_new_message) } else { SpannableStringBuilder().apply { - append(Util.getBoldedString(authorRecipient.getShortDisplayName(context))) + append(Util.getBoldedString(if (isNoteToSelf) context.getString(R.string.note_to_self) else authorRecipient.getShortDisplayName(context))) if (threadRecipient != authorRecipient) { append(Util.getBoldedString("@${threadRecipient.getDisplayName(context)}")) } @@ -111,7 +114,11 @@ sealed class NotificationItem(val threadRecipient: Recipient, protected val reco fun getPersonName(context: Context): CharSequence { return if (SignalStore.settings.messageNotificationsPrivacy.isDisplayContact) { - authorRecipient.getDisplayName(context) + if (isNoteToSelf) { + context.getString(R.string.note_to_self) + } else { + authorRecipient.getDisplayName(context) + } } else { context.getString(R.string.SingleRecipientNotificationBuilder_signal) } @@ -131,7 +138,11 @@ sealed class NotificationItem(val threadRecipient: Recipient, protected val reco fun getPersonIcon(context: Context): IconCompat? { return if (SignalStore.settings.messageNotificationsPrivacy.isDisplayContact) { - AvatarUtil.getIconCompat(context, authorRecipient) + if (isNoteToSelf) { + AvatarUtil.getIconCompatForNoteToSelf(context, authorRecipient) + } else { + AvatarUtil.getIconCompat(context, authorRecipient) + } } else { null } @@ -216,7 +227,7 @@ sealed class NotificationItem(val threadRecipient: Recipient, protected val reco /** * Represents a notification associated with a new message. */ -class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : NotificationItem(threadRecipient, record) { +class MessageNotification(threadRecipient: Recipient, record: MessageRecord, val isUnread: Boolean = false) : NotificationItem(threadRecipient, record) { override val timestamp: Long = record.timestamp override val authorRecipient: Recipient = record.fromRecipient.resolve() override val isNewNotification: Boolean = notifiedTimestamp == 0L && !record.isEditMessage diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt index 24c50d2a97..2a7c320bf6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt @@ -101,7 +101,7 @@ object NotificationStateProvider { for (notification: NotificationMessage in threadMessages) { when (notification.includeMessage(notificationProfile)) { - MessageInclusion.INCLUDE -> notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord)) + MessageInclusion.INCLUDE -> notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord, notification.isUnreadMessage)) MessageInclusion.EXCLUDE -> Unit MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) @@ -161,6 +161,7 @@ object NotificationStateProvider { ) { private val isGroupStoryReply: Boolean = thread.groupStoryId != null private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing && !isGroupStoryReply + private val isUnreadNoteToSelf: Boolean = isUnreadMessage && messageRecord.isOutgoing && threadRecipient.isSelf && !isGroupStoryReply private val isIncomingMissedCall: Boolean = !messageRecord.isOutgoing && (messageRecord.isMissedAudioCall || messageRecord.isMissedVideoCall) private val isNotifiableGroupStoryMessage: Boolean = @@ -170,7 +171,7 @@ object NotificationStateProvider { (isParentStorySentBySelf || messageRecord.hasGroupQuoteOrSelfMention() || (hasSelfRepliedToStory && !messageRecord.isStoryReaction())) fun includeMessage(notificationProfile: NotificationProfile?): MessageInclusion { - return if (isUnreadIncoming || stickyThread || isNotifiableGroupStoryMessage || isIncomingMissedCall) { + return if (isUnreadIncoming || isUnreadNoteToSelf || stickyThread || isNotifiableGroupStoryMessage || isIncomingMissedCall) { if (threadRecipient.isMuted && !breaksThroughMute()) { MessageInclusion.MUTE_FILTERED } else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.hasGroupQuoteOrSelfMention())) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ScheduledMessageManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/ScheduledMessageManager.kt index 82d99a61ab..9fc1c9c56b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ScheduledMessageManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ScheduledMessageManager.kt @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.IndividualSendJob import org.thoughtcrime.securesms.jobs.PushGroupSendJob +import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.recipients.RecipientId import kotlin.time.Duration.Companion.seconds @@ -55,12 +56,17 @@ class ScheduledMessageManager( val scheduledMessagesToSend = messagesTable.getScheduledMessagesBefore(System.currentTimeMillis()) for (record in scheduledMessagesToSend) { val expiresIn = SignalDatabase.recipients.getExpiresInSeconds(record.toRecipient.id) - if (messagesTable.clearScheduledStatus(record.threadId, record.id, expiresIn.seconds.inWholeMilliseconds)) { + val isNoteToSelf = record.toRecipient.isSelf + if (messagesTable.clearScheduledStatus(record.threadId, record.id, expiresIn.seconds.inWholeMilliseconds, markUnread = isNoteToSelf)) { if (record.toRecipient.isPushGroup) { PushGroupSendJob.enqueue(application, AppDependencies.jobManager, record.id, record.toRecipient.id, emptySet(), true) } else { IndividualSendJob.enqueue(application, AppDependencies.jobManager, record.id, record.toRecipient, true) } + + if (isNoteToSelf) { + AppDependencies.messageNotifier.updateNotification(application, ConversationId.forConversation(record.threadId)) + } } else { Log.i(TAG, "messageId=${record.id} was not a scheduled message, ignoring") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java index e897766921..774a80194c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java @@ -128,12 +128,17 @@ public final class AvatarUtil { */ @WorkerThread public static @NonNull IconCompat getIconCompatForShortcut(@NonNull Context context, @NonNull Recipient recipient) { - int size = AdaptiveBitmapMetrics.getInnerWidth(); if (recipient.isSelf()) { - Drawable noteToSelfDrawable = getNoteToSelfDrawable(context, recipient.getAvatarColor(), size); - return IconCompat.createWithBitmap(DrawableUtil.toBitmap(noteToSelfDrawable, size, size)); + return getIconCompatForNoteToSelf(context, recipient); } - return IconCompat.createWithBitmap(getBitmapForNotification(context, recipient, size)); + return IconCompat.createWithBitmap(getBitmapForNotification(context, recipient, AdaptiveBitmapMetrics.getInnerWidth())); + } + + @WorkerThread + public static @NonNull IconCompat getIconCompatForNoteToSelf(@NonNull Context context, @NonNull Recipient recipient) { + int size = AdaptiveBitmapMetrics.getInnerWidth(); + Drawable noteToSelfDrawable = getNoteToSelfDrawable(context, recipient.getAvatarColor(), size); + return IconCompat.createWithBitmap(DrawableUtil.toBitmap(noteToSelfDrawable, size, size)); } @WorkerThread