Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
67591fe
Working system
harshad1 Dec 27, 2025
8dfa32e
Changing multi-message delete option, adding files
harshad1 Dec 27, 2025
cf756cb
Wording
harshad1 Dec 27, 2025
93f7534
Format fixes
harshad1 Dec 27, 2025
df61390
No drag divider
harshad1 Dec 29, 2025
d639bcf
Drag behavior updates
harshad1 Dec 29, 2025
cfcc6fc
Added star, moved options to common source of truth
harshad1 Dec 30, 2025
8f30fa8
Accessability
harshad1 Dec 30, 2025
4ca60d3
Merge branch 'main' into configure_notification_actions
harshad1 Jan 16, 2026
e92f230
Merge remote-tracking branch 'upstream/main' into configure_notificat…
harshad1 Jan 25, 2026
79737ee
Added files
harshad1 Jan 25, 2026
e8ccdb6
Fixes for tests
harshad1 Jan 25, 2026
8485cef
Using standard spacings and dispose strategy
harshad1 Jan 25, 2026
5e83c51
Removed old changes
harshad1 Jan 25, 2026
c9971e8
Updated for single source of order truth
harshad1 Feb 7, 2026
dd3de9d
Merge branch 'main' into configure_notification_actions
harshad1 Feb 7, 2026
f1c6358
Unified to list handling directly
harshad1 Feb 7, 2026
b7ee310
initial move to full compose
harshad1 Feb 8, 2026
0c259e6
Revert to recycler
harshad1 Feb 8, 2026
847c769
Initial working compose dragger
harshad1 Feb 15, 2026
8bb05e1
Split the control
harshad1 Feb 15, 2026
cb6fedd
Pure compose drag ui
harshad1 Feb 15, 2026
d3867a1
Tweaked the controller
harshad1 Feb 16, 2026
70cae11
Merge remote-tracking branch 'upstream/main' into configure_notificat…
harshad1 Feb 16, 2026
062d742
Cleaner swipe
harshad1 Feb 16, 2026
8d8bd3a
cleanups
harshad1 Feb 17, 2026
3ac43eb
Fixing and reorganizing to address lint issues
harshad1 Feb 17, 2026
8f27f4d
Merge branch 'main' into configure_notification_actions
harshad1 Feb 17, 2026
b69b273
multiple message delete option re-added
harshad1 Feb 17, 2026
95a2ae5
Fix test check
harshad1 Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.thunderbird.core.common.notification

/**
* Token strings used to persist notification action ordering.
*/
object NotificationActionTokens {
const val REPLY = "reply"
const val MARK_AS_READ = "mark_as_read"
const val DELETE = "delete"
const val STAR = "star"
const val ARCHIVE = "archive"
const val SPAM = "spam"

val DEFAULT_ORDER: List<String> = listOf(REPLY, MARK_AS_READ, DELETE, STAR, ARCHIVE, SPAM)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single source of truth for default order


fun parseOrder(raw: String): List<String> {
return raw
.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() }
}

fun serializeOrder(tokens: List<String>): String = tokens.joinToString(separator = ",")
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package net.thunderbird.core.preference.notification

import net.thunderbird.core.common.notification.NotificationActionTokens
import net.thunderbird.core.preference.NotificationQuickDelete

const val NOTIFICATION_PREFERENCE_DEFAULT_IS_QUIET_TIME_ENABLED = false
const val NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_STARTS = "21:00"
const val NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END = "7:00"
const val NOTIFICATION_PREFERENCE_DEFAULT_IS_NOTIFICATION_DURING_QUIET_TIME_ENABLED = true
val NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_ORDER = NotificationActionTokens.DEFAULT_ORDER
const val NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_CUTOFF = 3
const val NOTIFICATION_PREFERENCE_MAX_MESSAGE_ACTIONS_SHOWN = 3
const val NOTIFICATION_PREFERENCE_DEFAULT_IS_SUMMARY_DELETE_ACTION_ENABLED = true
val NOTIFICATION_PREFERENCE_DEFAULT_QUICK_DELETE_BEHAVIOUR = NotificationQuickDelete.ALWAYS

data class NotificationPreference(
Expand All @@ -14,6 +19,9 @@ data class NotificationPreference(
val quietTimeEnds: String = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END,
val isNotificationDuringQuietTimeEnabled: Boolean =
NOTIFICATION_PREFERENCE_DEFAULT_IS_NOTIFICATION_DURING_QUIET_TIME_ENABLED,
val messageActionsOrder: List<String> = NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_ORDER,
val messageActionsCutoff: Int = NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_CUTOFF,
val isSummaryDeleteActionEnabled: Boolean = NOTIFICATION_PREFERENCE_DEFAULT_IS_SUMMARY_DELETE_ACTION_ENABLED,
val notificationQuickDeleteBehaviour: NotificationQuickDelete =
NOTIFICATION_PREFERENCE_DEFAULT_QUICK_DELETE_BEHAVIOUR,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const val KEY_QUIET_TIME_ENDS = "quietTimeEnds"
const val KEY_QUIET_TIME_STARTS = "quietTimeStarts"
const val KEY_QUIET_TIME_ENABLED = "quietTimeEnabled"
const val KEY_NOTIFICATION_DURING_QUIET_TIME_ENABLED = "notificationDuringQuietTimeEnabled"
const val KEY_MESSAGE_ACTIONS_ORDER = "messageActionsOrder"
const val KEY_MESSAGE_ACTIONS_CUTOFF = "messageActionsCutoff"
const val KEY_IS_SUMMARY_DELETE_ACTION_ENABLED = "isSummaryDeleteActionEnabled"

const val KEY_NOTIFICATION_QUICK_DELETE_BEHAVIOUR = "notificationQuickDelete"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.common.notification.NotificationActionTokens
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
Expand Down Expand Up @@ -47,6 +48,22 @@ class DefaultNotificationPreferenceManager(
key = KEY_NOTIFICATION_DURING_QUIET_TIME_ENABLED,
defValue = NOTIFICATION_PREFERENCE_DEFAULT_IS_NOTIFICATION_DURING_QUIET_TIME_ENABLED,
),
messageActionsOrder = NotificationActionTokens.parseOrder(
storage.getStringOrDefault(
key = KEY_MESSAGE_ACTIONS_ORDER,
defValue = NotificationActionTokens.serializeOrder(
NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_ORDER,
),
),
),
messageActionsCutoff = storage.getInt(
key = KEY_MESSAGE_ACTIONS_CUTOFF,
defValue = NOTIFICATION_PREFERENCE_DEFAULT_MESSAGE_ACTIONS_CUTOFF,
),
isSummaryDeleteActionEnabled = storage.getBoolean(
key = KEY_IS_SUMMARY_DELETE_ACTION_ENABLED,
defValue = NOTIFICATION_PREFERENCE_DEFAULT_IS_SUMMARY_DELETE_ACTION_ENABLED,
),
notificationQuickDeleteBehaviour = storage.getEnumOrDefault(
key = KEY_NOTIFICATION_QUICK_DELETE_BEHAVIOUR,
default = NOTIFICATION_PREFERENCE_DEFAULT_QUICK_DELETE_BEHAVIOUR,
Expand All @@ -71,6 +88,12 @@ class DefaultNotificationPreferenceManager(
KEY_NOTIFICATION_DURING_QUIET_TIME_ENABLED,
config.isNotificationDuringQuietTimeEnabled,
)
storageEditor.putString(
KEY_MESSAGE_ACTIONS_ORDER,
NotificationActionTokens.serializeOrder(config.messageActionsOrder),
)
storageEditor.putInt(KEY_MESSAGE_ACTIONS_CUTOFF, config.messageActionsCutoff)
storageEditor.putBoolean(KEY_IS_SUMMARY_DELETE_ACTION_ENABLED, config.isSummaryDeleteActionEnabled)
storageEditor.putEnum(
KEY_NOTIFICATION_QUICK_DELETE_BEHAVIOUR,
config.notificationQuickDeleteBehaviour,
Expand Down
5 changes: 5 additions & 0 deletions legacy/common/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@
android:label="@string/general_settings_title"
/>

<activity
android:name="com.fsck.k9.ui.settings.notificationactions.NotificationActionsSettingsActivity"
android:label="@string/notification_actions_settings_title"
/>

<activity
android:name="com.fsck.k9.ui.settings.account.AccountSettingsActivity"
android:label="@string/account_settings_title_fmt"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ internal class K9NotificationActionCreator(
}

override fun createDismissAllMessagesPendingIntent(account: LegacyAccountDto): PendingIntent {
val intent = NotificationActionService.createDismissAllMessagesIntent(context, account).apply {
val intent = NotificationActionIntents.createDismissAllMessagesIntent(context, account).apply {
data = Uri.parse("data:,dismissAll/${account.uuid}/${System.currentTimeMillis()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
}

override fun createDismissMessagePendingIntent(messageReference: MessageReference): PendingIntent {
val intent = NotificationActionService.createDismissMessageIntent(context, messageReference).apply {
val intent = NotificationActionIntents.createDismissMessageIntent(context, messageReference).apply {
data = Uri.parse("data:,dismiss/${messageReference.toIdentityString()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
Expand All @@ -101,7 +101,7 @@ internal class K9NotificationActionCreator(
}

override fun createMarkMessageAsReadPendingIntent(messageReference: MessageReference): PendingIntent {
val intent = NotificationActionService.createMarkMessageAsReadIntent(context, messageReference).apply {
val intent = NotificationActionIntents.createMarkMessageAsReadIntent(context, messageReference).apply {
data = Uri.parse("data:,markAsRead/${messageReference.toIdentityString()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
Expand All @@ -113,7 +113,7 @@ internal class K9NotificationActionCreator(
): PendingIntent {
val accountUuid = account.uuid
val intent =
NotificationActionService.createMarkAllAsReadIntent(context, accountUuid, messageReferences).apply {
NotificationActionIntents.createMarkAllAsReadIntent(context, accountUuid, messageReferences).apply {
data = Uri.parse("data:,markAllAsRead/$accountUuid/${System.currentTimeMillis()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
Expand Down Expand Up @@ -144,7 +144,7 @@ internal class K9NotificationActionCreator(
}

private fun createDeleteServicePendingIntent(messageReference: MessageReference): PendingIntent {
val intent = NotificationActionService.createDeleteMessageIntent(context, messageReference).apply {
val intent = NotificationActionIntents.createDeleteMessageIntent(context, messageReference).apply {
data = Uri.parse("data:,delete/${messageReference.toIdentityString()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
Expand Down Expand Up @@ -181,14 +181,14 @@ internal class K9NotificationActionCreator(
): PendingIntent {
val accountUuid = account.uuid
val intent =
NotificationActionService.createDeleteAllMessagesIntent(context, accountUuid, messageReferences).apply {
NotificationActionIntents.createDeleteAllMessagesIntent(context, accountUuid, messageReferences).apply {
data = Uri.parse("data:,deleteAll/$accountUuid/${System.currentTimeMillis()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
}

override fun createArchiveMessagePendingIntent(messageReference: MessageReference): PendingIntent {
val intent = NotificationActionService.createArchiveMessageIntent(context, messageReference).apply {
val intent = NotificationActionIntents.createArchiveMessageIntent(context, messageReference).apply {
data = Uri.parse("data:,archive/${messageReference.toIdentityString()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
Expand All @@ -198,19 +198,26 @@ internal class K9NotificationActionCreator(
account: LegacyAccountDto,
messageReferences: List<MessageReference>,
): PendingIntent {
val intent = NotificationActionService.createArchiveAllIntent(context, account, messageReferences).apply {
val intent = NotificationActionIntents.createArchiveAllIntent(context, account, messageReferences).apply {
data = Uri.parse("data:,archiveAll/${account.uuid}/${System.currentTimeMillis()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
}

override fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference): PendingIntent {
val intent = NotificationActionService.createMarkMessageAsSpamIntent(context, messageReference).apply {
val intent = NotificationActionIntents.createMarkMessageAsSpamIntent(context, messageReference).apply {
data = Uri.parse("data:,spam/${messageReference.toIdentityString()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
}

override fun createMarkMessageAsStarPendingIntent(messageReference: MessageReference): PendingIntent {
val intent = NotificationActionIntents.createMarkMessageAsStarIntent(context, messageReference).apply {
data = Uri.parse("data:,star/${messageReference.toIdentityString()}")
}
return PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, false)!!
}

private fun createMessageListIntent(account: LegacyAccountDto): Intent {
val folderId = defaultFolderProvider.getDefaultFolder(account)
val search = LocalMessageSearch().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class K9NotificationResourceProvider(private val context: Context) : Notificatio
override val iconMarkAsRead: Int = Icons.Outlined.MarkEmailRead
override val iconDelete: Int = Icons.Outlined.Delete
override val iconReply: Int = Icons.Outlined.Reply
override val iconArchive: Int = Icons.Outlined.Archive
override val iconMarkAsSpam: Int = Icons.Outlined.Report
override val iconStar: Int = Icons.Outlined.Star
override val iconNewMail: Int = Icons.Outlined.MarkEmailUnread
override val iconSendingMail: Int = Icons.Outlined.Sync
override val iconCheckingMail: Int = Icons.Outlined.Sync
Expand Down Expand Up @@ -103,6 +106,8 @@ class K9NotificationResourceProvider(private val context: Context) : Notificatio

override fun actionReply(): String = context.getString(R.string.notification_action_reply)

override fun actionStar(): String = context.getString(R.string.notification_action_star)

override fun actionArchive(): String = context.getString(R.string.notification_action_archive)

override fun actionArchiveAll(): String = context.getString(R.string.notification_action_archive_all)
Expand Down
1 change: 1 addition & 0 deletions legacy/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
api(projects.core.mail.mailserver)
api(projects.core.android.common)
api(projects.core.android.account)
api(projects.core.common)
api(projects.core.preference.impl)
api(projects.core.android.logging)
api(projects.core.logging.implFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ internal enum class NotificationAction {
Reply,
MarkAsRead,
Delete,
Archive,
Spam,
Star,
}

internal enum class WearNotificationAction {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ interface NotificationActionCreator {
): PendingIntent

fun createMarkMessageAsSpamPendingIntent(messageReference: MessageReference): PendingIntent

fun createMarkMessageAsStarPendingIntent(messageReference: MessageReference): PendingIntent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.fsck.k9.notification

import android.content.Context
import android.content.Intent
import app.k9mail.legacy.message.controller.MessageReference
import com.fsck.k9.controller.MessageReferenceHelper
import net.thunderbird.core.android.account.LegacyAccountDto

internal const val ACTION_MARK_AS_READ = "ACTION_MARK_AS_READ"
internal const val ACTION_DELETE = "ACTION_DELETE"
internal const val ACTION_ARCHIVE = "ACTION_ARCHIVE"
internal const val ACTION_SPAM = "ACTION_SPAM"
internal const val ACTION_STAR = "ACTION_STAR"
internal const val ACTION_DISMISS = "ACTION_DISMISS"
internal const val EXTRA_ACCOUNT_UUID = "accountUuid"
internal const val EXTRA_MESSAGE_REFERENCE = "messageReference"
internal const val EXTRA_MESSAGE_REFERENCES = "messageReferences"

object NotificationActionIntents {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved several functions here from NotificationActionService.kt as having them there exceeded the maximum functions lint rule

fun createMarkMessageAsReadIntent(context: Context, messageReference: MessageReference): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_MARK_AS_READ
putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
putExtra(EXTRA_MESSAGE_REFERENCES, arrayListOf(messageReference.toIdentityString()))
}
}

fun createMarkAllAsReadIntent(
context: Context,
accountUuid: String,
messageReferences: List<MessageReference>,
): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_MARK_AS_READ
putExtra(EXTRA_ACCOUNT_UUID, accountUuid)
putExtra(
EXTRA_MESSAGE_REFERENCES,
MessageReferenceHelper.toMessageReferenceStringList(messageReferences),
)
}
}

fun createDismissMessageIntent(context: Context, messageReference: MessageReference): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_DISMISS
putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
putExtra(EXTRA_MESSAGE_REFERENCE, messageReference.toIdentityString())
}
}

fun createDismissAllMessagesIntent(context: Context, account: LegacyAccountDto): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_DISMISS
putExtra(EXTRA_ACCOUNT_UUID, account.uuid)
}
}

fun createDeleteMessageIntent(context: Context, messageReference: MessageReference): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_DELETE
putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
putExtra(EXTRA_MESSAGE_REFERENCES, arrayListOf(messageReference.toIdentityString()))
}
}

fun createDeleteAllMessagesIntent(
context: Context,
accountUuid: String,
messageReferences: List<MessageReference>,
): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_DELETE
putExtra(EXTRA_ACCOUNT_UUID, accountUuid)
putExtra(
EXTRA_MESSAGE_REFERENCES,
MessageReferenceHelper.toMessageReferenceStringList(messageReferences),
)
}
}

fun createArchiveMessageIntent(context: Context, messageReference: MessageReference): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_ARCHIVE
putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
putExtra(EXTRA_MESSAGE_REFERENCES, arrayListOf(messageReference.toIdentityString()))
}
}

fun createArchiveAllIntent(
context: Context,
account: LegacyAccountDto,
messageReferences: List<MessageReference>,
): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_ARCHIVE
putExtra(EXTRA_ACCOUNT_UUID, account.uuid)
putExtra(
EXTRA_MESSAGE_REFERENCES,
MessageReferenceHelper.toMessageReferenceStringList(messageReferences),
)
}
}

fun createMarkMessageAsSpamIntent(context: Context, messageReference: MessageReference): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_SPAM
putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
putExtra(EXTRA_MESSAGE_REFERENCE, messageReference.toIdentityString())
}
}

fun createMarkMessageAsStarIntent(context: Context, messageReference: MessageReference): Intent {
return Intent(context, NotificationActionService::class.java).apply {
action = ACTION_STAR
putExtra(EXTRA_ACCOUNT_UUID, messageReference.accountUuid)
putExtra(EXTRA_MESSAGE_REFERENCES, arrayListOf(messageReference.toIdentityString()))
}
}
}
Loading