1
0
Fork 0

Merge branch 'main' into 236-android-action-buttons

This commit is contained in:
Philipp Heckel 2022-06-23 19:10:12 -04:00
commit c6c525ca3d
34 changed files with 530 additions and 175 deletions
app
fastlane/metadata/android/en-US/changelog

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ package io.heckel.ntfy.service
interface Connection {
fun start()
fun close()
fun since(): Long
fun since(): String?
}
data class ConnectionId(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
<string name="add_dialog_base_urls_dropdown_choose">Изберете адрес на услугата</string>
<string name="add_dialog_base_urls_dropdown_clear">Изчистване на адреса на услугата</string>
</resources>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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