Support "copy" action button to copy a value to the clipboard

This commit is contained in:
Philipp Heckel 2026-02-08 20:45:30 -05:00
parent 3daa41f89a
commit 018d3a875d
8 changed files with 34 additions and 6 deletions

View file

@ -156,6 +156,7 @@ class Backuper(val context: Context) {
body = a.body,
intent = a.intent,
extras = a.extras,
value = a.value,
progress = a.progress,
error = a.error
)
@ -316,6 +317,7 @@ class Backuper(val context: Context) {
body = a.body,
intent = a.intent,
extras = a.extras,
value = a.value,
progress = a.progress,
error = a.error
)
@ -459,7 +461,7 @@ data class Notification(
data class Action(
val id: String, // Synthetic ID to identify result, and easily pass via Broadcast and WorkManager
val action: String, // "view", "http" or "broadcast"
val action: String, // "view", "http", "broadcast", or "copy"
val label: String,
val clear: Boolean?, // clear notification after successful execution
val url: String?, // used in "view" and "http" actions
@ -468,6 +470,7 @@ data class Action(
val body: String?, // used in "http" action
val intent: String?, // used in "broadcast" action
val extras: Map<String,String>?, // used in "broadcast" action
val value: String? = null, // used in "copy" action
val progress: Int?, // used to indicate progress in popup
val error: String? // used to indicate errors in popup
)

View file

@ -222,7 +222,7 @@ data class Icon(
@Entity
data class Action(
@ColumnInfo(name = "id") val id: String, // Synthetic ID to identify result, and easily pass via Broadcast and WorkManager
@ColumnInfo(name = "action") val action: String, // "view", "http" or "broadcast"
@ColumnInfo(name = "action") val action: String, // "view", "http", "broadcast", or "copy"
@ColumnInfo(name = "label") val label: String,
@ColumnInfo(name = "clear") val clear: Boolean?, // clear notification after successful execution
@ColumnInfo(name = "url") val url: String?, // used in "view" and "http" actions
@ -231,6 +231,7 @@ data class Action(
@ColumnInfo(name = "body") val body: String?, // used in "http" action
@ColumnInfo(name = "intent") val intent: String?, // used in "broadcast" action
@ColumnInfo(name = "extras") val extras: Map<String,String>?, // used in "broadcast" action
@ColumnInfo(name = "value") val value: String?, // used in "copy" action
@ColumnInfo(name = "progress") val progress: Int?, // used to indicate progress in popup
@ColumnInfo(name = "error") val error: String?, // used to indicate errors in popup
)

View file

@ -37,7 +37,7 @@ data class MessageAttachment(
data class MessageAction(
val id: String,
val action: String,
val label: String, // "view", "broadcast" or "http"
val label: String, // "view", "broadcast", "http", or "copy"
val clear: Boolean?, // clear notification after successful execution
val url: String?, // used in "view" and "http" actions
val method: String?, // used in "http" action, default is POST (!)
@ -45,6 +45,7 @@ data class MessageAction(
val body: String?, // used in "http" action
val intent: String?, // used in "broadcast" action
val extras: Map<String,String>?, // used in "broadcast" action
val value: String?, // used in "copy" action
)
const val MESSAGE_ENCODING_BASE64 = "base64"

View file

@ -48,6 +48,7 @@ class NotificationParser {
body = a.body,
intent = a.intent,
extras = a.extras,
value = a.value,
progress = null,
error = null
)
@ -96,6 +97,7 @@ class NotificationParser {
body = a.body,
intent = a.intent,
extras = a.extras,
value = a.value,
progress = null,
error = null
)

View file

@ -267,7 +267,7 @@ class NotificationService(val context: Context) {
addViewUserActionWithoutClear(builder, action)
}
} else {
addHttpOrBroadcastUserAction(builder, notification, action)
addHttpBroadcastOrCopyUserAction(builder, notification, action)
}
}
}
@ -310,7 +310,7 @@ class NotificationService(val context: Context) {
}
}
private fun addHttpOrBroadcastUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) {
private fun addHttpBroadcastOrCopyUserAction(builder: NotificationCompat.Builder, notification: Notification, action: Action) {
val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_USER_ACTION)
putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id)
@ -323,7 +323,7 @@ class NotificationService(val context: Context) {
/**
* Receives the broadcast from
* - the "http" and "broadcast" action button (the "view" action is handled differently)
* - the "http", "broadcast", and "copy" action button (the "view" action is handled differently)
* - the "download"/"cancel" action button
*
* Then queues a Worker via WorkManager to execute the action in the background
@ -523,6 +523,7 @@ class NotificationService(val context: Context) {
const val ACTION_VIEW = "view"
const val ACTION_HTTP = "http"
const val ACTION_BROADCAST = "broadcast"
const val ACTION_COPY = "copy"
const val BROADCAST_EXTRA_TYPE = "type"
const val BROADCAST_EXTRA_NOTIFICATION_ID = "notificationId"

View file

@ -13,9 +13,11 @@ import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_BROADCAST
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_COPY
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_HTTP
import io.heckel.ntfy.util.HttpUtil
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.copyToClipboard
import io.heckel.ntfy.util.extractBaseUrl
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Locale
@ -45,6 +47,7 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
// ACTION_VIEW is not handled here. It's handled in the NotificationService and DetailAdapter.
ACTION_BROADCAST -> performBroadcastAction(action)
ACTION_HTTP -> performHttpAction(action)
ACTION_COPY -> performCopyAction(action)
}
} catch (e: Exception) {
Log.w(TAG, "Error executing action: ${e.message}", e)
@ -56,6 +59,15 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
return Result.success()
}
private fun performCopyAction(action: Action) {
val value = action.value ?: return
copyToClipboard(context, action.label, value)
if (action.clear == true) {
notifier.cancel(notification)
repository.markAsReadBySequenceId(subscription.id, notification.sequenceId)
}
}
private fun performBroadcastAction(action: Action) {
broadcaster.sendUserAction(action)
if (action.clear == true) {

View file

@ -32,6 +32,7 @@ import io.heckel.ntfy.msg.DownloadAttachmentWorker
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_COPY
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
import io.heckel.ntfy.util.*
import io.noties.markwon.Markwon
@ -515,6 +516,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
private fun runAction(context: Context, notification: Notification, action: Action): Boolean {
when (action.action) {
ACTION_VIEW -> runViewAction(context, action)
ACTION_COPY -> runCopyAction(context, action)
else -> runOtherUserAction(context, notification, action)
}
return true
@ -536,6 +538,11 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
}
}
private fun runCopyAction(context: Context, action: Action) {
val value = action.value ?: return
copyToClipboard(context, action.label, value)
}
private fun runOtherUserAction(context: Context, notification: Notification, action: Action) {
val intent = Intent(context, NotificationService.UserActionBroadcastReceiver::class.java).apply {
putExtra(NotificationService.BROADCAST_EXTRA_TYPE, NotificationService.BROADCAST_TYPE_USER_ACTION)

View file

@ -3,6 +3,7 @@ Features:
* Add "reconnecting to N topics ..." to foreground notification (#1101, thanks to @milosivanovic for reporting)
* Default server dialog with full-screen UI and stricter URL validation (ntfy-android#158)
* Show last notification time for UnifiedPush subscriptions (#1230, #1454, thanks to @Tealk and @user4andre for reporting)
* Support "copy" action button to copy a value to the clipboard (#1364, thanks to @SudoWatson for reporting)
Bug fixes + maintenance:
* Fix clear=true on action buttons not marking notification as read (#1029, thanks to @ElFishi for reporting)