diff --git a/app/build.gradle b/app/build.gradle
index bbe79de..bde4f87 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -91,4 +91,7 @@ dependencies {
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.liveDataVersion"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+
+ // Emojis (tags and such)
+ implementation 'com.vdurmont:emoji-java:5.1.1'
}
diff --git a/app/schemas/io.heckel.ntfy.data.Database/4.json b/app/schemas/io.heckel.ntfy.data.Database/4.json
new file mode 100644
index 0000000..eefb0af
--- /dev/null
+++ b/app/schemas/io.heckel.ntfy.data.Database/4.json
@@ -0,0 +1,138 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "06bd845a8d39dd10549f1aeb6b40d7c5",
+ "entities": [
+ {
+ "tableName": "Subscription",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "topic",
+ "columnName": "topic",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "instant",
+ "columnName": "instant",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mutedUntil",
+ "columnName": "mutedUntil",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Subscription_baseUrl_topic",
+ "unique": true,
+ "columnNames": [
+ "baseUrl",
+ "topic"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Notification",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`, `subscriptionId`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subscriptionId",
+ "columnName": "subscriptionId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationId",
+ "columnName": "notificationId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "3"
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id",
+ "subscriptionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '06bd845a8d39dd10549f1aeb6b40d7c5')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d3c2351..9210db0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
+
) {
val url = topicUrl(baseUrl, topic)
Log.d(TAG, "Publishing to $url")
- val request = Request.Builder().url(url).put(message.toRequestBody()).build();
- client.newCall(request).execute().use { response ->
+ var builder = Request.Builder()
+ .url(url)
+ .addHeader("X-Priority", priority.toString())
+ .put(message.toRequestBody())
+ if (tags.isNotEmpty()) {
+ builder = builder.addHeader("X-Tags", tags.joinToString(","))
+ }
+ if (title.isNotEmpty()) {
+ builder = builder.addHeader("X-Title", title)
+ }
+ client.newCall(builder.build()).execute().use { response ->
if (!response.isSuccessful) {
throw Exception("Unexpected response ${response.code} when publishing to $url")
}
@@ -87,7 +98,10 @@ class ApiService {
id = message.id,
subscriptionId = 0, // TO BE SET downstream
timestamp = message.time,
+ title = message.title ?: "",
message = message.message,
+ priority = toPriority(message.priority),
+ tags = joinTags(message.tags),
notificationId = Random.nextInt(),
deleted = false
)
@@ -109,7 +123,17 @@ class ApiService {
private fun fromString(subscriptionId: Long, s: String): Notification {
val message = gson.fromJson(s, Message::class.java)
- return Notification(message.id, subscriptionId, message.time, message.message, notificationId = 0, deleted = false)
+ return Notification(
+ id = message.id,
+ subscriptionId = subscriptionId,
+ timestamp = message.time,
+ title = message.title ?: "",
+ message = message.message,
+ priority = toPriority(message.priority),
+ tags = joinTags(message.tags),
+ notificationId = 0,
+ deleted = false
+ )
}
/* This annotation ensures that proguard still works in production builds,
@@ -120,6 +144,9 @@ class ApiService {
val time: Long,
val event: String,
val topic: String,
+ val priority: Int?,
+ val tags: List?,
+ val title: String?,
val message: String
)
diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
index 0eb3882..57409b4 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
@@ -6,23 +6,25 @@ import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
+import android.graphics.Color
import android.media.RingtoneManager
import android.os.Build
import android.util.Log
+import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
-import io.heckel.ntfy.data.topicShortUrl
+import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
-import kotlin.random.Random
+import io.heckel.ntfy.util.formatMessage
+import io.heckel.ntfy.util.formatTitle
class NotificationService(val context: Context) {
fun send(subscription: Subscription, notification: Notification) {
- val title = topicShortUrl(subscription.baseUrl, subscription.topic)
- Log.d(TAG, "Displaying notification $title: ${notification.message}")
+ Log.d(TAG, "Displaying notification $notification")
// Create an Intent for the activity you want to start
val intent = Intent(context, DetailActivity::class.java)
@@ -36,22 +38,33 @@ class NotificationService(val context: Context) {
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack
}
+ val title = formatTitle(subscription, notification)
+ val message = formatMessage(notification)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
- val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
+ val channelId = toChannelId(notification.priority)
+ var notificationBuilder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.primaryColor))
.setContentTitle(title)
- .setContentText(notification.message)
- .setStyle(NotificationCompat.BigTextStyle().bigText(notification.message))
+ .setContentText(message)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent) // Click target for notification
.setAutoCancel(true) // Cancel when notification is clicked
+ if (notification.priority == 4) {
+ notificationBuilder = notificationBuilder
+ .setVibrate(longArrayOf(500, 500, 500, 500, 500, 500))
+ .setLights(Color.YELLOW, 3000, 3000)
+ } else if (notification.priority == 5) {
+ notificationBuilder = notificationBuilder
+ .setVibrate(longArrayOf(1000, 500, 1000, 500, 1000, 500))
+ .setLights(Color.RED, 3000, 3000)
+ }
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val channelName = context.getString(R.string.channel_notifications_name) // Show's up in UI
- val channel = NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT)
- notificationManager.createNotificationChannel(channel)
+ createNotificationChannel(notificationManager, notification)
}
notificationManager.notify(notification.notificationId, notificationBuilder.build())
}
@@ -64,8 +77,34 @@ class NotificationService(val context: Context) {
}
}
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createNotificationChannel(notificationManager: NotificationManager, notification: Notification) {
+ val channel = when (notification.priority) {
+ 1 -> NotificationChannel(CHANNEL_ID_MIN, context.getString(R.string.channel_notifications_min_name), NotificationManager.IMPORTANCE_MIN)
+ 2 -> NotificationChannel(CHANNEL_ID_LOW, context.getString(R.string.channel_notifications_low_name), NotificationManager.IMPORTANCE_LOW)
+ 4 -> NotificationChannel(CHANNEL_ID_HIGH, context.getString(R.string.channel_notifications_high_name), NotificationManager.IMPORTANCE_HIGH)
+ 5 -> NotificationChannel(CHANNEL_ID_MAX, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_MAX)
+ else -> NotificationChannel(CHANNEL_ID_DEFAULT, context.getString(R.string.channel_notifications_default_name), NotificationManager.IMPORTANCE_DEFAULT)
+ }
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun toChannelId(priority: Int): String {
+ return when (priority) {
+ 1 -> CHANNEL_ID_MIN
+ 2 -> CHANNEL_ID_LOW
+ 4 -> CHANNEL_ID_HIGH
+ 5 -> CHANNEL_ID_MAX
+ else -> CHANNEL_ID_DEFAULT
+ }
+ }
+
companion object {
private const val TAG = "NtfyNotificationService"
- private const val CHANNEL_ID = "ntfy"
+ private const val CHANNEL_ID_MIN = "ntfy-min"
+ private const val CHANNEL_ID_LOW = "ntfy-low"
+ private const val CHANNEL_ID_DEFAULT = "ntfy"
+ private const val CHANNEL_ID_HIGH = "ntfy-high"
+ private const val CHANNEL_ID_MAX = "ntfy-max"
}
}
diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt b/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt
index 6e4e98f..e26cd68 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberConnection.kt
@@ -4,7 +4,7 @@ import android.util.Log
import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
-import io.heckel.ntfy.data.topicUrl
+import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.*
import okhttp3.Call
import java.util.concurrent.atomic.AtomicBoolean
diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt
index a9f574f..07b19e3 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt
@@ -15,7 +15,7 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Subscription
-import io.heckel.ntfy.data.topicUrl
+import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.ui.MainActivity
import kotlinx.coroutines.*
import java.util.concurrent.ConcurrentHashMap
diff --git a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt
index 0472144..36f80db 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt
@@ -121,8 +121,10 @@ class AddFragment : DialogFragment() {
if (baseUrls.count() == 1) {
baseUrlLayout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
baseUrlText.setText(baseUrls.first())
- } else {
+ } else if (baseUrls.count() > 1) {
baseUrlLayout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
+ } else {
+ baseUrlLayout.setEndIconDrawable(0)
}
}
}
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 83baac0..53dffdd 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
@@ -27,11 +27,13 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
-import io.heckel.ntfy.data.topicShortUrl
-import io.heckel.ntfy.data.topicUrl
+import io.heckel.ntfy.util.topicShortUrl
+import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService
+import io.heckel.ntfy.util.fadeStatusBarColor
+import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.*
import java.util.*
import kotlin.random.Random
@@ -324,8 +326,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
lifecycleScope.launch(Dispatchers.IO) {
try {
- val message = getString(R.string.detail_test_message, Date().toString())
- api.publish(subscriptionBaseUrl, subscriptionTopic, message)
+ val possibleTags = listOf("warning", "skull", "success", "triangular_flag_on_post", "de", "dog", "rotating_light", "cat", "bike")
+ val priority = Random.nextInt(1, 6)
+ val tags = possibleTags.shuffled().take(Random.nextInt(0, 3))
+ val title = if (Random.nextBoolean()) getString(R.string.detail_test_title) else ""
+ val message = getString(R.string.detail_test_message, priority)
+ api.publish(subscriptionBaseUrl, subscriptionTopic, message, title, priority, tags)
} catch (e: Exception) {
runOnUiThread {
Toast
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 e3a5534..b39dd34 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
@@ -3,12 +3,16 @@ package io.heckel.ntfy.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.ImageView
import android.widget.TextView
+import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Notification
+import io.heckel.ntfy.util.formatMessage
+import io.heckel.ntfy.util.formatTitle
import java.util.*
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
@@ -39,20 +43,51 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
class DetailViewHolder(itemView: View, private val selected: Set, val onClick: (Notification) -> Unit, val onLongClick: (Notification) -> Unit) :
RecyclerView.ViewHolder(itemView) {
private var notification: Notification? = null
+ 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 titleView: TextView = itemView.findViewById(R.id.detail_item_title_text)
private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text)
- private val newImageView: View = itemView.findViewById(R.id.detail_item_new)
+ private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
fun bind(notification: Notification) {
this.notification = notification
+
dateView.text = Date(notification.timestamp * 1000).toString()
- messageView.text = notification.message
+ messageView.text = formatMessage(notification)
newImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
itemView.setOnClickListener { onClick(notification) }
itemView.setOnLongClickListener { onLongClick(notification); true }
+ if (notification.title != "") {
+ titleView.visibility = View.VISIBLE
+ titleView.text = formatTitle(notification)
+ } else {
+ titleView.visibility = View.GONE
+ }
if (selected.contains(notification.id)) {
itemView.setBackgroundResource(R.color.primarySelectedRowColor);
}
+ val ctx = itemView.context
+ when (notification.priority) {
+ 1 -> {
+ priorityImageView.visibility = View.VISIBLE
+ priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_1_24dp))
+ }
+ 2 -> {
+ priorityImageView.visibility = View.VISIBLE
+ priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_2_24dp))
+ }
+ 3 -> {
+ priorityImageView.visibility = View.GONE
+ }
+ 4 -> {
+ priorityImageView.visibility = View.VISIBLE
+ priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_4_24dp))
+ }
+ 5 -> {
+ priorityImageView.visibility = View.VISIBLE
+ priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_5_24dp))
+ }
+ }
}
}
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
index 9ce23b9..73953e4 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -22,11 +22,13 @@ import androidx.work.*
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Subscription
-import io.heckel.ntfy.data.topicShortUrl
+import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.work.PollWorker
import io.heckel.ntfy.firebase.FirebaseMessenger
+import io.heckel.ntfy.util.fadeStatusBarColor
+import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
index 9cf7e24..adf52a5 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
@@ -11,11 +11,10 @@ import androidx.recyclerview.widget.RecyclerView
import io.heckel.ntfy.R
import io.heckel.ntfy.data.ConnectionState
import io.heckel.ntfy.data.Subscription
-import io.heckel.ntfy.data.topicShortUrl
+import io.heckel.ntfy.util.topicShortUrl
import java.text.DateFormat
import java.util.*
-
class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLongClick: (Subscription) -> Unit) :
ListAdapter(TopicDiffCallback) {
val selected = mutableSetOf() // Subscription IDs
diff --git a/app/src/main/java/io/heckel/ntfy/ui/Util.kt b/app/src/main/java/io/heckel/ntfy/ui/Util.kt
deleted file mode 100644
index 0834354..0000000
--- a/app/src/main/java/io/heckel/ntfy/ui/Util.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.heckel.ntfy.ui
-
-import android.animation.ArgbEvaluator
-import android.animation.ValueAnimator
-import android.view.Window
-import java.text.DateFormat
-import java.util.*
-
-// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
-fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
- val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
- statusBarColorAnimation.addUpdateListener { animator ->
- val color = animator.animatedValue as Int
- window.statusBarColor = color
- }
- statusBarColorAnimation.start()
-}
-
-fun formatDateShort(timestampSecs: Long): String {
- val mutedUntilDate = Date(timestampSecs*1000)
- return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate)
-}
diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt
new file mode 100644
index 0000000..3b72167
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt
@@ -0,0 +1,95 @@
+package io.heckel.ntfy.util
+
+import android.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.view.Window
+import com.vdurmont.emoji.EmojiManager
+import io.heckel.ntfy.data.Notification
+import io.heckel.ntfy.data.Subscription
+import java.text.DateFormat
+import java.util.*
+
+fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
+fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
+fun topicUrlJsonPoll(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/json?poll=1"
+fun topicShortUrl(baseUrl: String, topic: String) =
+ topicUrl(baseUrl, topic)
+ .replace("http://", "")
+ .replace("https://", "")
+
+fun formatDateShort(timestampSecs: Long): String {
+ val mutedUntilDate = Date(timestampSecs*1000)
+ return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate)
+}
+
+fun toPriority(priority: Int?): Int {
+ if (priority != null && (1..5).contains(priority)) return priority
+ else return 3
+}
+
+fun joinTags(tags: List?): String {
+ return tags?.joinToString(",") ?: ""
+}
+
+fun toTags(tags: String?): String {
+ return tags ?: ""
+}
+
+fun emojify(tags: List): List {
+ return tags.mapNotNull {
+ when (it.toLowerCase()) {
+ "warn", "warning" -> "\u26A0\uFE0F"
+ "success" -> "\u2714\uFE0F"
+ "failure" -> "\u274C"
+ else -> EmojiManager.getForAlias(it)?.unicode
+ }
+ }
+}
+
+/**
+ * Prepend tags/emojis to message, but only if there is a non-empty title.
+ * Otherwise the tags will be prepended to the title.
+ */
+fun formatMessage(notification: Notification): String {
+ return if (notification.title != "") {
+ notification.message
+ } else {
+ val emojis = emojify(notification.tags.split(","))
+ if (emojis.isEmpty()) {
+ notification.message
+ } else {
+ emojis.joinToString("") + " " + notification.message
+ }
+ }
+}
+
+/**
+ * See above; prepend emojis to title if the title is non-empty.
+ * Otherwise, they are prepended to the message.
+ */
+fun formatTitle(subscription: Subscription, notification: Notification): String {
+ return if (notification.title != "") {
+ formatTitle(notification)
+ } else {
+ topicShortUrl(subscription.baseUrl, subscription.topic)
+ }
+}
+
+fun formatTitle(notification: Notification): String {
+ val emojis = emojify(notification.tags.split(","))
+ return if (emojis.isEmpty()) {
+ notification.title
+ } else {
+ emojis.joinToString("") + " " + notification.title
+ }
+}
+
+// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
+fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
+ val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
+ statusBarColorAnimation.addUpdateListener { animator ->
+ val color = animator.animatedValue as Int
+ window.statusBarColor = color
+ }
+ statusBarColorAnimation.start()
+}
diff --git a/app/src/main/res/drawable/ic_priority_1_24dp.xml b/app/src/main/res/drawable/ic_priority_1_24dp.xml
new file mode 100644
index 0000000..58ff930
--- /dev/null
+++ b/app/src/main/res/drawable/ic_priority_1_24dp.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_priority_2_24dp.xml b/app/src/main/res/drawable/ic_priority_2_24dp.xml
new file mode 100644
index 0000000..129c30f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_priority_2_24dp.xml
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_priority_4_24dp.xml b/app/src/main/res/drawable/ic_priority_4_24dp.xml
new file mode 100644
index 0000000..671ad67
--- /dev/null
+++ b/app/src/main/res/drawable/ic_priority_4_24dp.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_priority_5_24dp.xml b/app/src/main/res/drawable/ic_priority_5_24dp.xml
new file mode 100644
index 0000000..9eaf7da
--- /dev/null
+++ b/app/src/main/res/drawable/ic_priority_5_24dp.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_detail_item.xml b/app/src/main/res/layout/fragment_detail_item.xml
index 1afb5b3..591540c 100644
--- a/app/src/main/res/layout/fragment_detail_item.xml
+++ b/app/src/main/res/layout/fragment_detail_item.xml
@@ -6,34 +6,58 @@
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal" android:clickable="true"
- android:focusable="true" android:paddingBottom="10dp"
- android:paddingTop="10dp" android:paddingStart="16dp"
- android:paddingEnd="10dp">
+ android:focusable="true"
+>
+
+ android:textAppearance="@style/TextAppearance.AppCompat.Small"
+ app:layout_constraintTop_toTopOf="parent"
+ android:layout_marginTop="10dp" app:layout_constraintStart_toStartOf="parent"
+ android:layout_marginStart="10dp"/>
+ app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginBottom="10dp"
+ app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
+ app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"/>
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 2f1e5cb..07315be 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -9,5 +9,8 @@
#EEEEEE
#C30000
+
+ #C30000
+ #E10000
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 565662b..6b1cdc0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,7 +6,11 @@
ntfy.sh
- Notifications
+ Notifications (Min Priority)
+ Notifications (Low Priority)
+ Notifications (Default Priority)
+ Notifications (High Priority)
+ Notifications (Max Priority)
Subscription Service
Listening for incoming notifications
You are subscribed to instant delivery topics
@@ -88,7 +92,8 @@
Permanently delete
Cancel
- This is a test notification from the Ntfy Android app. It was sent at %1$s.
+ Test: You can set a title if you like
+ This is a test notification from the Ntfy Android app. It has a priority of %1$d. If you send another one, it may look different.
Could not send test message: %1$s
Copied to clipboard
Instant delivery enabled
diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
index 5d8aff6..66876aa 100644
--- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
+++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
@@ -7,6 +7,9 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.msg.NotificationService
+import io.heckel.ntfy.util.joinTags
+import io.heckel.ntfy.util.toPriority
+import io.heckel.ntfy.util.toTags
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@@ -29,7 +32,10 @@ class FirebaseService : FirebaseMessagingService() {
val id = data["id"]
val timestamp = data["time"]?.toLongOrNull()
val topic = data["topic"]
+ val title = data["title"]
val message = data["message"]
+ val priority = data["priority"]?.toIntOrNull()
+ val tags = data["tags"]
if (id == null || topic == null || message == null || timestamp == null) {
Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, data=${data}")
return
@@ -45,8 +51,11 @@ class FirebaseService : FirebaseMessagingService() {
id = id,
subscriptionId = subscription.id,
timestamp = timestamp,
+ title = title ?: "",
message = message,
notificationId = Random.nextInt(),
+ priority = toPriority(priority),
+ tags = toTags(tags),
deleted = false
)
val shouldNotify = repository.addNotification(notification)
diff --git a/assets/arrow_drop_down_black_24dp.svg b/assets/arrow_drop_down_black_24dp.svg
new file mode 100644
index 0000000..63ee544
--- /dev/null
+++ b/assets/arrow_drop_down_black_24dp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/priority_1_24dp.svg b/assets/priority_1_24dp.svg
new file mode 100644
index 0000000..df6a0a4
--- /dev/null
+++ b/assets/priority_1_24dp.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/assets/priority_2_24dp.svg b/assets/priority_2_24dp.svg
new file mode 100644
index 0000000..10a89ad
--- /dev/null
+++ b/assets/priority_2_24dp.svg
@@ -0,0 +1,43 @@
+
+
diff --git a/assets/priority_4_24dp.svg b/assets/priority_4_24dp.svg
new file mode 100644
index 0000000..a1723cf
--- /dev/null
+++ b/assets/priority_4_24dp.svg
@@ -0,0 +1,43 @@
+
+
diff --git a/assets/priority_4_alt_24dp.svg b/assets/priority_4_alt_24dp.svg
new file mode 100644
index 0000000..1dc3831
--- /dev/null
+++ b/assets/priority_4_alt_24dp.svg
@@ -0,0 +1,39 @@
+
+
diff --git a/assets/priority_5_24dp.svg b/assets/priority_5_24dp.svg
new file mode 100644
index 0000000..71bc0e2
--- /dev/null
+++ b/assets/priority_5_24dp.svg
@@ -0,0 +1,39 @@
+
+
diff --git a/assets/priority_5_alt2_24dp.svg b/assets/priority_5_alt2_24dp.svg
new file mode 100644
index 0000000..2e2c444
--- /dev/null
+++ b/assets/priority_5_alt2_24dp.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/assets/priority_5_alt_24dp.svg b/assets/priority_5_alt_24dp.svg
new file mode 100644
index 0000000..d7ae1b7
--- /dev/null
+++ b/assets/priority_5_alt_24dp.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/assets/priority_high_black_24dp.svg b/assets/priority_high_black_24dp.svg
new file mode 100644
index 0000000..86ade34
--- /dev/null
+++ b/assets/priority_high_black_24dp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/priority_high_circle_red_24dp.svg b/assets/priority_high_circle_red_24dp.svg
new file mode 100644
index 0000000..6b9c943
--- /dev/null
+++ b/assets/priority_high_circle_red_24dp.svg
@@ -0,0 +1,44 @@
+
+
diff --git a/assets/report_black_24dp.svg b/assets/report_black_24dp.svg
new file mode 100644
index 0000000..19bd916
--- /dev/null
+++ b/assets/report_black_24dp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/warning_black_24dp.svg b/assets/warning_black_24dp.svg
new file mode 100644
index 0000000..22cf31b
--- /dev/null
+++ b/assets/warning_black_24dp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file