Open "Click" link when tapping notification, #110
This commit is contained in:
parent
adbc247279
commit
ac0ecbdcc1
6 changed files with 126 additions and 90 deletions
|
@ -5,6 +5,7 @@ import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
@ -509,6 +510,17 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
private fun onNotificationClick(notification: Notification) {
|
private fun onNotificationClick(notification: Notification) {
|
||||||
if (actionMode != null) {
|
if (actionMode != null) {
|
||||||
handleActionModeClick(notification)
|
handleActionModeClick(notification)
|
||||||
|
} else if (notification.click != "") {
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(notification.click)))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Cannot open click URL", e)
|
||||||
|
runOnUiThread {
|
||||||
|
Toast
|
||||||
|
.makeText(this@DetailActivity, getString(R.string.detail_item_cannot_open_click_url, e.message), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
copyToClipboard(notification)
|
copyToClipboard(notification)
|
||||||
}
|
}
|
||||||
|
@ -516,14 +528,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
|
|
||||||
private fun copyToClipboard(notification: Notification) {
|
private fun copyToClipboard(notification: Notification) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val message = decodeMessage(notification)
|
copyToClipboard(this, notification)
|
||||||
val text = message + "\n\n" + Date(notification.timestamp * 1000).toString()
|
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("notification message", text)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast
|
|
||||||
.makeText(this, getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,11 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.util.Log
|
|
||||||
import io.heckel.ntfy.msg.DownloadManager
|
import io.heckel.ntfy.msg.DownloadManager
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
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 repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
||||||
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
|
ListAdapter<Notification, DetailAdapter.DetailViewHolder>(TopicDiffCallback) {
|
||||||
|
@ -98,8 +96,11 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
if (selected.contains(notification.id)) {
|
if (selected.contains(notification.id)) {
|
||||||
itemView.setBackgroundResource(Colors.itemSelectedBackground(context))
|
itemView.setBackgroundResource(Colors.itemSelectedBackground(context))
|
||||||
}
|
}
|
||||||
|
val attachment = notification.attachment
|
||||||
|
val exists = if (attachment?.contentUri != null) fileExists(context, attachment.contentUri) else false
|
||||||
renderPriority(context, notification)
|
renderPriority(context, notification)
|
||||||
maybeRenderAttachment(context, notification)
|
maybeRenderMenu(context, notification, exists)
|
||||||
|
maybeRenderAttachment(context, notification, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderPriority(context: Context, notification: Notification) {
|
private fun renderPriority(context: Context, notification: Notification) {
|
||||||
|
@ -126,23 +127,20 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRenderAttachment(context: Context, notification: Notification) {
|
private fun maybeRenderAttachment(context: Context, notification: Notification, exists: Boolean) {
|
||||||
if (notification.attachment == null) {
|
if (notification.attachment == null) {
|
||||||
menuButton.visibility = View.GONE
|
|
||||||
attachmentImageView.visibility = View.GONE
|
attachmentImageView.visibility = View.GONE
|
||||||
attachmentBoxView.visibility = View.GONE
|
attachmentBoxView.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val attachment = notification.attachment
|
val attachment = notification.attachment
|
||||||
val exists = if (attachment.contentUri != null) fileExists(context, attachment.contentUri) else false
|
|
||||||
val image = attachment.contentUri != null && exists && supportedImage(attachment.type)
|
val image = attachment.contentUri != null && exists && supportedImage(attachment.type)
|
||||||
maybeRenderMenu(context, notification, attachment, exists)
|
|
||||||
maybeRenderAttachmentImage(context, attachment, image)
|
maybeRenderAttachmentImage(context, attachment, image)
|
||||||
maybeRenderAttachmentBox(context, notification, attachment, exists, image)
|
maybeRenderAttachmentBox(context, notification, attachment, exists, image)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRenderMenu(context: Context, notification: Notification, attachment: Attachment, exists: Boolean) {
|
private fun maybeRenderMenu(context: Context, notification: Notification, exists: Boolean) {
|
||||||
val menuButtonPopupMenu = createAttachmentPopup(context, menuButton, notification, attachment, exists) // Heavy lifting not during on-click
|
val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, exists) // Heavy lifting not during on-click
|
||||||
if (menuButtonPopupMenu != null) {
|
if (menuButtonPopupMenu != null) {
|
||||||
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
||||||
menuButton.visibility = View.VISIBLE
|
menuButton.visibility = View.VISIBLE
|
||||||
|
@ -158,7 +156,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
}
|
}
|
||||||
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
|
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
|
||||||
attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type))
|
attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type))
|
||||||
val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click
|
val attachmentBoxPopupMenu = maybeCreateMenuPopup(context, attachmentBoxView, notification, exists) // Heavy lifting not during on-click
|
||||||
if (attachmentBoxPopupMenu != null) {
|
if (attachmentBoxPopupMenu != null) {
|
||||||
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
||||||
} else {
|
} else {
|
||||||
|
@ -171,93 +169,107 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
attachmentBoxView.visibility = View.VISIBLE
|
attachmentBoxView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAttachmentPopup(context: Context, anchor: View?, notification: Notification, attachment: Attachment, exists: Boolean): PopupMenu? {
|
private fun maybeCreateMenuPopup(context: Context, anchor: View?, notification: Notification, exists: Boolean): PopupMenu? {
|
||||||
val popup = PopupMenu(context, anchor)
|
val popup = PopupMenu(context, anchor)
|
||||||
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
||||||
|
val attachment = notification.attachment // May be null
|
||||||
|
val hasAttachment = attachment != null
|
||||||
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
||||||
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
|
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
|
||||||
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
||||||
val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse)
|
val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse)
|
||||||
val deleteItem = popup.menu.findItem(R.id.detail_item_menu_delete)
|
val deleteItem = popup.menu.findItem(R.id.detail_item_menu_delete)
|
||||||
val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url)
|
val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url)
|
||||||
val expired = attachment.expires != null && attachment.expires < System.currentTimeMillis()/1000
|
val copyContentsItem = popup.menu.findItem(R.id.detail_item_menu_contents)
|
||||||
val inProgress = attachment.progress in 0..99
|
val expired = attachment?.expires != null && attachment.expires < System.currentTimeMillis()/1000
|
||||||
if (attachment.contentUri != null) {
|
val inProgress = attachment?.progress in 0..99
|
||||||
openItem.setOnMenuItemClickListener {
|
if (attachment != null) {
|
||||||
try {
|
if (attachment.contentUri != null) {
|
||||||
val contentUri = Uri.parse(attachment.contentUri)
|
openItem.setOnMenuItemClickListener {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, contentUri)
|
try {
|
||||||
intent.setDataAndType(contentUri, attachment.type ?: "application/octet-stream") // Required for Android <= P
|
val contentUri = Uri.parse(attachment.contentUri)
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
val intent = Intent(Intent.ACTION_VIEW, contentUri)
|
||||||
context.startActivity(intent)
|
intent.setDataAndType(contentUri, attachment.type ?: "application/octet-stream") // Required for Android <= P
|
||||||
} catch (e: ActivityNotFoundException) {
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
Toast
|
context.startActivity(intent)
|
||||||
.makeText(context, context.getString(R.string.detail_item_cannot_open_not_found), Toast.LENGTH_LONG)
|
} catch (e: ActivityNotFoundException) {
|
||||||
.show()
|
Toast
|
||||||
} catch (e: Exception) {
|
.makeText(context, context.getString(R.string.detail_item_cannot_open_not_found), Toast.LENGTH_LONG)
|
||||||
Toast
|
.show()
|
||||||
.makeText(context, context.getString(R.string.detail_item_cannot_open, e.message), Toast.LENGTH_LONG)
|
} catch (e: Exception) {
|
||||||
.show()
|
Toast
|
||||||
}
|
.makeText(context, context.getString(R.string.detail_item_cannot_open, e.message), Toast.LENGTH_LONG)
|
||||||
true
|
.show()
|
||||||
}
|
|
||||||
}
|
|
||||||
browseItem.setOnMenuItemClickListener {
|
|
||||||
val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS)
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
context.startActivity(intent)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
if (attachment.contentUri != null) {
|
|
||||||
deleteItem.setOnMenuItemClickListener {
|
|
||||||
try {
|
|
||||||
val contentUri = Uri.parse(attachment.contentUri)
|
|
||||||
val resolver = context.applicationContext.contentResolver
|
|
||||||
val deleted = resolver.delete(contentUri, null, null) > 0
|
|
||||||
if (!deleted) throw Exception("no rows deleted")
|
|
||||||
val newAttachment = attachment.copy(progress = PROGRESS_DELETED)
|
|
||||||
val newNotification = notification.copy(attachment = newAttachment)
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
repository.updateNotification(newNotification)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
true
|
||||||
Log.w(TAG, "Failed to update notification: ${e.message}", e)
|
|
||||||
Toast
|
|
||||||
.makeText(context, context.getString(R.string.detail_item_delete_failed, e.message), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
browseItem.setOnMenuItemClickListener {
|
||||||
|
val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
context.startActivity(intent)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
if (attachment.contentUri != null) {
|
||||||
|
deleteItem.setOnMenuItemClickListener {
|
||||||
|
try {
|
||||||
|
val contentUri = Uri.parse(attachment.contentUri)
|
||||||
|
val resolver = context.applicationContext.contentResolver
|
||||||
|
val deleted = resolver.delete(contentUri, null, null) > 0
|
||||||
|
if (!deleted) throw Exception("no rows deleted")
|
||||||
|
val newAttachment = attachment.copy(progress = PROGRESS_DELETED)
|
||||||
|
val newNotification = notification.copy(attachment = newAttachment)
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
repository.updateNotification(newNotification)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to update notification: ${e.message}", e)
|
||||||
|
Toast
|
||||||
|
.makeText(context, context.getString(R.string.detail_item_delete_failed, e.message), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copyUrlItem.setOnMenuItemClickListener {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("attachment url", attachment.url)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast
|
||||||
|
.makeText(context, context.getString(R.string.detail_item_menu_copy_url_copied), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
downloadItem.setOnMenuItemClickListener {
|
||||||
|
val requiresPermission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED
|
||||||
|
if (requiresPermission) {
|
||||||
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD)
|
||||||
|
return@setOnMenuItemClickListener true
|
||||||
|
}
|
||||||
|
DownloadManager.enqueue(context, notification.id, userAction = true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
cancelItem.setOnMenuItemClickListener {
|
||||||
|
DownloadManager.cancel(context, notification.id)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
copyUrlItem.setOnMenuItemClickListener {
|
if (notification.click != "") {
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
copyContentsItem.setOnMenuItemClickListener {
|
||||||
val clip = ClipData.newPlainText("attachment url", attachment.url)
|
copyToClipboard(context, notification)
|
||||||
clipboard.setPrimaryClip(clip)
|
true
|
||||||
Toast
|
|
||||||
.makeText(context, context.getString(R.string.detail_item_menu_copy_url_copied), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
downloadItem.setOnMenuItemClickListener {
|
|
||||||
val requiresPermission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED
|
|
||||||
if (requiresPermission) {
|
|
||||||
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD)
|
|
||||||
return@setOnMenuItemClickListener true
|
|
||||||
}
|
}
|
||||||
DownloadManager.enqueue(context, notification.id, userAction = true)
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
cancelItem.setOnMenuItemClickListener {
|
openItem.isVisible = hasAttachment && exists
|
||||||
DownloadManager.cancel(context, notification.id)
|
browseItem.isVisible = hasAttachment && exists
|
||||||
true
|
downloadItem.isVisible = hasAttachment && !exists && !expired && !inProgress
|
||||||
}
|
deleteItem.isVisible = hasAttachment && exists
|
||||||
openItem.isVisible = exists
|
copyUrlItem.isVisible = hasAttachment && !expired
|
||||||
browseItem.isVisible = exists
|
cancelItem.isVisible = hasAttachment && inProgress
|
||||||
downloadItem.isVisible = !exists && !expired && !inProgress
|
copyContentsItem.isVisible = notification.click != ""
|
||||||
deleteItem.isVisible = exists
|
val noOptions = !openItem.isVisible && !browseItem.isVisible && !downloadItem.isVisible
|
||||||
copyUrlItem.isVisible = !expired
|
&& !copyUrlItem.isVisible && !cancelItem.isVisible && !deleteItem.isVisible
|
||||||
cancelItem.isVisible = inProgress
|
&& !copyContentsItem.isVisible
|
||||||
val noOptions = !openItem.isVisible && !browseItem.isVisible && !downloadItem.isVisible && !copyUrlItem.isVisible && !cancelItem.isVisible && !deleteItem.isVisible
|
|
||||||
if (noOptions) {
|
if (noOptions) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package io.heckel.ntfy.util
|
||||||
|
|
||||||
import android.animation.ArgbEvaluator
|
import android.animation.ArgbEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
@ -18,6 +20,7 @@ import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
|
@ -366,3 +369,14 @@ fun ensureSafeNewFile(dir: File, name: String): File {
|
||||||
}
|
}
|
||||||
throw Exception("Cannot find safe file")
|
throw Exception("Cannot find safe file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun copyToClipboard(context: Context, notification: Notification) {
|
||||||
|
val message = decodeMessage(notification)
|
||||||
|
val text = message + "\n\n" + formatDateShort(notification.timestamp)
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("notification message", text)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast
|
||||||
|
.makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
|
@ -6,4 +6,5 @@
|
||||||
<item android:id="@+id/detail_item_menu_browse" android:title="@string/detail_item_menu_browse"/>
|
<item android:id="@+id/detail_item_menu_browse" android:title="@string/detail_item_menu_browse"/>
|
||||||
<item android:id="@+id/detail_item_menu_delete" android:title="@string/detail_item_menu_delete"/>
|
<item android:id="@+id/detail_item_menu_delete" android:title="@string/detail_item_menu_delete"/>
|
||||||
<item android:id="@+id/detail_item_menu_copy_url" android:title="@string/detail_item_menu_copy_url"/>
|
<item android:id="@+id/detail_item_menu_copy_url" android:title="@string/detail_item_menu_copy_url"/>
|
||||||
|
<item android:id="@+id/detail_item_menu_contents" android:title="@string/detail_item_menu_copy_contents"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -144,9 +144,12 @@
|
||||||
<string name="detail_item_menu_cancel">Cancel download</string>
|
<string name="detail_item_menu_cancel">Cancel download</string>
|
||||||
<string name="detail_item_menu_copy_url">Copy URL</string>
|
<string name="detail_item_menu_copy_url">Copy URL</string>
|
||||||
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
|
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
|
||||||
|
<string name="detail_item_menu_copy_contents">Copy notification</string>
|
||||||
|
<string name="detail_item_menu_copy_contents_copied">Copied notification clipboard</string>
|
||||||
<string name="detail_item_cannot_download">Cannot open or download attachment. Link expired and no local file found.</string>
|
<string name="detail_item_cannot_download">Cannot open or download attachment. Link expired and no local file found.</string>
|
||||||
<string name="detail_item_cannot_open">Cannot open attachment: %1$s</string>
|
<string name="detail_item_cannot_open">Cannot open attachment: %1$s</string>
|
||||||
<string name="detail_item_cannot_open_not_found">Cannot open attachment: File may have been deleted, or there is no app to open the file.</string>
|
<string name="detail_item_cannot_open_not_found">Cannot open attachment: File may have been deleted, or there is no app to open the file.</string>
|
||||||
|
<string name="detail_item_cannot_open_click_url">Cannot open click URL: %1$s</string>
|
||||||
<string name="detail_item_delete_failed">Cannot delete attachment: %1$s</string>
|
<string name="detail_item_delete_failed">Cannot delete attachment: %1$s</string>
|
||||||
<string name="detail_item_download_failed">Attachment download failed: %1$s</string>
|
<string name="detail_item_download_failed">Attachment download failed: %1$s</string>
|
||||||
<string name="detail_item_download_info_not_downloaded">not downloaded</string>
|
<string name="detail_item_download_info_not_downloaded">not downloaded</string>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
Features:
|
Features:
|
||||||
* Support for UnifiedPush 2.0 specification (bytes messages, #130)
|
* Support for UnifiedPush 2.0 specification (bytes messages, #130)
|
||||||
* Export/import settings and subscriptions (#115, thanks @cmeis for reporting)
|
* Export/import settings and subscriptions (#115, thanks @cmeis for reporting)
|
||||||
|
* Open "Click" link when tapping notification (#110, thanks @cmeis for reporting)
|
||||||
* JSON stream deprecation banner (#164)
|
* JSON stream deprecation banner (#164)
|
||||||
|
|
||||||
Bugs:
|
Bugs:
|
||||||
|
|
Loading…
Reference in a new issue