From 4771ccc6c0e98892299b50f3eeef3a92acfba8c0 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Fri, 29 Apr 2022 11:03:02 -0400 Subject: [PATCH] Make messages with links selectable --- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 2 +- .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 25 +++++++++++++---- app/src/main/java/io/heckel/ntfy/util/Util.kt | 28 +++++++++++++++++++ .../main/res/layout/fragment_detail_item.xml | 1 + .../metadata/android/en-US/changelog/27.txt | 4 +-- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index c1ab2b2..7c62745 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -188,7 +188,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra val onNotificationClick = { n: Notification -> onNotificationClick(n) } val onNotificationLongClick = { n: Notification -> onNotificationLongClick(n) } - adapter = DetailAdapter(this, repository, onNotificationClick, onNotificationLongClick) + adapter = DetailAdapter(this, lifecycleScope, repository, onNotificationClick, onNotificationLongClick) mainList = findViewById(R.id.detail_notification_list) mainList.adapter = adapter diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index 34c3088..ff84bb4 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -6,9 +6,11 @@ import android.content.* import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.drawable.RippleDrawable import android.net.Uri import android.os.Build import android.os.Environment +import android.os.Handler import android.provider.MediaStore import android.view.LayoutInflater import android.view.View @@ -29,11 +31,10 @@ import io.heckel.ntfy.msg.DownloadWorker import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW import io.heckel.ntfy.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.* -class DetailAdapter(private val activity: Activity, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) : + +class DetailAdapter(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) : ListAdapter(TopicDiffCallback) { val selected = mutableSetOf() // Notification IDs @@ -41,7 +42,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.fragment_detail_item, parent, false) - return DetailViewHolder(activity, repository, view, selected, onClick, onLongClick) + return DetailViewHolder(activity, lifecycleScope, repository, view, selected, onClick, onLongClick) } /* Gets current topic and uses it to bind view. */ @@ -62,9 +63,10 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo } /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */ - class DetailViewHolder(private val activity: Activity, private val repository: Repository, itemView: View, private val selected: Set, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) : + class DetailViewHolder(private val activity: Activity, private val lifecycleScope: CoroutineScope, private val repository: Repository, itemView: View, private val selected: Set, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) : RecyclerView.ViewHolder(itemView) { private var notification: Notification? = null + private val layout: View = itemView.findViewById(R.id.detail_item_layout) private val cardView: CardView = itemView.findViewById(R.id.detail_item_card) private val priorityImageView: ImageView = itemView.findViewById(R.id.detail_item_priority_image) private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text) @@ -86,6 +88,17 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo dateView.text = formatDateShort(notification.timestamp) messageView.text = maybeAppendActionErrors(formatMessage(notification), notification) + messageView.setOnClickListener { + // Click & Long-click listeners on the text as well, because "autoLink=web" makes them + // clickable, and so we cannot rely on the underlying card to perform the action. + // It's weird because "layout" is the ripple-able, but the card is clickable. + // See https://github.com/binwiederhier/ntfy/issues/226 + layout.ripple(lifecycleScope) + onClick(notification) + } + messageView.setOnLongClickListener { + onLongClick(notification); true + } newDotImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE cardView.setOnClickListener { onClick(notification) } cardView.setOnLongClickListener { onLongClick(notification); true } diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index cea4c86..9ab0ac0 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -9,6 +9,7 @@ import android.content.Context import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Resources +import android.graphics.drawable.RippleDrawable import android.net.Uri import android.os.Build import android.os.PowerManager @@ -25,6 +26,10 @@ import androidx.appcompat.app.AppCompatDelegate import io.heckel.ntfy.R import io.heckel.ntfy.db.* import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody @@ -368,6 +373,29 @@ fun View.makeEndIconSmaller(resources: Resources) { requestLayout() } +// Shows the ripple effect on the view, if it is ripple-able, see https://stackoverflow.com/a/56314062/1440785 +fun View.showRipple() { + if (background is RippleDrawable) { + background.state = intArrayOf(android.R.attr.state_pressed, android.R.attr.state_enabled) + } +} + +// Hides the ripple effect on the view, if it is ripple-able, see https://stackoverflow.com/a/56314062/1440785 +fun View.hideRipple() { + if (background is RippleDrawable) { + background.state = intArrayOf() + } +} + +// Toggles the ripple effect on the view, if it is ripple-able +fun View.ripple(scope: CoroutineScope) { + showRipple() + scope.launch(Dispatchers.Main) { + delay(200) + hideRipple() + } +} + // TextWatcher that only implements the afterTextChanged method class AfterChangedTextWatcher(val afterTextChangedFn: (s: Editable?) -> Unit) : TextWatcher { override fun afterTextChanged(s: Editable?) { diff --git a/app/src/main/res/layout/fragment_detail_item.xml b/app/src/main/res/layout/fragment_detail_item.xml index 990786c..c5ea539 100644 --- a/app/src/main/res/layout/fragment_detail_item.xml +++ b/app/src/main/res/layout/fragment_detail_item.xml @@ -20,6 +20,7 @@ app:cardPreventCornerOverlap="true" app:cardUseCompatPadding="true">