custom display names

fixes binwiederhier/ntfy#313
fixes binwiederhier/ntfy#291 (at least the android portion)
This commit is contained in:
Hunter Kehoe 2022-06-24 09:15:22 -06:00
parent 6333a063a1
commit bbc7549d7a
14 changed files with 95 additions and 22 deletions

View file

@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "b439720b55cf5e6bfdec2b56dd46103d",
"identityHash": "9363ad5196e88862acceb1bb9ee91124",
"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, `minPriority` INTEGER NOT NULL, `autoDelete` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, PRIMARY KEY(`id`))",
"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, `minPriority` INTEGER NOT NULL, `autoDelete` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, `displayName` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
@ -73,6 +73,12 @@
"columnName": "upConnectorToken",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
@ -320,7 +326,7 @@
"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, 'b439720b55cf5e6bfdec2b56dd46103d')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9363ad5196e88862acceb1bb9ee91124')"
]
}
}

View file

@ -100,7 +100,8 @@ class Backuper(val context: Context) {
lastNotificationId = s.lastNotificationId,
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken
upConnectorToken = s.upConnectorToken,
displayName = s.displayName,
))
} catch (e: Exception) {
Log.w(TAG, "Unable to restore subscription ${s.id} (${topicUrl(s.baseUrl, s.topic)}): ${e.message}. Ignoring.", e)
@ -224,7 +225,8 @@ class Backuper(val context: Context) {
lastNotificationId = s.lastNotificationId,
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken
upConnectorToken = s.upConnectorToken,
displayName = s.displayName
)
}
}
@ -331,7 +333,8 @@ data class Subscription(
val lastNotificationId: String?,
val icon: String?,
val upAppId: String?,
val upConnectorToken: String?
val upConnectorToken: String?,
val displayName: String?
)
data class Notification(

View file

@ -22,13 +22,14 @@ data class Subscription(
@ColumnInfo(name = "icon") val icon: String?, // content://-URI (or later other identifier)
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token
@ColumnInfo(name = "displayName") val displayName: String?,
@Ignore val totalCount: Int = 0, // Total notifications
@Ignore val newCount: Int = 0, // New notifications
@Ignore val lastActive: Long = 0, // Unix timestamp
@Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE
) {
constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long, minPriority: Int, autoDelete: Long, lastNotificationId: String, icon: String, upAppId: String, upConnectorToken: String) :
this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, lastNotificationId, icon, upAppId, upConnectorToken, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long, minPriority: Int, autoDelete: Long, lastNotificationId: String, icon: String, upAppId: String, upConnectorToken: String, displayName: String?) :
this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, lastNotificationId, icon, upAppId, upConnectorToken, displayName, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
}
enum class ConnectionState {
@ -47,6 +48,7 @@ data class SubscriptionWithMetadata(
val icon: String?,
val upAppId: String?,
val upConnectorToken: String?,
val displayName: String?,
val totalCount: Int,
val newCount: Int,
val lastActive: Long
@ -266,6 +268,7 @@ abstract class Database : RoomDatabase() {
private val MIGRATION_11_12 = object : Migration(11, 12) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE Subscription ADD COLUMN lastNotificationId TEXT")
db.execSQL("ALTER TABLE Subscription ADD COLUMN displayName TEXT")
}
}
}
@ -275,7 +278,7 @@ abstract class Database : RoomDatabase() {
interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -288,7 +291,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -301,7 +304,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -314,7 +317,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@ -327,7 +330,7 @@ interface SubscriptionDao {
@Query("""
SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken,
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive

View file

@ -384,6 +384,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken,
displayName = s.displayName,
totalCount = s.totalCount,
newCount = s.newCount,
lastActive = s.lastActive,
@ -408,6 +409,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
icon = s.icon,
upAppId = s.upAppId,
upConnectorToken = s.upConnectorToken,
displayName = s.displayName,
totalCount = s.totalCount,
newCount = s.newCount,
lastActive = s.lastActive,

View file

@ -300,6 +300,7 @@ class NotificationService(val context: Context) {
putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
putExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(subscription))
putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
}

View file

@ -54,6 +54,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
private var subscriptionId: Long = 0L // Set in onCreate()
private var subscriptionBaseUrl: String = "" // Set in onCreate()
private var subscriptionTopic: String = "" // Set in onCreate()
private var subscriptionDisplayName: String = "" // Set in onCreate() & updated by options menu!
private var subscriptionInstant: Boolean = false // Set in onCreate() & updated by options menu!
private var subscriptionMutedUntil: Long = 0L // Set in onCreate() & updated by options menu!
@ -97,7 +98,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
val secure = url.getBooleanQueryParameter("secure", true)
val baseUrl = if (secure) "https://${url.host}" else "http://${url.host}"
val topic = url.pathSegments.first()
title = topicShortUrl(baseUrl, topic)
// Subscribe to topic if it doesn't already exist
lifecycleScope.launch(Dispatchers.IO) {
@ -116,6 +116,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
icon = null,
upAppId = null,
upConnectorToken = null,
displayName = null,
totalCount = 0,
newCount = 0,
lastActive = Date().time/1000
@ -143,10 +144,13 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
}
}
title = displayName(subscription)
// Add extras needed in loadView(); normally these are added in MainActivity
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(subscription))
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
@ -161,13 +165,14 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
subscriptionId = intent.getLongExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, 0)
subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return
subscriptionTopic = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC) ?: return
subscriptionDisplayName = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME) ?: return
subscriptionInstant = intent.getBooleanExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, false)
subscriptionMutedUntil = intent.getLongExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, 0L)
// Set title
val subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return
val topicUrl = topicShortUrl(subscriptionBaseUrl, subscriptionTopic)
title = topicUrl
title = subscriptionDisplayName
// Set "how to instructions"
val howToExample: TextView = findViewById(R.id.detail_how_to_example)
@ -263,9 +268,11 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
subscriptionInstant = subscription.instant
subscriptionMutedUntil = subscription.mutedUntil
subscriptionDisplayName = displayName(subscription)
showHideInstantMenuItems(subscriptionInstant)
showHideMutedUntilMenuItems(subscriptionMutedUntil)
updateTitle(subscriptionDisplayName)
}
}
@ -543,6 +550,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
}
}
private fun updateTitle(subscriptionDisplayName: String) {
runOnUiThread {
title = subscriptionDisplayName
}
}
private fun onClearClick() {
Log.d(TAG, "Clearing all notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
@ -571,6 +584,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
intent.putExtra(EXTRA_SUBSCRIPTION_ID, subscriptionId)
intent.putExtra(EXTRA_SUBSCRIPTION_BASE_URL, subscriptionBaseUrl)
intent.putExtra(EXTRA_SUBSCRIPTION_TOPIC, subscriptionTopic)
intent.putExtra(EXTRA_SUBSCRIPTION_DISPLAY_NAME, subscriptionDisplayName)
startActivity(intent)
}
@ -747,5 +761,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
const val EXTRA_SUBSCRIPTION_ID = "subscriptionId"
const val EXTRA_SUBSCRIPTION_BASE_URL = "baseUrl"
const val EXTRA_SUBSCRIPTION_TOPIC = "topic"
const val EXTRA_SUBSCRIPTION_DISPLAY_NAME = "displayName"
}
}

View file

@ -55,9 +55,8 @@ class DetailSettingsActivity : AppCompatActivity() {
}
// Title
val baseUrl = intent.getStringExtra(DetailActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return
val topic = intent.getStringExtra(DetailActivity.EXTRA_SUBSCRIPTION_TOPIC) ?: return
title = topicShortUrl(baseUrl, topic)
val displayName = intent.getStringExtra(DetailActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME) ?: return
title = displayName
// Show 'Back' button
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@ -108,6 +107,7 @@ class DetailSettingsActivity : AppCompatActivity() {
loadAutoDeletePref()
loadIconSetPref()
loadIconRemovePref()
loadDisplayNamePref()
}
private fun loadInstantPref() {
@ -276,6 +276,33 @@ class DetailSettingsActivity : AppCompatActivity() {
}
}
private fun loadDisplayNamePref() {
val prefId = context?.getString(R.string.detail_settings_appearance_display_name_key) ?: return
val pref: EditTextPreference? = findPreference(prefId)
pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
pref?.text = subscription.displayName
pref?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val displayName: String? = if (value == "") {
null
} else {
value
}
val newSubscription = subscription.copy(displayName = displayName)
save(newSubscription) // TODO: does this need refresh=true?
activity?.runOnUiThread {
activity?.title = displayName(newSubscription)
}
}
override fun getString(key: String?, defValue: String?): String {
return subscription.displayName ?: ""
}
}
pref?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { _ ->
getString(R.string.detail_settings_appearance_display_name_summary, displayName(subscription), topicShortUrl(subscription.baseUrl, subscription.topic))
}
}
private fun createIconPickLauncher(): ActivityResultLauncher<String> {
return registerForActivityResult(ActivityResultContracts.GetContent()) { inputUri ->
if (inputUri == null) {

View file

@ -438,6 +438,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
icon = null,
upAppId = null,
upConnectorToken = null,
displayName = null,
totalCount = 0,
newCount = 0,
lastActive = Date().time/1000
@ -509,7 +510,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
}
}
} catch (e: Exception) {
val topic = topicShortUrl(subscription.baseUrl, subscription.topic)
val topic = displayName(subscription)
if (errorMessage == "") errorMessage = "$topic: ${e.message}"
errors++
}
@ -536,6 +537,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
intent.putExtra(EXTRA_SUBSCRIPTION_ID, subscription.id)
intent.putExtra(EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
intent.putExtra(EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
intent.putExtra(EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(subscription))
intent.putExtra(EXTRA_SUBSCRIPTION_INSTANT, subscription.instant)
intent.putExtra(EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil)
startActivity(intent)
@ -662,6 +664,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
const val EXTRA_SUBSCRIPTION_ID = "subscriptionId"
const val EXTRA_SUBSCRIPTION_BASE_URL = "subscriptionBaseUrl"
const val EXTRA_SUBSCRIPTION_TOPIC = "subscriptionTopic"
const val EXTRA_SUBSCRIPTION_DISPLAY_NAME = "subscriptionDisplayName"
const val EXTRA_SUBSCRIPTION_INSTANT = "subscriptionInstant"
const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil"
const val ANIMATION_DURATION = 80L

View file

@ -20,7 +20,7 @@ import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.readBitmapFromUriOrNull
import io.heckel.ntfy.util.topicShortUrl
import io.heckel.ntfy.util.displayName
import java.text.DateFormat
import java.util.*
@ -101,7 +101,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
if (subscription.icon != null) {
imageView.setImageBitmap(subscription.icon.readBitmapFromUriOrNull(context))
}
nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
nameView.text = displayName(subscription)
statusView.text = statusMessage
dateView.text = dateText
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE

View file

@ -79,6 +79,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
icon = null,
upAppId = appId,
upConnectorToken = connectorToken,
displayName = null,
totalCount = 0,
newCount = 0,
lastActive = Date().time/1000

View file

@ -53,6 +53,10 @@ fun topicUrlAuth(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/
fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?poll=1&since=$since"
fun topicShortUrl(baseUrl: String, topic: String) = shortUrl(topicUrl(baseUrl, topic))
fun displayName(subscription: Subscription) : String {
return subscription.displayName ?: topicShortUrl(subscription.baseUrl, subscription.topic)
}
fun shortUrl(url: String) = url
.replace("http://", "")
.replace("https://", "")
@ -176,7 +180,7 @@ fun formatTitle(subscription: Subscription, notification: Notification): String
return if (notification.title != "") {
formatTitle(notification)
} else {
topicShortUrl(subscription.baseUrl, subscription.topic)
displayName(subscription)
}
}

View file

@ -353,6 +353,8 @@
<string name="detail_settings_appearance_icon_remove_title">Subscription icon (tap to remove)</string>
<string name="detail_settings_appearance_icon_remove_summary">Icon displayed in notifications for this topic</string>
<string name="detail_settings_appearance_icon_error_saving">Unable to save icon: %1$s</string>
<string name="detail_settings_appearance_display_name_title">Display name</string>
<string name="detail_settings_appearance_display_name_summary">Set a custom display name for this subscription. Leave empty for default\nCurrent: %1$s\nDefault: %2$s</string>
<string name="detail_settings_global_setting_title">Use global setting</string>
<string name="detail_settings_global_setting_suffix">using global setting</string>

View file

@ -36,6 +36,7 @@
<string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</string>
<string name="detail_settings_appearance_icon_set_key" translatable="false">SubscriptionIconSet</string>
<string name="detail_settings_appearance_icon_remove_key" translatable="false">SubscriptionIconRemove</string>
<string name="detail_settings_appearance_display_name_key" translatable="false">SubscriptionDisplayName</string>
<!-- Main settings -->
<string-array name="settings_notifications_muted_until_entries">

View file

@ -38,5 +38,10 @@
app:title="@string/detail_settings_appearance_icon_remove_title"
app:summary="@string/detail_settings_appearance_icon_remove_summary"
app:isPreferenceVisible="false"/>
<EditTextPreference
app:key="@string/detail_settings_appearance_display_name_key"
app:title="@string/detail_settings_appearance_display_name_title"
app:summary="@string/detail_settings_appearance_display_name_summary"
app:isPreferenceVisible="false"/>
</PreferenceCategory>
</PreferenceScreen>