Make messages with links selectable

This commit is contained in:
Philipp Heckel 2022-04-29 11:03:02 -04:00
parent 0cff98e72e
commit 4771ccc6c0
5 changed files with 51 additions and 9 deletions

View file

@ -188,7 +188,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
val onNotificationClick = { n: Notification -> onNotificationClick(n) } val onNotificationClick = { n: Notification -> onNotificationClick(n) }
val onNotificationLongClick = { n: Notification -> onNotificationLongClick(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 = findViewById(R.id.detail_notification_list)
mainList.adapter = adapter mainList.adapter = adapter

View file

@ -6,9 +6,11 @@ import android.content.*
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.RippleDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.Handler
import android.provider.MediaStore import android.provider.MediaStore
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View 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
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) { ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
val selected = mutableSetOf<String>() // Notification IDs val selected = mutableSetOf<String>() // Notification IDs
@ -41,7 +42,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_detail_item, parent, false) .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. */ /* 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. */ /* 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<String>, 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<String>, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
private var notification: Notification? = null 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 cardView: CardView = itemView.findViewById(R.id.detail_item_card)
private val priorityImageView: ImageView = itemView.findViewById(R.id.detail_item_priority_image) private val priorityImageView: ImageView = itemView.findViewById(R.id.detail_item_priority_image)
private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text) 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) dateView.text = formatDateShort(notification.timestamp)
messageView.text = maybeAppendActionErrors(formatMessage(notification), notification) 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 newDotImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
cardView.setOnClickListener { onClick(notification) } cardView.setOnClickListener { onClick(notification) }
cardView.setOnLongClickListener { onLongClick(notification); true } cardView.setOnLongClickListener { onLongClick(notification); true }

View file

@ -9,6 +9,7 @@ import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.RippleDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
@ -25,6 +26,10 @@ import androidx.appcompat.app.AppCompatDelegate
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 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
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody import okhttp3.RequestBody
@ -368,6 +373,29 @@ fun View.makeEndIconSmaller(resources: Resources) {
requestLayout() 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 // TextWatcher that only implements the afterTextChanged method
class AfterChangedTextWatcher(val afterTextChangedFn: (s: Editable?) -> Unit) : TextWatcher { class AfterChangedTextWatcher(val afterTextChangedFn: (s: Editable?) -> Unit) : TextWatcher {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {

View file

@ -20,6 +20,7 @@
app:cardPreventCornerOverlap="true" app:cardPreventCornerOverlap="true"
app:cardUseCompatPadding="true"> app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/detail_item_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"

View file

@ -3,8 +3,8 @@ Features:
Bugs: Bugs:
* Accurate naming of "mute notifications" from "pause notifications" (#224, thanks to @shadow00 for reporting) * Accurate naming of "mute notifications" from "pause notifications" (#224, thanks to @shadow00 for reporting)
* Make messages with links selectable (#226, thanks to @StoyanDimitrov for reporting)
**Thanks for testing:** **Thanks for testing:**
Thanks to [@cmeis](https://github.com/cmeis), [@StoyanDimitrov](https://github.com/StoyanDimitrov), [@Fallenbagel](https://github.com/Fallenbagel) for testing, and Thanks to @cmeis, @StoyanDimitrov, @Fallenbagel for testing, and to @Joeharrison94 for the input.
to [@Joeharrison94](https://github.com/Joeharrison94) for the input.