diff --git a/app/schemas/io.heckel.ntfy.db.Database/12.json b/app/schemas/io.heckel.ntfy.db.Database/12.json
new file mode 100644
index 0000000..9743a78
--- /dev/null
+++ b/app/schemas/io.heckel.ntfy.db.Database/12.json
@@ -0,0 +1,326 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 12,
+    "identityHash": "b439720b55cf5e6bfdec2b56dd46103d",
+    "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`))",
+        "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
+          },
+          {
+            "fieldPath": "minPriority",
+            "columnName": "minPriority",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "autoDelete",
+            "columnName": "autoDelete",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastNotificationId",
+            "columnName": "lastNotificationId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "icon",
+            "columnName": "icon",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "upAppId",
+            "columnName": "upAppId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "upConnectorToken",
+            "columnName": "upConnectorToken",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_Subscription_baseUrl_topic",
+            "unique": true,
+            "columnNames": [
+              "baseUrl",
+              "topic"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
+          },
+          {
+            "name": "index_Subscription_upConnectorToken",
+            "unique": true,
+            "columnNames": [
+              "upConnectorToken"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)"
+          }
+        ],
+        "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, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, 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": "encoding",
+            "columnName": "encoding",
+            "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": "click",
+            "columnName": "click",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "actions",
+            "columnName": "actions",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "deleted",
+            "columnName": "deleted",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "attachment.name",
+            "columnName": "attachment_name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "attachment.type",
+            "columnName": "attachment_type",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "attachment.size",
+            "columnName": "attachment_size",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "attachment.expires",
+            "columnName": "attachment_expires",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "attachment.url",
+            "columnName": "attachment_url",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "attachment.contentUri",
+            "columnName": "attachment_contentUri",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "attachment.progress",
+            "columnName": "attachment_progress",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id",
+            "subscriptionId"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "User",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
+        "fields": [
+          {
+            "fieldPath": "baseUrl",
+            "columnName": "baseUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "username",
+            "columnName": "username",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "password",
+            "columnName": "password",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "baseUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "Log",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `tag` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `exception` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "timestamp",
+            "columnName": "timestamp",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "tag",
+            "columnName": "tag",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "level",
+            "columnName": "level",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "message",
+            "columnName": "message",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "exception",
+            "columnName": "exception",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "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, 'b439720b55cf5e6bfdec2b56dd46103d')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
index d49e972..120f41e 100644
--- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
+++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
@@ -97,6 +97,7 @@ class Backuper(val context: Context) {
                     mutedUntil = s.mutedUntil,
                     minPriority = s.minPriority ?: Repository.MIN_PRIORITY_USE_GLOBAL,
                     autoDelete = s.autoDelete ?: Repository.AUTO_DELETE_USE_GLOBAL,
+                    lastNotificationId = s.lastNotificationId,
                     icon = s.icon,
                     upAppId = s.upAppId,
                     upConnectorToken = s.upConnectorToken
@@ -220,6 +221,7 @@ class Backuper(val context: Context) {
                 mutedUntil = s.mutedUntil,
                 minPriority = s.minPriority,
                 autoDelete = s.autoDelete,
+                lastNotificationId = s.lastNotificationId,
                 icon = s.icon,
                 upAppId = s.upAppId,
                 upConnectorToken = s.upConnectorToken
@@ -326,6 +328,7 @@ data class Subscription(
     val mutedUntil: Long,
     val minPriority: Int?,
     val autoDelete: Long?,
+    val lastNotificationId: String?,
     val icon: String?,
     val upAppId: String?,
     val upConnectorToken: String?
diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt
index 7d64166..eba9cb9 100644
--- a/app/src/main/java/io/heckel/ntfy/db/Database.kt
+++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt
@@ -18,6 +18,7 @@ data class Subscription(
     @ColumnInfo(name = "mutedUntil") val mutedUntil: Long,
     @ColumnInfo(name = "minPriority") val minPriority: Int,
     @ColumnInfo(name = "autoDelete") val autoDelete: Long, // Seconds
+    @ColumnInfo(name = "lastNotificationId") val lastNotificationId: String?, // Used for polling, with since=<id>
     @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
@@ -26,8 +27,8 @@ data class Subscription(
     @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, icon: String, upAppId: String, upConnectorToken: String) :
-            this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, 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) :
+            this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, lastNotificationId, icon, upAppId, upConnectorToken, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
 }
 
 enum class ConnectionState {
@@ -42,6 +43,7 @@ data class SubscriptionWithMetadata(
     val mutedUntil: Long,
     val autoDelete: Long,
     val minPriority: Int,
+    val lastNotificationId: String?,
     val icon: String?,
     val upAppId: String?,
     val upConnectorToken: String?,
@@ -144,7 +146,7 @@ data class LogEntry(
             this(0, timestamp, tag, level, message, exception)
 }
 
-@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 11)
+@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 12)
 @TypeConverters(Converters::class)
 abstract class Database : RoomDatabase() {
     abstract fun subscriptionDao(): SubscriptionDao
@@ -170,6 +172,7 @@ abstract class Database : RoomDatabase() {
                     .addMigrations(MIGRATION_8_9)
                     .addMigrations(MIGRATION_9_10)
                     .addMigrations(MIGRATION_10_11)
+                    .addMigrations(MIGRATION_11_12)
                     .fallbackToDestructiveMigration()
                     .build()
                 this.instance = instance
@@ -259,6 +262,12 @@ abstract class Database : RoomDatabase() {
                 db.execSQL("ALTER TABLE Subscription ADD COLUMN icon TEXT")
             }
         }
+
+        private val MIGRATION_11_12 = object : Migration(11, 12) {
+            override fun migrate(db: SupportSQLiteDatabase) {
+                db.execSQL("ALTER TABLE Subscription ADD COLUMN lastNotificationId TEXT")
+            }
+        }
     }
 }
 
@@ -266,7 +275,7 @@ abstract class Database : RoomDatabase() {
 interface SubscriptionDao {
     @Query("""
         SELECT 
-          s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, 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,
           COUNT(n.id) totalCount, 
           COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, 
           IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -279,7 +288,7 @@ interface SubscriptionDao {
 
     @Query("""
         SELECT 
-          s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, 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,
           COUNT(n.id) totalCount, 
           COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, 
           IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -292,7 +301,7 @@ interface SubscriptionDao {
 
     @Query("""
         SELECT 
-          s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, 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,
           COUNT(n.id) totalCount, 
           COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, 
           IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -305,7 +314,7 @@ interface SubscriptionDao {
 
     @Query("""
         SELECT 
-          s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, 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,
           COUNT(n.id) totalCount, 
           COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, 
           IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -318,7 +327,7 @@ interface SubscriptionDao {
 
     @Query("""
         SELECT 
-          s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, 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,
           COUNT(n.id) totalCount, 
           COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, 
           IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -335,6 +344,9 @@ interface SubscriptionDao {
     @Update
     fun update(subscription: Subscription)
 
+    @Query("UPDATE subscription SET lastNotificationId = :lastNotificationId WHERE id = :subscriptionId")
+    fun updateLastNotificationId(subscriptionId: Long, lastNotificationId: String)
+
     @Query("DELETE FROM subscription WHERE id = :subscriptionId")
     fun remove(subscriptionId: Long)
 }
diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
index 5786699..d35a25d 100644
--- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt
+++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
@@ -116,6 +116,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
         if (maybeExistingNotification != null) {
             return false
         }
+        subscriptionDao.updateLastNotificationId(notification.subscriptionId, notification.id)
         notificationDao.add(notification)
         return true
     }
@@ -299,13 +300,13 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
             .apply()
     }
 
-    fun getJsonStreamRemindTime(): Long {
-        return sharedPrefs.getLong(SHARED_PREFS_JSON_STREAM_REMIND_TIME, JSON_STREAM_REMIND_TIME_ALWAYS)
+    fun getWebSocketRemindTime(): Long {
+        return sharedPrefs.getLong(SHARED_PREFS_WEBSOCKET_REMIND_TIME, WEBSOCKET_REMIND_TIME_ALWAYS)
     }
 
-    fun setJsonStreamRemindTime(timeMillis: Long) {
+    fun setWebSocketRemindTime(timeMillis: Long) {
         sharedPrefs.edit()
-            .putLong(SHARED_PREFS_JSON_STREAM_REMIND_TIME, timeMillis)
+            .putLong(SHARED_PREFS_WEBSOCKET_REMIND_TIME, timeMillis)
             .apply()
     }
 
@@ -379,6 +380,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
                 mutedUntil = s.mutedUntil,
                 minPriority = s.minPriority,
                 autoDelete = s.autoDelete,
+                lastNotificationId = s.lastNotificationId,
                 icon = s.icon,
                 upAppId = s.upAppId,
                 upConnectorToken = s.upConnectorToken,
@@ -402,6 +404,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
             mutedUntil = s.mutedUntil,
             minPriority = s.minPriority,
             autoDelete = s.autoDelete,
+            lastNotificationId = s.lastNotificationId,
             icon = s.icon,
             upAppId = s.upAppId,
             upConnectorToken = s.upConnectorToken,
@@ -448,7 +451,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
         const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
         const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
         const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
-        const val SHARED_PREFS_JSON_STREAM_REMIND_TIME = "JsonStreamRemindTime" // Deprecation of JSON stream
+        const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)
         const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL
         const val SHARED_PREFS_DEFAULT_BASE_URL = "DefaultBaseURL"
         const val SHARED_PREFS_LAST_TOPICS = "LastTopics"
@@ -483,8 +486,8 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
         const val BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS = 1L
         const val BATTERY_OPTIMIZATIONS_REMIND_TIME_NEVER = Long.MAX_VALUE
 
-        const val JSON_STREAM_REMIND_TIME_ALWAYS = 1L
-        const val JSON_STREAM_REMIND_TIME_NEVER = Long.MAX_VALUE
+        const val WEBSOCKET_REMIND_TIME_ALWAYS = 1L
+        const val WEBSOCKET_REMIND_TIME_NEVER = Long.MAX_VALUE
 
         private const val TAG = "NtfyRepository"
         private var instance: Repository? = null
diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
index 0693834..64baa45 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
@@ -84,8 +84,8 @@ class ApiService {
         }
     }
 
-    fun poll(subscriptionId: Long, baseUrl: String, topic: String, user: User?, since: Long = 0L): List<Notification> {
-        val sinceVal = if (since == 0L) "all" else since.toString()
+    fun poll(subscriptionId: Long, baseUrl: String, topic: String, user: User?, since: String? = null): List<Notification> {
+        val sinceVal = since ?: "all"
         val url = topicUrlJsonPoll(baseUrl, topic, sinceVal)
         Log.d(TAG, "Polling topic $url")
 
@@ -108,12 +108,12 @@ class ApiService {
     fun subscribe(
         baseUrl: String,
         topics: String,
-        since: Long,
+        since: String?,
         user: User?,
         notify: (topic: String, Notification) -> Unit,
         fail: (Exception) -> Unit
     ): Call {
-        val sinceVal = if (since == 0L) "all" else since.toString()
+        val sinceVal = since ?: "all"
         val url = topicUrlJson(baseUrl, topics, sinceVal)
         Log.d(TAG, "Opening subscription connection to $url")
         val request = requestBuilder(url, user).build()
diff --git a/app/src/main/java/io/heckel/ntfy/service/Connection.kt b/app/src/main/java/io/heckel/ntfy/service/Connection.kt
index a8dfed1..71a98be 100644
--- a/app/src/main/java/io/heckel/ntfy/service/Connection.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/Connection.kt
@@ -3,7 +3,7 @@ package io.heckel.ntfy.service
 interface Connection {
     fun start()
     fun close()
-    fun since(): Long
+    fun since(): String?
 }
 
 data class ConnectionId(
diff --git a/app/src/main/java/io/heckel/ntfy/service/JsonConnection.kt b/app/src/main/java/io/heckel/ntfy/service/JsonConnection.kt
index 9ccb93e..8bca688 100644
--- a/app/src/main/java/io/heckel/ntfy/service/JsonConnection.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/JsonConnection.kt
@@ -14,7 +14,7 @@ class JsonConnection(
     private val repository: Repository,
     private val api: ApiService,
     private val user: User?,
-    private val sinceTime: Long,
+    private val sinceId: String?,
     private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit,
     private val notificationListener: (Subscription, Notification) -> Unit,
     private val serviceActive: () -> Boolean
@@ -25,7 +25,7 @@ class JsonConnection(
     private val topicsStr = topicsToSubscriptionIds.keys.joinToString(separator = ",")
     private val url = topicUrl(baseUrl, topicsStr)
 
-    private var since: Long = sinceTime
+    private var since: String? = sinceId
     private lateinit var call: Call
     private lateinit var job: Job
 
@@ -39,7 +39,7 @@ class JsonConnection(
                 Log.d(TAG, "[$url] (Re-)starting connection for subscriptions: $topicsToSubscriptionIds")
                 val startTime = System.currentTimeMillis()
                 val notify = notify@ { topic: String, notification: Notification ->
-                    since = notification.timestamp
+                    since = notification.id
                     val subscriptionId = topicsToSubscriptionIds[topic] ?: return@notify
                     val subscription = repository.getSubscription(subscriptionId) ?: return@notify
                     val notificationWithSubscriptionId = notification.copy(subscriptionId = subscription.id)
@@ -81,7 +81,7 @@ class JsonConnection(
         }
     }
 
-    override fun since(): Long {
+    override fun since(): String? {
         return since
     }
 
diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
index b453e4d..634dfe3 100644
--- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
@@ -178,14 +178,8 @@ class SubscriberService : Service() {
         val newConnectionIds = desiredConnectionIds.subtract(activeConnectionIds)
         val obsoleteConnectionIds = activeConnectionIds.subtract(desiredConnectionIds)
         val match = activeConnectionIds == desiredConnectionIds
-        val newSinceByBaseUrl = connections
-            .map { e ->
-                // Get last message timestamp to determine new ?since= param; set to $last+1 if it
-                // is defined to avoid retrieving old messages. See comment below too.
-                val lastMessage = e.value.since()
-                val newSince = if (lastMessage > 0) lastMessage+1 else 0
-                e.key.baseUrl to newSince
-            }
+        val sinceByBaseUrl = connections
+            .map { e -> e.key.baseUrl to e.value.since() } // Use since=<id>, avoid retrieving old messages (see comment below)
             .toMap()
 
         Log.d(TAG, "Refreshing subscriptions")
@@ -205,7 +199,7 @@ class SubscriberService : Service() {
             // IMPORTANT: Do NOT request old messages for new connections; we call poll() in MainActivity to
             // retrieve old messages. This is important, so we don't download attachments from old messages.
 
-            val since = newSinceByBaseUrl[connectionId.baseUrl] ?: (System.currentTimeMillis() / 1000)
+            val since = sinceByBaseUrl[connectionId.baseUrl] ?: "none"
             val serviceActive = { -> isServiceStarted }
             val user = repository.getUser(connectionId.baseUrl)
             val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
diff --git a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt
index a79ce07..1f168d3 100644
--- a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt
@@ -5,15 +5,19 @@ import android.os.Build
 import android.os.Handler
 import android.os.Looper
 import io.heckel.ntfy.db.*
-import io.heckel.ntfy.util.Log
 import io.heckel.ntfy.msg.ApiService.Companion.requestBuilder
 import io.heckel.ntfy.msg.NotificationParser
+import io.heckel.ntfy.util.Log
 import io.heckel.ntfy.util.topicShortUrl
 import io.heckel.ntfy.util.topicUrlWs
-import okhttp3.*
+import okhttp3.OkHttpClient
+import okhttp3.Response
+import okhttp3.WebSocket
+import okhttp3.WebSocketListener
 import java.util.*
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicLong
+import java.util.concurrent.atomic.AtomicReference
 import kotlin.random.Random
 
 /**
@@ -30,7 +34,7 @@ class WsConnection(
     private val connectionId: ConnectionId,
     private val repository: Repository,
     private val user: User?,
-    private val sinceTime: Long,
+    private val sinceId: String?,
     private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit,
     private val notificationListener: (Subscription, Notification) -> Unit,
     private val alarmManager: AlarmManager
@@ -49,7 +53,7 @@ class WsConnection(
     private val globalId = GLOBAL_ID.incrementAndGet()
     private val listenerId = AtomicLong(0)
 
-    private val since = AtomicLong(sinceTime)
+    private val since = AtomicReference<String?>(sinceId)
     private val baseUrl = connectionId.baseUrl
     private val topicsToSubscriptionIds = connectionId.topicsToSubscriptionIds
     private val subscriptionIds = topicsToSubscriptionIds.values
@@ -71,7 +75,8 @@ class WsConnection(
         }
         state = State.Connecting
         val nextListenerId = listenerId.incrementAndGet()
-        val sinceVal = if (since.get() == 0L) "all" else since.get().toString()
+        val sinceId = since.get()
+        val sinceVal = sinceId ?: "all"
         val urlWithSince = topicUrlWs(baseUrl, topicsStr, sinceVal)
         val request = requestBuilder(urlWithSince, user).build()
         Log.d(TAG, "$shortUrl (gid=$globalId): Opening $urlWithSince with listener ID $nextListenerId ...")
@@ -92,7 +97,7 @@ class WsConnection(
     }
 
     @Synchronized
-    override fun since(): Long {
+    override fun since(): String? {
         return since.get()
     }
 
@@ -141,7 +146,7 @@ class WsConnection(
                 val subscription = repository.getSubscription(subscriptionId) ?: return@synchronize
                 val notificationWithSubscriptionId = notification.copy(subscriptionId = subscription.id)
                 notificationListener(subscription, notificationWithSubscriptionId)
-                since.set(notification.timestamp)
+                since.set(notification.id)
             }
         }
 
diff --git a/app/src/main/java/io/heckel/ntfy/ui/BaseUrl.kt b/app/src/main/java/io/heckel/ntfy/ui/BaseUrl.kt
index d1fd23a..d54de06 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/BaseUrl.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/BaseUrl.kt
@@ -9,13 +9,17 @@ import io.heckel.ntfy.R
 
 fun initBaseUrlDropdown(baseUrls: List<String>, textView: AutoCompleteTextView, layout: TextInputLayout) {
     // Base URL dropdown behavior; Oh my, why is this so complicated?!
+    val context = layout.context
     val toggleEndIcon = {
         if (textView.text.isNotEmpty()) {
             layout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
+            layout.endIconContentDescription = context.getString(R.string.add_dialog_base_urls_dropdown_clear)
         } else if (baseUrls.isEmpty()) {
             layout.setEndIconDrawable(0)
+            layout.endIconContentDescription = ""
         } else {
             layout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
+            layout.endIconContentDescription = context.getString(R.string.add_dialog_base_urls_dropdown_choose)
         }
     }
     layout.setEndIconOnClickListener {
@@ -23,11 +27,14 @@ fun initBaseUrlDropdown(baseUrls: List<String>, textView: AutoCompleteTextView,
             textView.text.clear()
             if (baseUrls.isEmpty()) {
                 layout.setEndIconDrawable(0)
+                layout.endIconContentDescription = ""
             } else {
                 layout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
+                layout.endIconContentDescription = context.getString(R.string.add_dialog_base_urls_dropdown_choose)
             }
         } else if (textView.text.isEmpty() && baseUrls.isNotEmpty()) {
             layout.setEndIconDrawable(R.drawable.ic_drop_up_gray_24dp)
+            layout.endIconContentDescription = context.getString(R.string.add_dialog_base_urls_dropdown_choose)
             textView.showDropDown()
         }
     }
@@ -49,10 +56,13 @@ fun initBaseUrlDropdown(baseUrls: List<String>, textView: AutoCompleteTextView,
     textView.setAdapter(adapter)
     if (baseUrls.count() == 1) {
         layout.setEndIconDrawable(R.drawable.ic_cancel_gray_24dp)
+        layout.endIconContentDescription = context.getString(R.string.add_dialog_base_urls_dropdown_clear)
         textView.setText(baseUrls.first())
     } else if (baseUrls.count() > 1) {
         layout.setEndIconDrawable(R.drawable.ic_drop_down_gray_24dp)
+        layout.endIconContentDescription = context.getString(R.string.add_dialog_base_urls_dropdown_choose)
     } else {
         layout.setEndIconDrawable(0)
+        layout.endIconContentDescription = ""
     }
 }
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 7e2621b..cfa4553 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
@@ -105,13 +105,14 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
             if (subscription == null) {
                 val instant = baseUrl != appBaseUrl
                 subscription = Subscription(
-                    id = Random.nextLong(),
+                    id = randomSubscriptionId(),
                     baseUrl = baseUrl,
                     topic = topic,
                     instant = instant,
                     mutedUntil = 0,
                     minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
                     autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
+                    lastNotificationId = null,
                     icon = null,
                     upAppId = null,
                     upConnectorToken = null,
@@ -457,8 +458,9 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
 
         lifecycleScope.launch(Dispatchers.IO) {
             try {
-                val user = repository.getUser(subscriptionBaseUrl) // May be null
-                val notifications = api.poll(subscriptionId, subscriptionBaseUrl, subscriptionTopic, user)
+                val subscription = repository.getSubscription(subscriptionId) ?: return@launch
+                val user = repository.getUser(subscription.baseUrl) // May be null
+                val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
                 val newNotifications = repository.onlyNewNotifications(subscriptionId, notifications)
                 val toastMessage = if (newNotifications.isEmpty()) {
                     getString(R.string.refresh_message_no_results)
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 6781f98..5321a85 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -9,11 +9,13 @@ import android.net.Uri
 import android.os.Build
 import android.os.Bundle
 import android.provider.Settings
+import android.text.method.LinkMovementMethod
 import android.view.ActionMode
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
 import android.widget.Button
+import android.widget.TextView
 import android.widget.Toast
 import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
@@ -41,6 +43,7 @@ import io.heckel.ntfy.work.PollWorker
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import java.security.SecureRandom
 import java.util.*
 import java.util.concurrent.TimeUnit
 import kotlin.random.Random
@@ -119,9 +122,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
                     Log.addScrubTerm(s.topic)
                 }
 
-                // Update banner + JSON stream banner
+                // Update banner + WebSocket banner
                 showHideBatteryBanner(subscriptions)
-                showHideJsonStreamBanner(subscriptions)
+                showHideWebSocketBanner(subscriptions)
             }
         }
 
@@ -169,21 +172,25 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
             }
         }
 
-        // JSON stream banner
-        val jsonStreamBanner = findViewById<View>(R.id.main_banner_json_stream) // Banner visibility is toggled in onResume()
-        val jsonStreamDismissButton = findViewById<Button>(R.id.main_banner_json_stream_dontaskagain)
-        val jsonStreamRemindButton = findViewById<Button>(R.id.main_banner_json_stream_remind_later)
-        val jsonStreamLearnMoreButton = findViewById<Button>(R.id.main_banner_json_stream_learn_mode)
-        jsonStreamDismissButton.setOnClickListener {
-            jsonStreamBanner.visibility = View.GONE
-            repository.setJsonStreamRemindTime(Repository.JSON_STREAM_REMIND_TIME_NEVER)
+        // WebSocket banner
+        val wsBanner = findViewById<View>(R.id.main_banner_websocket) // Banner visibility is toggled in onResume()
+        val wsText = findViewById<TextView>(R.id.main_banner_websocket_text)
+        val wsDismissButton = findViewById<Button>(R.id.main_banner_websocket_dontaskagain)
+        val wsRemindButton = findViewById<Button>(R.id.main_banner_websocket_remind_later)
+        val wsEnableButton = findViewById<Button>(R.id.main_banner_websocket_enable)
+        wsText.movementMethod = LinkMovementMethod.getInstance() // Make links clickable
+        wsDismissButton.setOnClickListener {
+            wsBanner.visibility = View.GONE
+            repository.setWebSocketRemindTime(Repository.WEBSOCKET_REMIND_TIME_NEVER)
         }
-        jsonStreamRemindButton.setOnClickListener {
-            jsonStreamBanner.visibility = View.GONE
-            repository.setJsonStreamRemindTime(System.currentTimeMillis() + ONE_DAY_MILLIS)
+        wsRemindButton.setOnClickListener {
+            wsBanner.visibility = View.GONE
+            repository.setWebSocketRemindTime(System.currentTimeMillis() + ONE_DAY_MILLIS)
         }
-        jsonStreamLearnMoreButton.setOnClickListener {
-            startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_banner_json_stream_button_learn_more_url))))
+        wsEnableButton.setOnClickListener {
+            repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS)
+            SubscriberServiceManager(this).restart()
+            wsBanner.visibility = View.GONE
         }
 
         // Create notification channels right away, so we can configure them immediately after installing the app
@@ -217,13 +224,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
         Log.d(TAG, "Battery: ignoring optimizations = $ignoringOptimizations (we want this to be true); instant subscriptions = $hasInstantSubscriptions; remind time reached = $batteryRemindTimeReached; banner = $showBanner")
     }
 
-    private fun showHideJsonStreamBanner(subscriptions: List<Subscription>) {
-        val hasSelfhostedSubscriptions = subscriptions.count { it.baseUrl != appBaseUrl } > 0
+    private fun showHideWebSocketBanner(subscriptions: List<Subscription>) {
+        val hasSelfHostedSubscriptions = subscriptions.count { it.baseUrl != appBaseUrl } > 0
         val usingWebSockets = repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS
-        val jsonStreamRemindTimeReached = repository.getJsonStreamRemindTime() < System.currentTimeMillis()
-        val showBanner = hasSelfhostedSubscriptions && jsonStreamRemindTimeReached && !usingWebSockets
-        val jsonStreamBanner = findViewById<View>(R.id.main_banner_json_stream)
-        jsonStreamBanner.visibility = if (showBanner) View.VISIBLE else View.GONE
+        val wsRemindTimeReached = repository.getWebSocketRemindTime() < System.currentTimeMillis()
+        val showBanner = hasSelfHostedSubscriptions && wsRemindTimeReached && !usingWebSockets
+        val wsBanner = findViewById<View>(R.id.main_banner_websocket)
+        wsBanner.visibility = if (showBanner) View.VISIBLE else View.GONE
     }
 
     private fun schedulePeriodicPollWorker() {
@@ -420,13 +427,14 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
 
         // Add subscription to database
         val subscription = Subscription(
-            id = Random.nextLong(),
+            id = randomSubscriptionId(),
             baseUrl = baseUrl,
             topic = topic,
             instant = instant,
             mutedUntil = 0,
             minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
             autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
+            lastNotificationId = null,
             icon = null,
             upAppId = null,
             upConnectorToken = null,
@@ -488,9 +496,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
             var errorMessage = "" // First error
             var newNotificationsCount = 0
             repository.getSubscriptions().forEach { subscription ->
+                Log.d(TAG, "subscription: ${subscription}")
                 try {
                     val user = repository.getUser(subscription.baseUrl) // May be null
-                    val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
+                    val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
                     val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
                     newNotifications.forEach { notification ->
                         newNotificationsCount++
diff --git a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
index 9cc3255..6906399 100644
--- a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
+++ b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
@@ -7,10 +7,7 @@ import io.heckel.ntfy.app.Application
 import io.heckel.ntfy.db.Repository
 import io.heckel.ntfy.db.Subscription
 import io.heckel.ntfy.service.SubscriberServiceManager
-import io.heckel.ntfy.util.Log
-import io.heckel.ntfy.util.randomString
-import io.heckel.ntfy.util.shortUrl
-import io.heckel.ntfy.util.topicUrlUp
+import io.heckel.ntfy.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.delay
@@ -71,13 +68,14 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
                 val topic = UP_PREFIX + randomString(TOPIC_RANDOM_ID_LENGTH)
                 val endpoint = topicUrlUp(baseUrl, topic)
                 val subscription = Subscription(
-                    id = Random.nextLong(),
+                    id = randomSubscriptionId(),
                     baseUrl = baseUrl,
                     topic = topic,
                     instant = true, // No Firebase, always instant!
                     mutedUntil = 0,
                     minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
                     autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
+                    lastNotificationId = null,
                     icon = null,
                     upAppId = appId,
                     upConnectorToken = connectorToken,
diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt
index 6676942..aa23f55 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Util.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt
@@ -43,6 +43,7 @@ import java.text.DateFormat
 import java.text.StringCharacterIterator
 import java.util.*
 import kotlin.math.abs
+import kotlin.math.absoluteValue
 
 fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
 fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush
@@ -276,6 +277,13 @@ fun randomString(len: Int): String {
     return (1..len).map { chars[random.nextInt(chars.size)] }.joinToString("")
 }
 
+// Generates a random, positive subscription ID between 0-10M. This ensures that it doesn't have issues
+// when exported to JSON. It uses SecureRandom, because Random causes issues in the emulator (generating the
+// same value again and again), sometimes.
+fun randomSubscriptionId(): Long {
+    return SecureRandom().nextLong().absoluteValue % 100_000_000
+}
+
 // Allows letting multiple variables at once, see https://stackoverflow.com/a/35522422/1440785
 inline fun <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
     return if (p1 != null && p2 != null) block(p1, p2) else null
diff --git a/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt b/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt
index 90a1e0e..582d071 100644
--- a/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt
+++ b/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt
@@ -45,7 +45,7 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
                         baseUrl = subscription.baseUrl,
                         topic = subscription.topic,
                         user = user,
-                        since = subscription.lastActive
+                        since = subscription.lastNotificationId
                     )
                     val newNotifications = repository
                         .onlyNewNotifications(subscription.id, notifications)
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index c286b50..0bb4bd6 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -86,64 +86,64 @@
             android:layout_height="wrap_content"
             app:shapeAppearance="?shapeAppearanceLargeComponent" app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/main_banner_battery"
-            android:id="@+id/main_banner_json_stream" android:visibility="visible">
+            android:id="@+id/main_banner_websocket" android:visibility="visible">
 
         <androidx.constraintlayout.widget.ConstraintLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:id="@+id/main_banner_json_stream_constraint" android:elevation="5dp">
+                android:id="@+id/main_banner_websocket_constraint" android:elevation="5dp">
 
             <ImageView
                     android:layout_width="28dp"
                     android:layout_height="28dp" app:srcCompat="@drawable/ic_announcement_orange_24dp"
-                    android:id="@+id/main_banner_json_stream_image"
+                    android:id="@+id/main_banner_websocket_image"
                     app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toTopOf="@+id/main_banner_json_stream_text"
-                    app:layout_constraintEnd_toStartOf="@id/main_banner_json_stream_text"
-                    app:layout_constraintBottom_toBottomOf="@+id/main_banner_json_stream_text"
+                    app:layout_constraintTop_toTopOf="@+id/main_banner_websocket_text"
+                    app:layout_constraintEnd_toStartOf="@id/main_banner_websocket_text"
+                    app:layout_constraintBottom_toBottomOf="@+id/main_banner_websocket_text"
                     android:layout_marginStart="15dp"/>
             <TextView
-                    android:id="@+id/main_banner_json_stream_text"
+                    android:id="@+id/main_banner_websocket_text"
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
-                    android:text="@string/main_banner_json_stream_text"
+                    android:text="@string/main_banner_websocket_text"
                     android:textAppearance="@style/TextAppearance.AppCompat.Small"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintTop_toTopOf="parent"
                     android:layout_marginEnd="15dp" android:layout_marginTop="15dp"
-                    app:layout_constraintStart_toEndOf="@+id/main_banner_json_stream_image"
-                    android:layout_marginStart="10dp" android:autoLink="web"/>
+                    app:layout_constraintStart_toEndOf="@+id/main_banner_websocket_image"
+                    android:layout_marginStart="10dp"
+            />
 
             <androidx.constraintlayout.helper.widget.Flow
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     app:layout_constraintEnd_toEndOf="parent"
-                    app:constraint_referenced_ids="main_banner_json_stream_remind_later,main_banner_json_stream_dontaskagain,main_banner_json_stream_learn_mode" app:layout_constraintTop_toBottomOf="@id/main_banner_json_stream_text" app:flow_horizontalAlign="end" app:flow_wrapMode="chain" app:flow_horizontalStyle="packed" android:layout_marginEnd="15dp" android:id="@+id/flow" app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="15dp" app:flow_horizontalBias="1"
+                    app:constraint_referenced_ids="main_banner_websocket_remind_later,main_banner_websocket_dontaskagain,main_banner_websocket_enable" app:layout_constraintTop_toBottomOf="@id/main_banner_websocket_text" app:flow_horizontalAlign="end" app:flow_wrapMode="chain" app:flow_horizontalStyle="packed" android:layout_marginEnd="15dp" android:id="@+id/flow" app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="15dp" app:flow_horizontalBias="1"
                     app:flow_verticalGap="0dp" app:flow_horizontalGap="0dp"/>
 
             <com.google.android.material.button.MaterialButton
-                    android:id="@+id/main_banner_json_stream_remind_later"
+                    android:id="@+id/main_banner_websocket_remind_later"
                     style="@style/Widget.MaterialComponents.Button.TextButton"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:text="@string/main_banner_json_stream_button_remind_later"
+                    android:text="@string/main_banner_websocket_button_remind_later"
                     tools:layout_editor_absoluteX="86dp" tools:layout_editor_absoluteY="83dp"/>
 
             <com.google.android.material.button.MaterialButton
-                    android:id="@+id/main_banner_json_stream_dontaskagain"
+                    android:id="@+id/main_banner_websocket_dontaskagain"
                     style="@style/Widget.MaterialComponents.Button.TextButton"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:text="@string/main_banner_json_stream_button_dismiss"
+                    android:text="@string/main_banner_websocket_button_dismiss"
                     tools:layout_editor_absoluteX="260dp" tools:layout_editor_absoluteY="83dp"/>
 
-
             <com.google.android.material.button.MaterialButton
-                    android:id="@+id/main_banner_json_stream_learn_mode"
+                    android:id="@+id/main_banner_websocket_enable"
                     style="@style/Widget.MaterialComponents.Button.TextButton"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:text="@string/main_banner_json_stream_button_learn_more"
+                    android:text="@string/main_banner_websocket_button_enable_now"
                     tools:layout_editor_absoluteX="253dp" tools:layout_editor_absoluteY="131dp"/>
         </androidx.constraintlayout.widget.ConstraintLayout>
     </com.google.android.material.card.MaterialCardView>
@@ -155,7 +155,7 @@
             android:visibility="visible"
             app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/main_banner_json_stream">
+            app:layout_constraintTop_toBottomOf="@id/main_banner_websocket">
         <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/main_subscriptions_list"
                 android:layout_width="match_parent"
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 2b70853..2e36282 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -82,7 +82,6 @@
     <string name="main_item_status_reconnecting">свързване…</string>
     <string name="main_no_subscriptions_text">В момента няма абонаменти</string>
     <string name="main_unified_push_toast">Този абонамент се управлява от %1$s чрез UnifiedPush</string>
-    <string name="main_banner_json_stream_button_learn_more">Научете повече</string>
     <string name="add_dialog_button_cancel">Отказ</string>
     <string name="add_dialog_button_subscribe">Абониране</string>
     <string name="add_dialog_button_back">Назад</string>
@@ -99,9 +98,9 @@
     <string name="detail_clear_dialog_permanently_delete">Премахване</string>
     <string name="detail_clear_dialog_cancel">Отказ</string>
     <string name="main_banner_battery_button_remind_later">По-късно</string>
-    <string name="main_banner_json_stream_button_remind_later">По-късно</string>
+    <string name="main_banner_websocket_button_remind_later">По-късно</string>
     <string name="main_banner_battery_button_dismiss">Отхвърляне</string>
-    <string name="main_banner_json_stream_button_dismiss">Отхвърляне</string>
+    <string name="main_banner_websocket_button_dismiss">Отхвърляне</string>
     <string name="main_banner_battery_text">Оптимизирането на батерията трябва да е изключено, за да бъдат избегнати забавяния при получаване на известията.</string>
     <string name="main_banner_battery_button_fix_now">Настройки</string>
     <string name="add_dialog_instant_delivery">Незабавно доставяне в режим на сън</string>
@@ -184,7 +183,6 @@
     <string name="settings_notifications_auto_delete_three_months">След три месеца</string>
     <string name="settings_notifications_auto_delete_one_month">След един месец</string>
     <string name="settings_advanced_export_logs_entry_copy_scrubbed">Копиране в междинната памет (цензурирано)</string>
-    <string name="main_banner_json_stream_text">От юни 2022 г. за връзка със сървърите на ntfy ще се използва WebSockets. Не забравяйте да настроите собствения сървър да го поддържа. За да проверите дали поддръжката на WebSocket работи, разрешете я в Настройки, в раздел Протокол за връзка.</string>
     <string name="settings_advanced_connection_protocol_summary_jsonhttp">За свързване със сървъра се използва поток от JSON през HTTP. Методът е остарял и ще бъде премахнат през месец юни 2022 год.</string>
     <string name="detail_test_message">Това е пробно известие от приложението ntfy за Android. То е с приоритет %1$d. Ако изпратите друго, то може да изглежда по различен начин.</string>
     <string name="detail_test_title">Проба: Ако желаете можете да сложите заглавие</string>
@@ -319,4 +317,6 @@
     <string name="detail_settings_notifications_instant_summary_off">Известията се доставят с помощта на Firebase. Доставката може да се забави, но изразходва по-малко батерия.</string>
     <string name="detail_settings_appearance_icon_error_saving">Значката не може да бъде запазена: %1$s</string>
     <string name="detail_settings_global_setting_suffix">от общите настройки</string>
-</resources>
\ No newline at end of file
+    <string name="add_dialog_base_urls_dropdown_choose">Изберете адрес на услугата</string>
+    <string name="add_dialog_base_urls_dropdown_clear">Изчистване на адреса на услугата</string>
+</resources>
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 9519cf8..dc99d16 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -42,9 +42,8 @@
     <string name="main_unified_push_toast">Tento odběr spravuje %1$s prostřednictvím UnifiedPush</string>
     <string name="main_banner_battery_text">Optimalizace baterie by měla být pro aplikaci vypnutá, aby se předešlo problémům s doručováním oznámení.</string>
     <string name="main_banner_battery_button_remind_later">Zeptat se později</string>
-    <string name="main_banner_json_stream_text">Přepněte na WebSockets v Nastavení / \"Protokol připojení\" na , abyste zajistili, že budete moci komunikovat s vlastním serverem ntfy i po červnu 2022.</string>
-    <string name="main_banner_json_stream_button_remind_later">Zeptat se později</string>
-    <string name="main_banner_json_stream_button_dismiss">Zavřít</string>
+    <string name="main_banner_websocket_button_remind_later">Zeptat se později</string>
+    <string name="main_banner_websocket_button_dismiss">Zavřít</string>
     <string name="add_dialog_topic_name_hint">Název tématu, např. phils_alerts</string>
     <string name="add_dialog_use_another_server_description">Zadejte URL služby a přihlaste se k odběru témat z jiných serverů.</string>
     <string name="add_dialog_instant_delivery">Okamžité doručení v režimu spánku</string>
@@ -233,7 +232,6 @@
     <string name="refresh_message_error_one">Nepodařilo se obnovit odběr: %1$s</string>
     <string name="channel_subscriber_notification_instant_text_four">Přihlášeno k odběru čtyř témat okamžitého doručení</string>
     <string name="channel_subscriber_notification_noinstant_text_three">Odběr tří témat</string>
-    <string name="main_banner_json_stream_button_learn_more">Zjistit více</string>
     <string name="main_menu_notifications_disabled_forever">Oznámení ztlumena</string>
     <string name="main_how_to_intro">Kliknutím na tlačítko + vytvoříte téma nebo se k němu přihlásíte. Poté obdržíte na svém zařízení oznámení při odesílání zpráv prostřednictvím PUT nebo POST.</string>
     <string name="main_banner_battery_button_dismiss">Zavřít</string>
@@ -319,4 +317,4 @@
     <string name="channel_subscriber_notification_instant_text_six">Přihlášeno k odběru šesti témat okamžitého doručení</string>
     <string name="detail_settings_notifications_instant_summary_off">Oznámení jsou doručena pomocí služby Firebase. Doručování může být zpožděné, ale spotřebovává méně baterie.</string>
     <string name="detail_settings_notifications_instant_title">Okamžité doručení</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 88715e6..eb9e63f 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -45,8 +45,7 @@
     <string name="main_unified_push_toast">Dieses Abo wird von %1$s per UnifiedPush verwaltet</string>
     <string name="main_banner_battery_text">Batterieoptimierungen sollten für die App deaktiviert sein, um Zustellungsprobleme bei Benachrichtigungen zu vermeiden.</string>
     <string name="main_banner_battery_button_fix_now">Jetzt beheben</string>
-    <string name="main_banner_json_stream_button_remind_later">Später fragen</string>
-    <string name="main_banner_json_stream_button_learn_more">Mehr erfahren</string>
+    <string name="main_banner_websocket_button_remind_later">Später fragen</string>
     <string name="add_dialog_title">Thema abonnieren</string>
     <string name="add_dialog_description_below">Themen sind evtl. nicht kennwort-geschützt, also wähle einen schwer zu erratenden Namen. Nach dem Abonnieren kannst Du Benachrichtigungen POSTen/PUTen.</string>
     <string name="add_dialog_topic_name_hint">Themenname, z.B. phils_alerts</string>
@@ -57,7 +56,7 @@
     <string name="add_dialog_instant_delivery_description">Stellt sicher, dass Benachrichtigungen sofort zugestellt werden, auch wenn das Gerät im Schlafmodus ist.</string>
     <string name="add_dialog_button_cancel">Abbrechen</string>
     <string name="main_banner_battery_button_dismiss">Verwerfen</string>
-    <string name="main_banner_json_stream_button_dismiss">Verwerfen</string>
+    <string name="main_banner_websocket_button_dismiss">Verwerfen</string>
     <string name="add_dialog_login_title">Anmeldung erforderlich</string>
     <string name="add_dialog_login_description">Dieses Thema benötigt eine Anmeldung. Bitte gib Benutzernamen und Kennwort ein.</string>
     <string name="add_dialog_login_password_hint">Kennwort</string>
@@ -238,7 +237,6 @@
     <string name="main_banner_battery_button_remind_later">Später fragen</string>
     <string name="main_action_mode_delete_dialog_message">Vom gewählten Thema abmelden und alle Benachrichtigungen endgültig löschen\?</string>
     <string name="main_how_to_intro">Klicke den + Button zum Erstellen oder Abonnieren eines Themas. Dann erhältst Du Benachrichtigungen auf dem Gerät, wenn Nachrichten per PUT oder POST veröffentlicht werden.</string>
-    <string name="main_banner_json_stream_text">Stelle in Einstellungen das Verbindungsprotokoll auf WebSockets um, damit die App sich auch nach Juni 2022 mit Deinem selbst betriebenen Server verbinden kann.</string>
     <string name="add_dialog_login_error_not_authorized">Anmeldung fehlgeschlagen. Keine Berechtigung für Benutzer %1$s.</string>
     <string name="add_dialog_login_username_hint">Benutzername</string>
     <string name="add_dialog_foreground_description">Sofortnachrichten sind für andere Server als %1$s immer aktiviert.</string>
@@ -319,4 +317,4 @@
     <string name="detail_settings_notifications_instant_title">Sofortnachrichten</string>
     <string name="detail_settings_appearance_icon_remove_summary">Icon, das in Benachrichtigungen zu diesem Thema angezeigt wird</string>
     <string name="detail_settings_notifications_instant_summary_on">Benachrichtigungen werden sofort zugestellt. Benötigt einen Vordergrund-Dienst und verbraucht mehr Akku.</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index b2a2e63..f43313d 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -18,7 +18,7 @@
     <string name="channel_subscriber_notification_noinstant_text_more">Suscrito a %1$d tópicos</string>
     <string name="refresh_message_result">%1$d notificación(es) recibida(s)</string>
     <string name="refresh_message_no_results">Todo está actualizado</string>
-    <string name="refresh_message_error">No se pudieron actualizar %1$d suscripciones 
+    <string name="refresh_message_error">No se pudieron actualizar %1$d suscripciones
 \n
 \n%2$s</string>
     <string name="refresh_message_error_one">No se pudo actualizar la suscripción: %1$s</string>
@@ -36,8 +36,7 @@
     <string name="main_banner_battery_text">La optimización de batería debe deshabilitarse para esta app para evitar problemas con la entrega de notificaciones.</string>
     <string name="main_banner_battery_button_remind_later">Preguntar más tarde</string>
     <string name="main_banner_battery_button_fix_now">Corregir ahora</string>
-    <string name="main_banner_json_stream_button_dismiss">Descartar</string>
-    <string name="main_banner_json_stream_button_learn_more">Más información</string>
+    <string name="main_banner_websocket_button_dismiss">Descartar</string>
     <string name="add_dialog_title">Suscribirse al tópico</string>
     <string name="add_dialog_description_below">Los tópicos podrían no estar protegidos por contraseña, así que elija un nombre que sea difícil de adivinar. Una vez suscrito, puede enviar notificaciones con PUT/POST.</string>
     <string name="add_dialog_topic_name_hint">Nombre del tópico, p. ej. phils_alerts</string>
@@ -277,7 +276,7 @@
     <string name="settings_general_dark_mode_summary_light">Modo claro activado</string>
     <string name="settings_backup_restore_restore_summary">Importar configuración, notificaciones y usuarios</string>
     <string name="user_dialog_description_edit">Puede editar el nombre de usuario / contraseña para el usuario seleccionado o eliminarlo.</string>
-    <string name="main_banner_json_stream_button_remind_later">Preguntar más tarde</string>
+    <string name="main_banner_websocket_button_remind_later">Preguntar más tarde</string>
     <string name="add_dialog_foreground_description">La entrega instantánea siempre está habilitada para los hosts que no sean %1$s.</string>
     <string name="add_dialog_login_title">Se requiere inicio de sesión</string>
     <string name="main_action_mode_delete_dialog_permanently_delete">Eliminar permanentemente</string>
@@ -289,7 +288,6 @@
     <string name="main_menu_rate_title">Califica la aplicación ⭐</string>
     <string name="main_unified_push_toast">Esta suscripción es gestionada por %1$s a través de UnifiedPush</string>
     <string name="main_banner_battery_button_dismiss">Descartar</string>
-    <string name="main_banner_json_stream_text">Cambie a WebSockets en Ajustes / \"Protocolo de conexión\" ahora para asegurarse de que puede comunicarse con su servidor autoalojado ntfy después de junio de 2022.</string>
     <string name="add_dialog_use_another_server">Usar otro servidor</string>
     <string name="detail_how_to_link">Instrucciones detalladas están disponibles en ntfy.sh y en la documentación.</string>
     <string name="main_menu_settings_title">Ajustes</string>
@@ -319,4 +317,4 @@
     <string name="detail_settings_notifications_instant_summary_off">Las notificaciones se entregan usando Firebase. La entrega puede retrasarse, pero consume menos batería.</string>
     <string name="detail_settings_notifications_instant_title">Entrega instantánea</string>
     <string name="detail_settings_appearance_icon_set_summary">Establecer un icono para que aparezca en las notificaciones</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index eb9b873..86ad97a 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -20,9 +20,8 @@
     <string name="main_add_button_description">Ajout d\'abonnement</string>
     <string name="main_how_to_link">Des instructions détaillées sont disponible sur ntfy.sh et dans la documentation.</string>
     <string name="main_banner_battery_text">L\'optimisation de la pile devrait être désactivée pour l\'application afin d\'éviter des problèmes de réception de notifications.</string>
-    <string name="main_banner_json_stream_button_remind_later">Demander plus tard</string>
-    <string name="main_banner_json_stream_button_dismiss">Ignorer</string>
-    <string name="main_banner_json_stream_button_learn_more">En apprendre plus</string>
+    <string name="main_banner_websocket_button_remind_later">Demander plus tard</string>
+    <string name="main_banner_websocket_button_dismiss">Ignorer</string>
     <string name="add_dialog_topic_name_hint">Nom de sujet, ex. : phils_alerts</string>
     <string name="add_dialog_instant_delivery">Livraison instantanée en mode somnolence</string>
     <string name="add_dialog_button_cancel">Annuler</string>
@@ -83,7 +82,6 @@
     <string name="main_how_to_intro">Cliquez le + pour créer ou vous abonner à un sujet. Ensuite, vous recevrez des notifications sur votre appareil lors de l\'envoi de messages par PUT ou POST.</string>
     <string name="main_unified_push_toast">Cet abonnement est géré par %1$s à l\'aide de UnifiedPush</string>
     <string name="main_banner_battery_button_remind_later">Demander plus tard</string>
-    <string name="main_banner_json_stream_text">Changez pour les WebSockets dans Paramètres / «Protocole de connexion» maintenant pour assurer que vous puissiez communiquer avec votre serveur ntfy hébergé par vous-même après le mois de juin 2022.</string>
     <string name="add_dialog_description_below">Les sujets ne peuvent pas être protégés par mot de passe, choisissez donc un nom difficile à deviner. Une fois abonné, vous pourrez PUT/POST des notifcations.</string>
     <string name="add_dialog_title">Abonner au sujet</string>
     <string name="add_dialog_error_connection_failed">La connexion a échouée : %1$s</string>
@@ -319,4 +317,4 @@
     <string name="detail_settings_appearance_icon_set_summary">Choisir une icône à afficher pour les notifications</string>
     <string name="detail_settings_global_setting_title">Utiliser le paramètre global</string>
     <string name="detail_settings_global_setting_suffix">utilisation du paramètre global</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 323ac11..986418d 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -39,9 +39,8 @@
     <string name="main_banner_battery_button_remind_later">Tanya nanti</string>
     <string name="main_banner_battery_button_dismiss">Abaikan</string>
     <string name="main_banner_battery_button_fix_now">Perbaiki sekarang</string>
-    <string name="main_banner_json_stream_text">Ubah ke WebSockets dalam Pengaturan / \"Protokol koneksi\" sekarang untuk memastikan Anda dapat berkomunikasi dengan server ntfy Anda setelah Juni 2022.</string>
-    <string name="main_banner_json_stream_button_remind_later">Tanya nanti</string>
-    <string name="main_banner_json_stream_button_dismiss">Abaikan</string>
+    <string name="main_banner_websocket_button_remind_later">Tanya nanti</string>
+    <string name="main_banner_websocket_button_dismiss">Abaikan</string>
     <string name="add_dialog_title">Berlangganan ke sebuah topik</string>
     <string name="add_dialog_description_below">Topik mungkin tidak dilindungi oleh kata sandi, jadi pilih sebuah nama yang susah untuk ditebak. Setelah berlangganan, Anda dapat PUT/POST notifikasi.</string>
     <string name="add_dialog_topic_name_hint">Nama topik, mis. pemberitahuan_andi</string>
@@ -223,7 +222,6 @@
     <string name="channel_subscriber_notification_instant_text_more">Berlangganan ke %1$d topik pengiriman instan</string>
     <string name="channel_subscriber_notification_noinstant_text_four">Berlangganan ke empat topik</string>
     <string name="refresh_message_result">%1$d notifikasi diterima</string>
-    <string name="main_banner_json_stream_button_learn_more">Pelajari lebih lanjut</string>
     <string name="channel_subscriber_notification_noinstant_text_three">Berlangganan ke tiga topik</string>
     <string name="channel_subscriber_notification_noinstant_text_more">Berlangganan ke %1$d topik</string>
     <string name="add_dialog_instant_delivery_description">Memastikan pesan dikirim dengan segera, bahkan jika perangkatnya tidak aktif.</string>
@@ -320,4 +318,6 @@
     <string name="detail_settings_notifications_instant_title">Pengiriman instan</string>
     <string name="detail_settings_notifications_instant_summary_off">Notifikasi dikirim menggunakan Firebase. Pengiriman mungkin telat, tetapi mengkonsumsi lebih sedikit baterai.</string>
     <string name="detail_settings_appearance_icon_set_title">Ikon langganan</string>
-</resources>
\ No newline at end of file
+    <string name="add_dialog_base_urls_dropdown_choose">Pilih URL layanan</string>
+    <string name="add_dialog_base_urls_dropdown_clear">Hapus URL layanan</string>
+</resources>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 81075ca..cb36163 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -36,9 +36,8 @@
     <string name="main_banner_battery_button_remind_later">Chiedi in seguito</string>
     <string name="main_banner_battery_button_dismiss">Abbandona</string>
     <string name="main_banner_battery_button_fix_now">Correggi ora</string>
-    <string name="main_banner_json_stream_text">Passa ora a WebSockets in Impostazioni/\"Protocollo di connessione\" per poter comunicare con il tuo server ntfy selfhosted dopo giugno 2022.</string>
-    <string name="main_banner_json_stream_button_remind_later">Chiedi in seguito</string>
-    <string name="main_banner_json_stream_button_dismiss">Abbandona</string>
+    <string name="main_banner_websocket_button_remind_later">Chiedi in seguito</string>
+    <string name="main_banner_websocket_button_dismiss">Abbandona</string>
     <string name="add_dialog_title">Iscriviti al topic</string>
     <string name="add_dialog_topic_name_hint">Nome del topic, es. phils_alerts</string>
     <string name="add_dialog_use_another_server">Usa un altro server</string>
@@ -193,7 +192,6 @@
     <string name="main_no_subscriptions_text">Sembra che non ci sia nessuna iscrizione al momento.</string>
     <string name="main_menu_report_bug_title">Segnala un bug</string>
     <string name="main_menu_rate_title">Valuta l\'app ⭐</string>
-    <string name="main_banner_json_stream_button_learn_more">Scopri di più</string>
     <string name="add_dialog_description_below">I topic possono essere non protetti da password, per cui scegli un nome che è difficile da indovinare. Una volta iscritti, è possibile effettuare notifiche PUT/POST.</string>
     <string name="add_dialog_instant_delivery_description">Assicura che i messaggi siano consegnati immediatamente, anche se il device non è attivo.</string>
     <string name="add_dialog_login_description">Questo topic richiede il login. Per favore, inserire username e password.</string>
@@ -319,4 +317,4 @@
     <string name="detail_settings_appearance_icon_remove_summary">Icona visualizzata nelle notifiche di questo topic</string>
     <string name="detail_settings_appearance_icon_error_saving">Impossibile salvare l\'icona: %1$s</string>
     <string name="detail_settings_global_setting_suffix">utilizzando l\'impostazione globale</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 3eaa2f7..8fb0788 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -23,7 +23,7 @@
     <string name="main_banner_battery_button_remind_later">שאל אחר כך</string>
     <string name="main_banner_battery_button_dismiss">הסר</string>
     <string name="main_banner_battery_button_fix_now">תקן עכשיו</string>
-    <string name="main_banner_json_stream_button_remind_later">שאל אחר כך</string>
+    <string name="main_banner_websocket_button_remind_later">שאל אחר כך</string>
     <string name="channel_notifications_min_name">התראות (עדיפות מינימאלית)</string>
     <string name="channel_subscriber_service_name">שירות רישום</string>
     <string name="channel_subscriber_notification_title">מאזין להתראות נכנסות</string>
@@ -39,5 +39,4 @@
     <string name="main_item_status_reconnecting">מתחבר מחדש…</string>
     <string name="main_how_to_intro">לחצ\\י על + על מנת ליצור או להירשם אל מול נושא מסוים. לאחר מכן תקבל\\י התראות במכשירך כשתשלח\\י התראות דרך PUT או POST.</string>
     <string name="main_how_to_link">הוראות מפורטות זמינות ב-ntfy.sh, ובדוקומנטציה.</string>
-    <string name="main_banner_json_stream_text">החלפ\\י ל-WebSockets תחת הגדרות \\ ״פרוטוקול חיבור״ עכשיו על מנת לוודא שאת\\ה יכול\\ה לתקשר עם שרת ה-ntfy באחסון עצמי שלך גם לאחר יוני 2022.</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index cc11b69..409b7d2 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -67,10 +67,8 @@
     <string name="add_dialog_title">トピックを購読</string>
     <string name="add_dialog_button_login">ログイン</string>
     <string name="add_dialog_login_error_not_authorized">ログインに失敗しました。ユーザー名 %1$s は許可されていません。</string>
-    <string name="main_banner_json_stream_button_remind_later">後で通知</string>
-    <string name="main_banner_json_stream_button_dismiss">無視</string>
-    <string name="main_banner_json_stream_button_learn_more">詳しく</string>
-    <string name="main_banner_json_stream_text">2022年6月以降もセルフホストのntfyサーバーと通信できるように、設定の「接続プロトコル」をWebSocketsに変更してください。</string>
+    <string name="main_banner_websocket_button_remind_later">後で通知</string>
+    <string name="main_banner_websocket_button_dismiss">無視</string>
     <string name="add_dialog_description_below">トピックはパスワード保護されないので、推測されにくい名前にしてください。購読した後、PUT/POSTで通知を送信できます。</string>
     <string name="add_dialog_instant_delivery">Dozeモードでの即時配信</string>
     <string name="add_dialog_instant_delivery_description">デバイスが非アクティブの状態でもメッセージが即時配信されるようにします。</string>
@@ -319,4 +317,4 @@
     <string name="detail_settings_notifications_instant_summary_off">通知はFirebaseを用いて配信されます。配信が遅延する事がありますが、バッテリーの消費は抑えられます。</string>
     <string name="detail_settings_appearance_icon_set_summary">通知に表示されるアイコンを指定します</string>
     <string name="channel_subscriber_notification_noinstant_text_five">トピックを5件購読しています</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index ac1c130..69f57e1 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -29,8 +29,7 @@
     <string name="main_item_status_reconnecting">kobler til igjen …</string>
     <string name="main_item_date_yesterday">i går</string>
     <string name="main_add_button_description">Legg til abonnement</string>
-    <string name="main_banner_json_stream_button_remind_later">Spør senere</string>
-    <string name="main_banner_json_stream_button_learn_more">Lær mer</string>
+    <string name="main_banner_websocket_button_remind_later">Spør senere</string>
     <string name="add_dialog_title">Abonner på emnet</string>
     <string name="add_dialog_topic_name_hint">Emnenavn, f.eks. halgeirs_varsler</string>
     <string name="add_dialog_use_another_server">Bruk en annen tjener</string>
@@ -41,7 +40,7 @@
     <string name="main_no_subscriptions_text">Abonner på noe først</string>
     <string name="main_banner_battery_text">Batterioptimalisering bør skrus av for å unngå problemer med merknadsleveringen.</string>
     <string name="main_banner_battery_button_dismiss">Avfei</string>
-    <string name="main_banner_json_stream_button_dismiss">Avfei</string>
+    <string name="main_banner_websocket_button_dismiss">Avfei</string>
     <string name="add_dialog_description_below">Det kan hende emner ikke er passordsbeskyttet, så velg et navn som ikke er enkelt å gjette. Når du har abonnert kan du utføre PUT/POST av merknader.</string>
     <string name="add_dialog_use_another_server_description">Du kan abonnere på emner fra en annen tjener. Skriv inn tjener-nettadressen nedenfor.</string>
     <string name="main_item_status_unified_push">%1$s (UnifiedPush)</string>
@@ -294,10 +293,9 @@
     <string name="main_how_to_link">Mer detaljert instruks er å finne på ntfy.sh-nettsiden og i dokumentasjonen.</string>
     <string name="main_unified_push_toast">Dette abonnementet håndteres av %1$s via UnifiedPush</string>
     <string name="add_dialog_foreground_description">Umiddelbar levering er alltid påskrudd for andre verter enn %1$s.</string>
-    <string name="main_banner_json_stream_text">Fra Juni 2022 vil Vev-socketer bli brukt til å kommunisere med ntfy-tjenerne. Forsikre deg om at du har satt opp din selvtjente tjener for å støtte det. For å sjekke om vev-socket-støtte fungerer, kan du skru det på i innstillingene under «Tilkoblingsprotokoll».</string>
     <string name="settings_notifications_priority_min">min</string>
     <string name="settings_notifications_priority_low">lav</string>
     <string name="settings_notifications_priority_default">forvalg</string>
     <string name="settings_notifications_priority_high">høy</string>
     <string name="settings_notifications_priority_max">maks.</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 44d199b..84799a1 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -16,7 +16,7 @@
     <string name="main_banner_battery_button_remind_later">Vraag later</string>
     <string name="main_banner_battery_button_dismiss">Afwijzen</string>
     <string name="main_banner_battery_button_fix_now">Nu oplossen</string>
-    <string name="main_banner_json_stream_button_remind_later">Vraag later</string>
+    <string name="main_banner_websocket_button_remind_later">Vraag later</string>
     <string name="add_dialog_title">Abonneren op onderwerp</string>
     <string name="add_dialog_login_title">Aanmelden vereist</string>
     <string name="add_dialog_button_back">Terug</string>
@@ -70,7 +70,7 @@
     <string name="main_action_mode_delete_dialog_permanently_delete">Permanent verwijderen</string>
     <string name="main_unified_push_toast">Dit abonnement wordt beheerd door %1$s via UnifiedPush</string>
     <string name="main_banner_battery_text">Batterij optimalisatie zou uitgeschakeld moeten zijn voor deze app om problemen met de bezorging van meldingen te voorkomen.</string>
-    <string name="main_banner_json_stream_button_dismiss">Afwijzen</string>
+    <string name="main_banner_websocket_button_dismiss">Afwijzen</string>
     <string name="user_dialog_button_delete">Gebruiker verwijderen</string>
     <string name="user_dialog_button_cancel">Annuleren</string>
     <string name="settings_advanced_connection_protocol_entry_ws">WebSockets</string>
@@ -141,10 +141,8 @@
     <string name="add_dialog_login_description">Dit onderwerp vereist dat je inlogt. Gelieve een gebruikersnaam en wachtwoord in te vullen.</string>
     <string name="detail_clear_dialog_message">Verwijder alle notificaties in dit onderwerp\?</string>
     <string name="add_dialog_instant_delivery">Onmiddellijke levering in \"doze\" modus</string>
-    <string name="main_banner_json_stream_text">Schakel nu over naar WebSockets in Instellingen / \"Verbindingsprotocol\" om er zeker van te zijn dat je kunt communiceren met je zelf gehoste ntfy server na juni 2022.</string>
     <string name="add_dialog_description_below">Onderwerpen zijn mogelijk niet beveiligd met een wachtwoord, dus kies een naam die moeilijk te raden is. Eenmaal geabonneerd, kunt u berichten PUT/POST\'en.</string>
     <string name="add_dialog_use_another_server_description">Vul server URLS hieronder in om te abonneren op onderwerpen van andere servers.</string>
-    <string name="main_banner_json_stream_button_learn_more">Leer meer</string>
     <string name="add_dialog_instant_delivery_description">Zorgt ervoor dat berichten onmiddellijk worden afgeleverd, zelfs als het toestel inactief is.</string>
     <string name="add_dialog_login_error_not_authorized">Login gefaald. Gebruiker %1$s is niet geauthoriseerd.</string>
     <string name="detail_no_notifications_text">Je hebt nog geen notificaties voor dit onderwerp ontvangen.</string>
@@ -319,4 +317,4 @@
     <string name="detail_settings_appearance_icon_set_summary">Stel een icoon in wat zal worden weergegeven in notificaties</string>
     <string name="detail_settings_appearance_icon_remove_title">Abonnementen icoon (tap om te verwijderen)</string>
     <string name="detail_settings_global_setting_suffix">Gebruikt globale instelling</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index f1af50c..6a5dbcf 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -41,9 +41,8 @@
     <string name="main_how_to_link">Instruções detalhadas disponíveis em ntfy.sh, e na documentação.</string>
     <string name="main_banner_battery_text">Otimização de bateria deve estar desligada para evitar problemas de entrega de notificação no aplicativo.</string>
     <string name="main_item_status_unified_push">%1$s (UnifiedPush)</string>
-    <string name="main_banner_json_stream_button_remind_later">Perguntar depois</string>
-    <string name="main_banner_json_stream_button_dismiss">Dispensar</string>
-    <string name="main_banner_json_stream_button_learn_more">Saiba mais</string>
+    <string name="main_banner_websocket_button_remind_later">Perguntar depois</string>
+    <string name="main_banner_websocket_button_dismiss">Dispensar</string>
     <string name="add_dialog_title">Inscrever-se no tema</string>
     <string name="add_dialog_description_below">Temas podem não ser protegidos por senha, escolha um nome difícil de adivinhar. Uma vez inscrito, você pode usar as notificações PUT/POST.</string>
     <string name="add_dialog_topic_name_hint">Nome do tema, ex: alertas_jose</string>
@@ -209,7 +208,6 @@
     <string name="main_menu_docs_title">Leia a documentação</string>
     <string name="main_action_mode_menu_unsubscribe">Cancelar inscrição</string>
     <string name="main_unified_push_toast">Essa inscrição é gerenciada por %1$s via UnifiedPush</string>
-    <string name="main_banner_json_stream_text">Altere para WebSockets em Configurações / \"Protocolo de conexão\" para garantir a comunicação com seu servidor hospedado ntfy após Junho de 2022.</string>
     <string name="main_item_status_text_not_one">%1$d notificações</string>
     <string name="main_how_to_intro">Clique no + para criar ou inscrever-se em um tema. Você receberá notificações no seu dispositivo ao enviar mensagens via PUT ou POST.</string>
     <string name="main_banner_battery_button_remind_later">Perguntar depois</string>
@@ -319,4 +317,4 @@
     <string name="channel_subscriber_notification_instant_text_six">Inscrito em seis tópicos de entrega instantânea</string>
     <string name="channel_subscriber_notification_noinstant_text_five">Inscrito em cinco tópicos</string>
     <string name="detail_settings_global_setting_suffix">usando configurações globais</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 3beb5ba..620f18b 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -70,15 +70,14 @@
     <string name="main_banner_battery_button_remind_later">Спросить позже</string>
     <string name="main_banner_battery_button_fix_now">Исправить</string>
     <string name="main_banner_battery_button_dismiss">Закрыть</string>
-    <string name="main_banner_json_stream_button_dismiss">Закрыть</string>
+    <string name="main_banner_websocket_button_dismiss">Закрыть</string>
     <string name="main_add_button_description">Добавить подписку</string>
     <string name="main_action_mode_menu_unsubscribe">Отписаться</string>
     <string name="main_item_status_text_one">%1$d уведомление</string>
     <string name="main_item_status_reconnecting">повторное подключение …</string>
     <string name="main_no_subscriptions_text">Похоже, у вас ещё нет подписок.</string>
     <string name="main_action_mode_delete_dialog_message">Отписаться от выбранных тем, и навсегда удалить все уведомления\?</string>
-    <string name="main_banner_json_stream_button_remind_later">Спросить позже</string>
-    <string name="main_banner_json_stream_button_learn_more">Узнать больше</string>
+    <string name="main_banner_websocket_button_remind_later">Спросить позже</string>
     <string name="add_dialog_title">Подписаться на тему</string>
     <string name="add_dialog_button_cancel">Отмена</string>
     <string name="main_unified_push_toast">Эта подписка управляется %1$s через UnifiedPush</string>
@@ -153,7 +152,6 @@
     <string name="refresh_message_no_results">Всё обновлено</string>
     <string name="add_dialog_instant_delivery">Мгновенная доставка в спящем режиме</string>
     <string name="main_banner_battery_text">Оптимизация батареи должна быть выключена, чтобы избежать проблем с доставкой уведомлений.</string>
-    <string name="main_banner_json_stream_text">Переключитесь на WebSockets в Настройках / \"Connection protocol\" сейчас, чтобы вы могли пользоваться своим собственным ntfy сервером после июня 2022.</string>
     <string name="add_dialog_error_connection_failed">Ошибка связи: %1$s</string>
     <string name="add_dialog_login_title">Требуется вход в аккаунт</string>
     <string name="add_dialog_login_description">Эта тема требует авторизации. Пожалуйста, введите имя пользователя и пароль.</string>
@@ -308,4 +306,4 @@
     <string name="channel_subscriber_notification_instant_text_six">Подписан на шесть тем с мгновенной доставкой</string>
     <string name="channel_subscriber_notification_noinstant_text_five">Подписан на пять тем</string>
     <string name="channel_subscriber_notification_noinstant_text_six">Подписан на шесть тем</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 08801c6..1197767 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -41,8 +41,8 @@
     <string name="main_how_to_link">Ayrıntılı talimatlar ntfy.sh adrsimde ve belgelerde bulunabilir.</string>
     <string name="main_unified_push_toast">Bu abonelik, %1$s tarafından UnifiedPush aracılığıyla yönetiliyor</string>
     <string name="main_banner_battery_text">Bildirim teslim sorunlarından kaçınmak için uygulama için pil iyileştirmesi kapalı olmalıdır.</string>
-    <string name="main_banner_json_stream_button_remind_later">Daha sonra sor</string>
-    <string name="main_banner_json_stream_button_dismiss">Kapat</string>
+    <string name="main_banner_websocket_button_remind_later">Daha sonra sor</string>
+    <string name="main_banner_websocket_button_dismiss">Kapat</string>
     <string name="add_dialog_title">Konuya abone ol</string>
     <string name="add_dialog_topic_name_hint">Konu adı, örn. benim_uyarilarim</string>
     <string name="add_dialog_use_another_server">Başka bir sunucu kullan</string>
@@ -209,8 +209,6 @@
     <string name="main_item_date_yesterday">dün</string>
     <string name="main_banner_battery_button_dismiss">Kapat</string>
     <string name="main_banner_battery_button_fix_now">Şimdi düzelt</string>
-    <string name="main_banner_json_stream_text">Haziran 2022\'den sonra kendi barındırdığınız ntfy sunucunuzla iletişim kurabildiğinizden emin olmak için şimdi Ayarlar / \"Bağlantı protokolü\" bölümünde WebSockets\'e geçin.</string>
-    <string name="main_banner_json_stream_button_learn_more">Daha fazla bilgi edin</string>
     <string name="detail_how_to_example">Örnek (curl kullanarak):<br/><tt>$ curl -d \"Merhaba\" %1$s</tt></string>
     <string name="detail_delete_dialog_permanently_delete">Kalıcı olarak sil</string>
     <string name="detail_test_message_error_unauthorized_user">Mesaj gönderilemiyor: \"%1$s\" kullanıcısı yetkilendirilmedi.</string>
@@ -319,4 +317,6 @@
     <string name="detail_settings_appearance_icon_remove_title">Abonelik simgesi (kaldırmak için dokunun)</string>
     <string name="detail_settings_appearance_icon_remove_summary">Bu konu için bildirimlerde görüntülenen simge</string>
     <string name="detail_settings_global_setting_title">Genel ayarı kullan</string>
-</resources>
\ No newline at end of file
+    <string name="add_dialog_base_urls_dropdown_clear">Hizmet URL\'sini temizle</string>
+    <string name="add_dialog_base_urls_dropdown_choose">Hizmet URL\'sini seç</string>
+</resources>
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index e3fb3b3..b569d48 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -47,10 +47,8 @@
     <string name="add_dialog_login_error_not_authorized">登录失败。 %1$s用户无权访问。</string>
     <string name="add_dialog_login_new_user">新用户</string>
     <string name="detail_no_notifications_text">当前还没有关于此主题的通知。</string>
-    <string name="main_banner_json_stream_text">请在“链接协议”中选择 WebSockets 以保证在 2022 年 6 月之后仍能收到来自自建 ntfy 服务器的推送。</string>
-    <string name="main_banner_json_stream_button_remind_later">稍后再问</string>
-    <string name="main_banner_json_stream_button_dismiss">暂时不管</string>
-    <string name="main_banner_json_stream_button_learn_more">详情</string>
+    <string name="main_banner_websocket_button_remind_later">稍后再问</string>
+    <string name="main_banner_websocket_button_dismiss">暂时不管</string>
     <string name="add_dialog_title">订阅主题</string>
     <string name="add_dialog_description_below">主题未必有密码保护,因此请选择难以猜测的名称。订阅之后,您可以通知 PUT/POST 请求发送通知。</string>
     <string name="add_dialog_topic_name_hint">主题名称,例如:phils_alerts</string>
@@ -319,4 +317,4 @@
     <string name="detail_settings_appearance_icon_error_saving">无法保存图标:%1$s</string>
     <string name="detail_settings_global_setting_title">使用全局设置</string>
     <string name="detail_settings_global_setting_suffix">使用全局设置</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 54993a1..10cb1a3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,6 @@
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools"
+           tools:ignore="MissingTranslation">
+
     <!-- Notification channels -->
     <string name="channel_notifications_min_name">Notifications (min priority)</string>
     <string name="channel_notifications_low_name">Notifications (low priority)</string>
@@ -72,11 +74,11 @@
     <string name="main_banner_battery_button_dismiss">Dismiss</string>
     <string name="main_banner_battery_button_fix_now">Fix now</string>
 
-    <!-- Main activity: JSON stream banner -->
-    <string name="main_banner_json_stream_text">Switch to WebSockets in Settings / \"Connection protocol\" now to ensure you can communicate with your selfhosted ntfy server after June 2022.</string>
-    <string name="main_banner_json_stream_button_remind_later">Ask later</string>
-    <string name="main_banner_json_stream_button_dismiss">Dismiss</string>
-    <string name="main_banner_json_stream_button_learn_more">Learn more</string>
+    <!-- Main activity: WebSocket banner -->
+    <string name="main_banner_websocket_text">Switching to WebSockets is the recommended way to connect to your server, and could improve battery life, but may require <a href="https://ntfy.sh/docs/config/#nginxapache2caddy">additional config in your proxy</a>. This can be toggled in the Settings.</string>
+    <string name="main_banner_websocket_button_remind_later">Ask later</string>
+    <string name="main_banner_websocket_button_dismiss">Dismiss</string>
+    <string name="main_banner_websocket_button_enable_now">Enable now</string>
 
     <!-- Add dialog -->
     <string name="add_dialog_title">Subscribe to topic</string>
@@ -101,6 +103,8 @@
     <string name="add_dialog_login_password_hint">Password</string>
     <string name="add_dialog_login_error_not_authorized">Login failed. User %1$s not authorized.</string>
     <string name="add_dialog_login_new_user">New user</string>
+    <string name="add_dialog_base_urls_dropdown_choose">Choose service URL</string>
+    <string name="add_dialog_base_urls_dropdown_clear">Clear service URL</string>
 
     <!-- Detail activity -->
     <string name="detail_no_notifications_text">You haven\'t received any notifications for this topic yet.</string>
@@ -330,8 +334,8 @@
     <string name="settings_advanced_clear_logs_summary">Delete previously recorded logs, and start over</string>
     <string name="settings_advanced_clear_logs_deleted_toast">Logs deleted</string>
     <string name="settings_advanced_connection_protocol_title">Connection protocol</string>
-    <string name="settings_advanced_connection_protocol_summary_jsonhttp">Use a JSON stream over HTTP to connect to the server. This method is deprecated and will be removed in June 2022.</string>
-    <string name="settings_advanced_connection_protocol_summary_ws">Use WebSockets to connect to the server. This will become the default in June 2022.</string>
+    <string name="settings_advanced_connection_protocol_summary_jsonhttp">Use a JSON stream over HTTP to connect to the server. This method is battle-tested, but may consume more battery.</string>
+    <string name="settings_advanced_connection_protocol_summary_ws">Use WebSockets to connect to the server. This is the recommended method, but may require additional config in your proxy.</string>
     <string name="settings_advanced_connection_protocol_entry_jsonhttp">JSON stream over HTTP</string>
     <string name="settings_advanced_connection_protocol_entry_ws">WebSockets</string>
     <string name="settings_about_header">About</string>
diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml
index cff7e5d..e199f8d 100644
--- a/app/src/main/res/values/values.xml
+++ b/app/src/main/res/values/values.xml
@@ -10,7 +10,6 @@
     <!-- Main activity -->
     <string name="main_menu_report_bug_url" translatable="false">https://github.com/binwiederhier/ntfy/issues</string>
     <string name="main_menu_docs_url" translatable="false">https://ntfy.sh/docs</string>
-    <string name="main_banner_json_stream_button_learn_more_url" translatable="false">https://ntfy.sh/docs/deprecations</string>
 
     <!-- Settings constants -->
     <string name="settings_notifications_muted_until_key" translatable="false">MutedUntil</string>
diff --git a/fastlane/metadata/android/en-US/changelog/28.txt b/fastlane/metadata/android/en-US/changelog/28.txt
index 42000e0..fa1e1af 100644
--- a/fastlane/metadata/android/en-US/changelog/28.txt
+++ b/fastlane/metadata/android/en-US/changelog/28.txt
@@ -1,6 +1,11 @@
+Features:
+* Polling is now done with since=<id> API, which makes deduping easier (#165)
+* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
+
 Bugs:
-* Fixed: Long-click selecting of notifications scrolls to the top (#235, thanks to @wunter8)
+* Long-click selecting of notifications doesn't scoll to the top anymore (#235, thanks to @wunter8)
 * Add attachment and click URL extras to MESSAGE_RECEIVED broadcast (#329, thanks to @wunter8)
+* Accessibility: Clear/choose service URL button in base URL dropdown now has a label (#292, thanks to @mhameed for reporting)
 
 Additional translations:
 * Italian (thanks to @Genio2003)