Priorities, titles and tags

This commit is contained in:
Philipp Heckel 2021-11-27 16:18:09 -05:00
parent 3556ffda8f
commit 8fd7bb5639
36 changed files with 890 additions and 78 deletions

View file

@ -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'
}

View file

@ -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')"
]
}
}

View file

@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:name=".app.Application"

View file

@ -5,7 +5,6 @@ import androidx.room.*
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.flow.Flow
import java.util.*
@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true)])
data class Subscription(
@ -38,17 +37,20 @@ data class SubscriptionWithMetadata(
val lastActive: Long
)
@Entity
@Entity(primaryKeys = ["id", "subscriptionId"])
data class Notification(
@PrimaryKey val id: String, // TODO make [id, subscriptionId] the primary key
@ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "subscriptionId") val subscriptionId: Long,
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "message") val message: String,
@ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID
@ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max
@ColumnInfo(name = "tags") val tags: String,
@ColumnInfo(name = "deleted") val deleted: Boolean,
)
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 3)
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 4)
abstract class Database : RoomDatabase() {
abstract fun subscriptionDao(): SubscriptionDao
abstract fun notificationDao(): NotificationDao
@ -63,6 +65,7 @@ abstract class Database : RoomDatabase() {
.databaseBuilder(context.applicationContext, Database::class.java,"AppDatabase")
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4)
.fallbackToDestructiveMigration()
.build()
this.instance = instance
@ -90,6 +93,15 @@ abstract class Database : RoomDatabase() {
db.execSQL("ALTER TABLE Subscription ADD COLUMN mutedUntil INTEGER NOT NULL DEFAULT('0')")
}
}
private val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE Notification_New (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, tags TEXT NOT NULL, deleted INTEGER NOT NULL, PRIMARY KEY(id, subscriptionId))")
db.execSQL("INSERT INTO Notification_New SELECT id, subscriptionId, timestamp, '', message, notificationId, 3, '', deleted FROM Notification")
db.execSQL("DROP TABLE Notification")
db.execSQL("ALTER TABLE Notification_New RENAME TO Notification")
}
}
}
}

View file

@ -1,10 +0,0 @@
package io.heckel.ntfy.data
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://", "")

View file

@ -4,9 +4,11 @@ import android.util.Log
import androidx.annotation.Keep
import com.google.gson.Gson
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.topicUrl
import io.heckel.ntfy.data.topicUrlJson
import io.heckel.ntfy.data.topicUrlJsonPoll
import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.util.topicUrlJson
import io.heckel.ntfy.util.topicUrlJsonPoll
import io.heckel.ntfy.util.toPriority
import io.heckel.ntfy.util.joinTags
import okhttp3.*
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
@ -26,12 +28,21 @@ class ApiService {
.readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this
.build()
fun publish(baseUrl: String, topic: String, message: String) {
fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>) {
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<String>?,
val title: String?,
val message: String
)

View file

@ -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"
}
}

View file

@ -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

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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<String>, 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))
}
}
}
}

View file

@ -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

View file

@ -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<Subscription, MainAdapter.SubscriptionViewHolder>(TopicDiffCallback) {
val selected = mutableSetOf<Long>() // Subscription IDs

View file

@ -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)
}

View file

@ -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>?): String {
return tags?.joinToString(",") ?: ""
}
fun toTags(tags: String?): String {
return tags ?: ""
}
fun emojify(tags: List<String>): List<String> {
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()
}

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="m12.195,20.8283a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277l-5.9848,3.6353 -5.9848,-3.6353a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#999999"
android:strokeLineCap="round"/>
<path
android:pathData="m12.195,15.694a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372A1.2746,1.2746 0,0 0,19.9307 9.7205,1.2746 1.2746,0 0,0 18.1798,9.2928L12.195,12.9281 6.2102,9.2928a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#b3b3b3"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.1168,10.4268a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277l-5.9848,3.6353 -5.9848,-3.6353a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511L11.455,10.2416a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#cccccc"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="m12.1727,17.7744a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277L12.1727,15.0085 6.1879,11.3731a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#999999"
android:strokeLineCap="round"/>
<path
android:pathData="m12.1727,12.64a1.2747,1.2747 0,0 0,0.6616 -0.1852L19.4809,8.4177A1.2746,1.2746 0,0 0,19.9084 6.6666,1.2746 1.2746,0 0,0 18.1575,6.2388L12.1727,9.8742 6.1879,6.2388a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#b3b3b3"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.1168,6.5394A1.2747,1.2747 0,0 0,11.4552 6.7246l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353A1.2746,1.2746 0,0 0,19.8525 12.5129,1.2746 1.2746,0 0,0 19.425,10.7618L12.7786,6.7246A1.2747,1.2747 0,0 0,12.1168 6.5394Z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#c60000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.195,11.8067a1.2747,1.2747 0,0 0,-0.6616 0.1852l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353a1.2746,1.2746 0,0 0,1.7509 -0.4277,1.2746 1.2746,0 0,0 -0.4275,-1.7511l-6.6464,-4.0372a1.2747,1.2747 0,0 0,-0.6618 -0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#de0000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.1168,3.4051A1.2747,1.2747 0,0 0,11.4552 3.5903L4.8086,7.6275A1.2746,1.2746 0,0 0,4.381 9.3786,1.2746 1.2746,0 0,0 6.132,9.8063L12.1168,6.171 18.1016,9.8063A1.2746,1.2746 0,0 0,19.8525 9.3786,1.2746 1.2746,0 0,0 19.425,7.6275L12.7786,3.5903A1.2747,1.2747 0,0 0,12.1168 3.4051Z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#aa0000"
android:strokeLineCap="round"/>
<path
android:pathData="M12.1168,8.5394A1.2747,1.2747 0,0 0,11.4552 8.7246l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353A1.2746,1.2746 0,0 0,19.8525 14.5129,1.2746 1.2746,0 0,0 19.425,12.7618L12.7786,8.7246A1.2747,1.2747 0,0 0,12.1168 8.5394Z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#c60000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.195,13.8067a1.2747,1.2747 0,0 0,-0.6616 0.1852l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353a1.2746,1.2746 0,0 0,1.7509 -0.4277,1.2746 1.2746,0 0,0 -0.4275,-1.7511l-6.6464,-4.0372a1.2747,1.2747 0,0 0,-0.6618 -0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#de0000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -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"
>
<TextView
android:text="Sun, October 31, 2021, 10:43:12"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/detail_item_date_text"
android:textAppearance="@style/TextAppearance.AppCompat.Small" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="10dp" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="10dp"/>
<TextView
android:layout_width="10dp"
android:layout_height="10dp" android:id="@+id/detail_item_new"
android:layout_height="10dp" android:id="@+id/detail_item_new_dot"
android:layout_gravity="center"
android:background="@drawable/ic_circle"
android:gravity="center"
app:layout_constraintTop_toTopOf="@+id/detail_item_date_text"
app:layout_constraintBottom_toBottomOf="@+id/detail_item_date_text"
android:layout_marginTop="1dp" app:layout_constraintStart_toEndOf="@+id/detail_item_date_text"
android:layout_marginTop="1dp"
app:layout_constraintStart_toEndOf="@id/detail_item_priority_image"
android:layout_marginStart="5dp"/>
<TextView
android:text="This is a very very very long message. It could be as long as 1024 charaters, which is a lot more than you'd think. No, really so far this message is barely 180 characters long. I can't believe how long 1024 bytes are. This is outrageous. Oh you know what, I think I won't type the whole thing. This seems a little too long for a sample text. Well, anyway, it was nice chatting. So far this message is about 400 bytes long. So maybe just double what you see and that's that."
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/detail_item_message_text"
android:textColor="@color/primaryTextColor"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintTop_toBottomOf="@+id/detail_item_date_text"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>
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"/>
<TextView
android:text="This is an optional title. It can also be a little longer but not too long."
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/detail_item_title_text"
android:textColor="@color/primaryTextColor"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="10dp" android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@+id/detail_item_date_text"/>
<ImageView
android:layout_width="16dp"
android:layout_height="16dp" app:srcCompat="@drawable/ic_priority_5_24dp"
android:id="@+id/detail_item_priority_image"
app:layout_constraintStart_toEndOf="@+id/detail_item_date_text"
app:layout_constraintTop_toTopOf="@+id/detail_item_date_text"
app:layout_constraintBottom_toBottomOf="@+id/detail_item_date_text" android:layout_marginStart="5dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -9,5 +9,8 @@
<color name="primarySelectedRowColor">#EEEEEE</color>
<color name="primaryDangerButtonColor">#C30000</color>
<color name="primaryPriorityUrgentColor">#C30000</color>
<color name="primaryPriorityHighColor">#E10000</color>
</resources>

View file

@ -6,7 +6,11 @@
<string name="app_base_host">ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
<!-- Notification channels -->
<string name="channel_notifications_name">Notifications</string>
<string name="channel_notifications_min_name">Notifications (Min Priority)</string>
<string name="channel_notifications_low_name">Notifications (Low Priority)</string>
<string name="channel_notifications_default_name">Notifications (Default Priority)</string>
<string name="channel_notifications_high_name">Notifications (High Priority)</string>
<string name="channel_notifications_max_name">Notifications (Max Priority)</string>
<string name="channel_subscriber_service_name">Subscription Service</string>
<string name="channel_subscriber_notification_title">Listening for incoming notifications</string>
<string name="channel_subscriber_notification_text">You are subscribed to instant delivery topics</string>
@ -88,7 +92,8 @@
</string>
<string name="detail_delete_dialog_permanently_delete">Permanently delete</string>
<string name="detail_delete_dialog_cancel">Cancel</string>
<string name="detail_test_message">This is a test notification from the Ntfy Android app. It was sent at %1$s.</string>
<string name="detail_test_title">Test: You can set a title if you like</string>
<string name="detail_test_message">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.</string>
<string name="detail_test_message_error">Could not send test message: %1$s</string>
<string name="detail_copied_to_clipboard_message">Copied to clipboard</string>
<string name="detail_instant_delivery_enabled">Instant delivery enabled</string>

View file

@ -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)

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 10l5 5 5-5z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_1_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834324"
inkscape:cy="15.742768"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,20.828316 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984807,3.635327 -5.9848086,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464146,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="rect3554" />
<path
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,15.694014 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037176 A 1.2745823,1.2745823 0 0 0 19.930749,9.7205243 1.2745823,1.2745823 0 0 0 18.179821,9.2928073 L 12.195014,12.928134 6.2102054,9.2928073 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464146,4.037176 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="path9314" />
<path
style="color:#000000;fill:#cccccc;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.116784,10.426777 a 1.2747098,1.2747098 0 0 0 0.661606,-0.185205 l 6.646593,-4.0371767 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751108 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984808,3.635327 -5.9848066,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750928,0.427718 1.2745823,1.2745823 0 0 0 0.427537,1.751108 L 11.455,10.241572 a 1.2747098,1.2747098 0 0 0 0.661784,0.185205 z"
id="path9316" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_2_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834324"
inkscape:cy="15.742768"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.172712,17.774352 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 L 12.172712,15.00847 6.1879033,11.373143 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464147,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="rect3554" />
<path
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.172712,12.64005 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 L 19.48091,8.4176679 A 1.2745823,1.2745823 0 0 0 19.908447,6.6665602 1.2745823,1.2745823 0 0 0 18.157519,6.2388432 L 12.172712,9.8741699 6.1879033,6.2388432 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464147,4.0371761 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="path9314" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_4_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834324"
inkscape:cy="15.742768"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.116784,6.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,6.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,12.512932 1.2745823,1.2745823 0 0 0 19.424984,10.761824 L 12.778569,6.724648 A 1.2747098,1.2747098 0 0 0 12.116784,6.5394415 Z"
id="path9314" />
<path
style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,11.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
id="path9316" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_4_alt_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="19.300483"
inkscape:cx="8.1604177"
inkscape:cy="7.4609534"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#aa0000;fill-opacity:1;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.073041,8.4104485 A 1.2171741,1.2171741 0 0 0 11.441298,8.5872953 L 5.0947071,12.442249 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.672069 1.2170524,1.2170524 0 0 0 1.6718977,0.408412 l 5.7146758,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408412 1.2170524,1.2170524 0 0 0 -0.408239,-1.672069 L 12.704955,8.5872953 A 1.2171741,1.2171741 0 0 0 12.073041,8.4104485 Z"
id="rect3554" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_5_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="26.806921"
inkscape:cx="18.894374"
inkscape:cy="14.026229"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
d="M 16.748063,0.46162067 H 7.1927317 L 0.44251841,7.211834 v 9.555331 l 6.75021329,6.750213 h 9.5553313 l 6.750213,-6.750213 V 7.211834 Z M 11.970397,18.778139 c -0.922231,0 -1.665138,-0.742908 -1.665138,-1.665138 0,-0.92223 0.742907,-1.665138 1.665138,-1.665138 0.92223,0 1.665138,0.742908 1.665138,1.665138 0,0.92223 -0.742908,1.665138 -1.665138,1.665138 z m 1.280875,-5.507765 H 10.689521 V 5.5851222 h 2.561751 z"
id="path29312"
style="fill:#ac0000;fill-opacity:1;stroke-width:1.28088" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_5_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834323"
inkscape:cy="15.742767"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#aa0000;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.116784,3.40514 A 1.2747098,1.2747098 0 0 0 11.455179,3.5903463 L 4.8085864,7.6275238 A 1.2745823,1.2745823 0 0 0 4.3810494,9.3786313 1.2745823,1.2745823 0 0 0 6.1319775,9.8063489 L 12.116784,6.1710217 18.101593,9.8063489 A 1.2745823,1.2745823 0 0 0 19.85252,9.3786313 1.2745823,1.2745823 0 0 0 19.424984,7.6275238 L 12.778569,3.5903463 A 1.2747098,1.2747098 0 0 0 12.116784,3.40514 Z"
id="rect3554" />
<path
style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.116784,8.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,8.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,14.512932 1.2745823,1.2745823 0 0 0 19.424984,12.761824 L 12.778569,8.724648 A 1.2747098,1.2747098 0 0 0 12.116784,8.5394415 Z"
id="path9314" />
<path
style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,13.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
id="path9316" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_5_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="19.300483"
inkscape:cx="8.1604177"
inkscape:cy="7.4609534"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#ff5630;fill-opacity:1;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.368788,3.4216973 A 1.2171741,1.2171741 0 0 0 11.737045,3.5985441 L 5.3904546,7.4534983 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.672069 1.2170524,1.2170524 0 0 0 1.6718977,0.408412 l 5.7146753,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408412 1.2170524,1.2170524 0 0 0 -0.408239,-1.672069 L 13.000702,3.5985441 A 1.2171741,1.2171741 0 0 0 12.368788,3.4216973 Z"
id="rect3554" />
<path
style="color:#000000;fill:#ff7452;fill-opacity:1;stroke:none;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.368788,8.6910833 a 1.2171741,1.2171741 0 0 0 -0.631743,0.176847 L 5.3904546,12.722883 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.67207 1.2170524,1.2170524 0 0 0 1.6718977,0.408411 l 5.7146753,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408411 1.2170524,1.2170524 0 0 0 -0.408239,-1.67207 l -6.34642,-3.8549527 a 1.2171741,1.2171741 0 0 0 -0.631914,-0.176847 z"
id="path9314" />
<path
style="color:#000000;fill:#ff8f73;fill-opacity:1;stroke:none;stroke-width:0.0878234;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.368788,14.136115 a 1.2171741,1.2171741 0 0 0 -0.631743,0.176846 l -6.3465904,3.854954 a 1.2170524,1.2170524 0 0 0 -0.4082396,1.672069 1.2170524,1.2170524 0 0 0 1.6718977,0.408412 l 5.7146753,-3.471242 5.714676,3.471242 a 1.2170524,1.2170524 0 0 0 1.671897,-0.408412 1.2170524,1.2170524 0 0 0 -0.408239,-1.672069 l -6.34642,-3.854954 a 1.2171741,1.2171741 0 0 0 -0.631914,-0.176846 z"
id="path9316" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><circle cx="12" cy="19" r="2"/><path d="M10 3h4v12h-4z"/></svg>

After

Width:  |  Height:  |  Size: 204 B

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg874"
sodipodi:docname="priority_high_circle_red_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs878" />
<sodipodi:namedview
id="namedview876"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="24.506368"
inkscape:cx="13.894348"
inkscape:cy="9.8749843"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg874" />
<path
id="path25891"
style="fill:#800000;stroke-width:34.5068;stroke-linecap:round;stroke-linejoin:round"
d="M 11.838253,0.22539283 A 11.693702,11.693702 0 0 0 0.14541272,11.918233 11.693702,11.693702 0 0 0 11.838253,23.612994 11.693702,11.693702 0 0 0 23.533014,11.918233 11.693702,11.693702 0 0 0 11.838253,0.22539283 Z M 11.918883,1.530783 A 10.412385,10.412385 0 0 1 22.331287,11.945109 10.412385,10.412385 0 0 1 11.918881,22.357515 10.412385,10.412385 0 0 1 1.5045542,11.945109 10.412385,10.412385 0 0 1 11.918881,1.530783 Z M 10.095174,3.8612884 V 14.592364 h 3.576385 V 3.8612884 Z m 1.789152,12.5202276 a 1.7886362,1.7886362 0 0 0 -1.789152,1.789152 1.7886362,1.7886362 0 0 0 1.789152,1.787233 1.7886362,1.7886362 0 0 0 1.787233,-1.787233 1.7886362,1.7886362 0 0 0 -1.787233,-1.789152 z" />
<path
d="M 0.14541272,0.22539283 H 23.734581 V 23.814561 H 0.14541272 Z"
fill="none"
id="path868"
style="stroke-width:0.982882" />
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17.3c-.72 0-1.3-.58-1.3-1.3 0-.72.58-1.3 1.3-1.3.72 0 1.3.58 1.3 1.3 0 .72-.58 1.3-1.3 1.3zm1-4.3h-2V7h2v6z"/></svg>

After

Width:  |  Height:  |  Size: 329 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>

After

Width:  |  Height:  |  Size: 207 B