diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts
index 8168c92bf8..09489a023a 100644
--- a/apps/multiplatform/android/build.gradle.kts
+++ b/apps/multiplatform/android/build.gradle.kts
@@ -148,6 +148,11 @@ dependencies {
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
+ // Push notifications
+ implementation("org.unifiedpush.android:connector:3.0.10")
+ // Allow using Play Services if available
+ implementation("org.unifiedpush.android:embedded-fcm-distributor:3.0.0")
+
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml
index d6059896a5..b226d2709f 100644
--- a/apps/multiplatform/android/src/main/AndroidManifest.xml
+++ b/apps/multiplatform/android/src/main/AndroidManifest.xml
@@ -186,6 +186,13 @@
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
/>
+
+
+
+
+
+
durationMs) {
Log.d(TAG, "Worker: work is done")
break
}
- delay(5000)
+ delay(durationMs - lastMsgMs + 200L)
}
}
} catch (_: TimeoutCancellationException) { // When timeout happens
- Log.d(TAG, "Worker: work is done (took $durationSeconds sec)")
+ Log.d(TAG, "Worker: Still work to do, restarting a new work (took $durationMs sec)")
+ // We reschedule a new work now (in 200 ms)
+ shouldReschedule = true
+ initialDelayMs = 200
} catch (_: CancellationException) { // When user opens the app while the worker is still working
Log.d(TAG, "Worker: interrupted")
} catch (e: Exception) {
Log.d(TAG, "Worker: unexpected exception: ${e.stackTraceToString()}")
}
- if (shouldReschedule) reschedule()
+ if (shouldReschedule) {
+ reschedule(
+ initialDelayMs = initialDelayMs,
+ intervalMs = intervalMs,
+ timeoutMs = timeoutMs,
+ durationMs = durationMs
+ )
+ }
return Result.success()
}
- private fun reschedule() = MessagesFetcherWorker.scheduleWork()
+ /**
+ * Reschedule a work if [initialDelayMs] > 0
+ */
+ private fun reschedule(
+ initialDelayMs: Int,
+ intervalMs: Int,
+ timeoutMs: Int,
+ durationMs: Int
+ ) = {
+ if (initialDelayMs > 0)
+ MessagesFetcherWorker.scheduleWork(initialDelayMs, intervalMs, timeoutMs, durationMs)
+ }
}
diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/PushService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/PushService.kt
new file mode 100644
index 0000000000..37d3a72ad1
--- /dev/null
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/PushService.kt
@@ -0,0 +1,92 @@
+package chat.simplex.app
+
+import chat.simplex.common.model.CC
+import chat.simplex.common.platform.Log
+import chat.simplex.common.platform.chatModel
+import chat.simplex.common.platform.ntfManager
+import chat.simplex.common.views.helpers.generalGetString
+import chat.simplex.res.MR
+import kotlinx.coroutines.*
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import org.unifiedpush.android.connector.FailedReason
+import org.unifiedpush.android.connector.PushService
+import org.unifiedpush.android.connector.data.PushEndpoint
+import org.unifiedpush.android.connector.data.PushMessage
+
+class PushService: PushService() {
+ private val json = Json {
+ ignoreUnknownKeys = true
+ }
+
+ @Serializable
+ data class PNMessage(
+ val verification: String? = null
+ )
+
+ private fun onVerification(code: String) {
+ CoroutineScope(Dispatchers.Default).launch {
+ chatModel.controller.sendCmd(
+ null,
+ CC.APIVerifySavedNtf(code),
+ log = true
+ )
+ }
+ }
+
+ override fun onMessage(message: PushMessage, instance: String) {
+ Log.d(TAG, "onMessage")
+ val pn: PNMessage = json.decodeFromString(String(message.content))
+ when {
+ pn.verification != null -> onVerification(pn.verification)
+ }
+ MessagesFetcherWorker.scheduleWork(200, 0)
+ }
+
+ override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
+ Log.d(TAG, "onNewEndpoint")
+ endpoint.pubKeySet ?: run {
+ // Should not happen
+ Log.w(TAG, "Missing pubKeySet")
+ return
+ }
+ CoroutineScope(Dispatchers.Default).launch {
+ chatModel.controller.sendCmd(
+ null,
+ CC.APIRegisterWebPush(endpoint.url, endpoint.pubKeySet!!.auth, endpoint.pubKeySet!!.pubKey),
+ log = true
+ )
+ }
+ }
+
+ override fun onRegistrationFailed(reason: FailedReason, instance: String) {
+ Log.d(TAG, "onRegistrationFailed: $reason")
+ val title = generalGetString(MR.strings.icon_descr_instant_notifications)
+ val text = when (reason) {
+ FailedReason.NETWORK -> generalGetString(MR.strings.unifiedpush_registration_failed_network)
+ FailedReason.VAPID_REQUIRED, // Should not happen, VAPID will be required
+ FailedReason.INTERNAL_ERROR -> generalGetString(MR.strings.unifiedpush_registration_failed_unknown)
+ FailedReason.ACTION_REQUIRED -> generalGetString(MR.strings.unifiedpush_registration_failed_action)
+
+ }
+ ntfManager.showMessage(title, text)
+ }
+
+ override fun onUnregistered(instance: String) {
+ Log.d(TAG, "onUnregistered")
+ val title = generalGetString(MR.strings.icon_descr_instant_notifications)
+ val text = generalGetString(MR.strings.unifiedpush_unregistered)
+ ntfManager.showMessage(title, text)
+ CoroutineScope(Dispatchers.Default).launch {
+ chatModel.controller.sendCmd(
+ null,
+ CC.APIDeleteSavedNtf(),
+ log = true
+ )
+ }
+ }
+
+ companion object {
+ private const val TAG = "PushService"
+ }
+}
\ No newline at end of file
diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
index 83767f90d7..7b32efbb2d 100644
--- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt
@@ -20,6 +20,7 @@ import androidx.work.*
import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
+import chat.simplex.app.platform.PushManager
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
@@ -237,6 +238,16 @@ class SimplexApp: Application(), LifecycleEventObserver {
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
+ if (mode == NotificationsMode.INSTANT) {
+ CoroutineScope(Dispatchers.Default).launch {
+ SimplexService.initUnifiedPush(this) {
+ // Change notifications mode only if everything is correctly setup
+ chatModel.controller.appPrefs.notificationsMode.set(mode)
+ }
+ }
+ } else {
+ chatModel.controller.appPrefs.notificationsMode.set(mode)
+ }
SimplexService.showBackgroundServiceNoticeIfNeeded(showOffAlert = false)
}
@@ -246,6 +257,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
NotificationsMode.SERVICE -> CoroutineScope(Dispatchers.Default).launch { platform.androidServiceStart() }
NotificationsMode.PERIODIC -> SimplexApp.context.schedulePeriodicWakeUp()
NotificationsMode.OFF -> {}
+ NotificationsMode.INSTANT -> {}
}
}
@@ -259,10 +271,15 @@ class SimplexApp: Application(), LifecycleEventObserver {
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
- if (appPrefs.notificationsMode.get() == NotificationsMode.SERVICE)
- withBGApi {
+ when (appPrefs.notificationsMode.get()) {
+ NotificationsMode.SERVICE -> withBGApi {
platform.androidServiceStart()
}
+ NotificationsMode.INSTANT -> withBGApi {
+ PushManager.initStart(context)
+ }
+ else -> {}
+ }
}
}
@@ -370,6 +387,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
+ override val supportsPushNotifications = true
}
}
diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt
index 289ecc0a31..0b78d8a57e 100644
--- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt
@@ -22,6 +22,7 @@ import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.NtfManager
+import chat.simplex.app.platform.PushManager
import chat.simplex.common.AppLock
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
@@ -427,6 +428,10 @@ class SimplexService: Service() {
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
+ suspend fun initUnifiedPush(scope: CoroutineScope, onSuccess: () -> Unit) {
+ PushManager.initUnifiedPush(androidAppContext, scope, onSuccess)
+ }
+
fun showBackgroundServiceNoticeIfNeeded(showOffAlert: Boolean = true) {
val appPrefs = ChatController.appPrefs
val mode = appPrefs.notificationsMode.get()
diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt
index 5d8371708c..fda263063b 100644
--- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt
@@ -230,6 +230,9 @@ object NtfManager {
val builder = NotificationCompat.Builder(context, MessageChannel)
.setContentTitle(title)
.setContentText(text)
+ .setStyle(
+ NotificationCompat.BigTextStyle().bigText(text)
+ )
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/platform/PushManager.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/platform/PushManager.kt
new file mode 100644
index 0000000000..7d0a573489
--- /dev/null
+++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/platform/PushManager.kt
@@ -0,0 +1,341 @@
+package chat.simplex.app.platform
+
+import SectionItemView
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.*
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import chat.simplex.common.model.*
+import chat.simplex.common.model.ChatController.getUserServers
+import chat.simplex.common.platform.Log
+import chat.simplex.common.platform.chatModel
+import chat.simplex.common.views.helpers.*
+import chat.simplex.common.views.usersettings.networkAndServers.showAddServerDialog
+import chat.simplex.res.MR
+import dev.icerock.moko.resources.compose.painterResource
+import dev.icerock.moko.resources.compose.stringResource
+import kotlinx.coroutines.*
+import org.unifiedpush.android.connector.UnifiedPush
+import androidx.core.net.toUri
+
+/**
+ * Object with functions to interact with push services
+ */
+object PushManager {
+ private const val TAG = "PushManager"
+ private val VAPID_REGEX = Regex("^[A-Za-z0-9_-]{87}(=+)?$")
+
+ /**
+ * Check if a NTF server is setup and ask user's push distributor
+ *
+ * Show an alert if NTF server isn't define,
+ * If a single distrib is available, use it
+ * If many, ask distributor to use with a dialog
+ * Else alert about missing service
+ */
+ suspend fun initUnifiedPush(context: Context, scope: CoroutineScope, onSuccess: () -> Unit) {
+ val rh = chatModel.remoteHostId()
+ val userServers = getUserServers(rh) ?: listOf()
+ val userNtfServer = userServers.userNtfServer()
+ ?: run {
+ Log.d(TAG, "User doesn't have any NTF server")
+ showMissingNTFDialog(scope, rh, userServers)
+ // After coming back from the server view, users will have to click on "Instant" again
+ return
+ }
+ val vapid = userNtfServer.vapid()
+ ?: run {
+ Log.d(TAG, "User NTF Server doesn't have any VAPID FP")
+ showNTFNoVAPIDDialog(scope, rh, userServers)
+ // After coming back from the server view, users will have to click on "Instant" again
+ return
+ }
+ val distributors = UnifiedPush.getDistributors(context)
+ when (distributors.size) {
+ 0 -> {
+ Log.d(TAG, "No distributor found")
+ showMissingPushServiceDialog()
+ }
+ 1 -> {
+ Log.d(TAG, "Found only one distributor installed")
+ UnifiedPush.saveDistributor(context, distributors.first())
+ register(context, vapid)
+ onSuccess()
+ }
+ else -> {
+ Log.d(TAG, "Found many distributors installed")
+ showSelectPushServiceIntroDialog {
+ showSelectPushServiceDialog(context, distributors) {
+ UnifiedPush.saveDistributor(context, it)
+ register(context, vapid)
+ onSuccess()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Register to UnifiedPush distributor if any is already used
+ *
+ * To run when the app starts; call [chat.simplex.app.PushService.onUnregistered]
+ * if the distributor is uninstalled
+ */
+ suspend fun initStart(context: Context) {
+ Log.d(TAG, "Init UnifiedPush during app startup")
+ //TODO: limit to once a day to reduce registrations to ntf server ?
+ UnifiedPush.getAckDistributor(context)?.let {
+ val vapid = getUserServers(chatModel.remoteHostId())
+ ?.userNtfServer()
+ ?.vapid()
+ // Unregister if the user deleted the ntf server or change it with one without vapid
+ ?: return UnifiedPush.unregister(context)
+ register(context, vapid)
+ }
+ }
+
+ private fun register(context: Context, vapid: String) {
+ UnifiedPush.register(context, vapid = vapid)
+ }
+
+ /**
+ * Get user NTF server
+ */
+ private fun List.userNtfServer(): UserServer? {
+ return this.firstNotNullOfOrNull {
+ it.ntfServers.firstOrNull { s -> s.enabled }
+ }
+ }
+
+ /**
+ * Get vapid fingerprint from the URI
+ */
+ private fun UserServer.vapid(): String ? = server
+ .toUri()
+ .getQueryParameter("vapid")
+ ?.takeIf { VAPID_REGEX.matches(it) }
+
+ /**
+ * Show a dialog to inform about missing NTF server
+ */
+ private fun showMissingNTFDialog(scope: CoroutineScope, rh: Long?, userServers: List) = AlertManager.shared.showAlert {
+ AlertDialog(
+ onDismissRequest = AlertManager.shared::hideAlert,
+ title = {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Icon(
+ painterResource(MR.images.ic_warning),
+ contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
+ )
+ Text(
+ stringResource(MR.strings.icon_descr_instant_notifications),
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ text = {
+ Text(stringResource(MR.strings.warning_push_needs_ntf_server))
+ },
+ // Go to user's servers settings
+ confirmButton = {
+ TextButton(onClick = {
+ AlertManager.shared.hideAlert()
+ showAddServerDialog(scope, rh, userServers)
+ }) { Text(stringResource(MR.strings.smp_servers_add)) }
+ },
+ // Ignore
+ dismissButton = {
+ TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
+ },
+ shape = RoundedCornerShape(corner = CornerSize(25.dp))
+ )
+ }
+
+ /**
+ * Show a dialog to inform about missing VAPID with the NTF server
+ */
+ private fun showNTFNoVAPIDDialog(scope: CoroutineScope, rh: Long?, userServers: List) = AlertManager.shared.showAlert {
+ AlertDialog(
+ onDismissRequest = AlertManager.shared::hideAlert,
+ title = {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Icon(
+ painterResource(MR.images.ic_warning),
+ contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
+ )
+ Text(
+ stringResource(MR.strings.icon_descr_instant_notifications),
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ text = {
+ Text(stringResource(MR.strings.warning_push_needs_ntf_server_with_vapid))
+ },
+ // Go to user's servers settings
+ confirmButton = {
+ TextButton(onClick = {
+ AlertManager.shared.hideAlert()
+ showAddServerDialog(scope, rh, userServers)
+ }) { Text(stringResource(MR.strings.smp_servers_add)) }
+ },
+ // Ignore
+ dismissButton = {
+ TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
+ },
+ shape = RoundedCornerShape(corner = CornerSize(25.dp))
+ )
+ }
+ /**
+ * Dialog to add a server, manually or with a QR code
+ */
+ private fun showAddServerDialog(scope: CoroutineScope, rh: Long?, userServers: List) {
+ val userServersState = mutableStateOf(userServers)
+ val serverErrors = mutableStateOf(listOf())
+ showAddServerDialog(scope, userServersState, serverErrors, rh)
+ }
+
+ /**
+ * Show a dialog to inform about missing push service
+ */
+ private fun showMissingPushServiceDialog() = AlertManager.shared.showAlert {
+ AlertDialog(
+ onDismissRequest = AlertManager.shared::hideAlert,
+ title = {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Icon(
+ painterResource(MR.images.ic_warning),
+ contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
+ )
+ Text(
+ stringResource(MR.strings.icon_descr_instant_notifications),
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ text = {
+ Text(
+ buildAnnotatedString {
+ append(stringResource(MR.strings.warning_push_needs_push_service))
+ withLink(LinkAnnotation.Url(url = "https://unifiedpush.org")) {
+ withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
+ append("https://unifiedpush.org")
+ }
+ }
+ }
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.ok)) }
+ },
+ shape = RoundedCornerShape(corner = CornerSize(25.dp))
+ )
+ }
+
+ /**
+ * Show an intro to explain why they need to select a push service
+ */
+ private fun showSelectPushServiceIntroDialog(onConfirm: () -> Unit) = AlertManager.shared.showAlert {
+ AlertDialog(
+ onDismissRequest = AlertManager.shared::hideAlert,
+ title = {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Icon(
+ painterResource(MR.images.ic_notifications),
+ contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
+ )
+ Text(
+ stringResource(MR.strings.icon_descr_instant_notifications),
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ text = {
+ Text(stringResource(MR.strings.select_push_service_intro))
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ AlertManager.shared.hideAlert()
+ onConfirm()
+ }
+ ) { Text(stringResource(MR.strings.select_push_service)) }
+ },
+ dismissButton = {
+ TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
+ },
+ shape = RoundedCornerShape(corner = CornerSize(25.dp))
+ )
+ }
+
+
+ /**
+ * Show a dialog to select push service
+ *
+ * @param distributors List of installed distributors' packageName
+ * @param onSelected run when a distributor is selected, its param is the selected distrib packageName
+ */
+ private fun showSelectPushServiceDialog(context: Context, distributors: List, onSelected: (String) -> Unit) = AlertManager.shared.showAlert {
+ AlertDialog(
+ onDismissRequest = AlertManager.shared::hideAlert,
+ title = {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Icon(
+ painterResource(MR.images.ic_notifications),
+ contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
+ )
+ Text(
+ stringResource(MR.strings.select_push_service),
+ fontWeight = FontWeight.Bold
+ )
+ }
+ },
+ text = {
+ Column {
+ distributors.map {
+ if (it == context.packageName) it to "Play Services"
+ else it to context.getApplicationName(it)
+ }.forEach {
+ SectionItemView({
+ AlertManager.shared.hideAlert()
+ onSelected(it.first)
+ }) {
+ Text(it.second)
+ }
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
+ },
+ shape = RoundedCornerShape(corner = CornerSize(25.dp))
+ )
+ }
+
+ private fun Context.getApplicationName(applicationId: String): String {
+ return try {
+ val ai = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getApplicationInfo(
+ applicationId,
+ PackageManager.ApplicationInfoFlags.of(
+ PackageManager.GET_META_DATA.toLong()
+ )
+ )
+ } else {
+ packageManager.getApplicationInfo(applicationId, 0)
+ }
+ packageManager.getApplicationLabel(ai)
+ } catch (e: PackageManager.NameNotFoundException) {
+ applicationId
+ } as String
+ }
+
+}
\ No newline at end of file
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt
index 540533e5ad..6ddf0c35ba 100644
--- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt
@@ -9,7 +9,7 @@ val NotificationsMode.requiresIgnoringBatterySinceSdk: Int get() = when(this) {
NotificationsMode.OFF -> Int.MAX_VALUE
NotificationsMode.PERIODIC -> Build.VERSION_CODES.M
NotificationsMode.SERVICE -> Build.VERSION_CODES.S
- /*INSTANT -> Int.MAX_VALUE - for Firebase notifications */
+ NotificationsMode.INSTANT -> Int.MAX_VALUE
}
val NotificationsMode.requiresIgnoringBattery
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 1b0714dd9d..61d2734979 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -3693,6 +3693,9 @@ sealed class CC {
class ResetAgentServersStats(): CC()
class GetAgentSubsTotal(val userId: Long): CC()
class GetAgentServersSummary(val userId: Long): CC()
+ class APIRegisterWebPush(val endpoint: String, val auth: String, val p256dh: String): CC()
+ class APIVerifySavedNtf(val code: String): CC()
+ class APIDeleteSavedNtf(): CC()
val cmdString: String get() = when (this) {
is Console -> cmd
@@ -3895,6 +3898,9 @@ sealed class CC {
is ResetAgentServersStats -> "/reset servers stats"
is GetAgentSubsTotal -> "/get subs total $userId"
is GetAgentServersSummary -> "/get servers summary $userId"
+ is APIRegisterWebPush -> "/_ntf register webpush $endpoint $auth $p256dh INSTANT"
+ is APIDeleteSavedNtf -> "/_ntf delete saved"
+ is APIVerifySavedNtf -> "/_ntf verify $code"
}
val cmdType: String get() = when (this) {
@@ -4063,6 +4069,9 @@ sealed class CC {
is ResetAgentServersStats -> "resetAgentServersStats"
is GetAgentSubsTotal -> "getAgentSubsTotal"
is GetAgentServersSummary -> "getAgentServersSummary"
+ is APIRegisterWebPush -> "apiRegisterWebPush"
+ is APIDeleteSavedNtf -> "apiDeleteSavedNtf"
+ is APIVerifySavedNtf -> "apiVerifySavedNtf"
}
data class ItemRange(val from: Long, val to: Long)
@@ -4163,6 +4172,7 @@ class DBEncryptionConfig(val currentKey: String, val newKey: String)
@Serializable
enum class ServerProtocol {
+ @SerialName("ntf") NTF,
@SerialName("smp") SMP,
@SerialName("xftp") XFTP;
}
@@ -4280,6 +4290,12 @@ data class ServerOperator(
val serverDomains: List,
val conditionsAcceptance: ConditionsAcceptance,
val enabled: Boolean,
+ /**
+ * Roles for ntf servers
+ *
+ * default to (true, true), for transition from old ServerOperator
+ */
+ val ntfRoles: ServerRoles = ServerRoles(true, true),
val smpRoles: ServerRoles,
val xftpRoles: ServerRoles,
) {
@@ -4301,6 +4317,7 @@ data class ServerOperator(
serverDomains = listOf("simplex.im"),
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false),
enabled = true,
+ ntfRoles = ServerRoles(storage = true, proxy = true),
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
)
@@ -4318,6 +4335,7 @@ data class ServerOperator(
other.serverDomains == this.serverDomains &&
other.conditionsAcceptance == this.conditionsAcceptance &&
other.enabled == this.enabled &&
+ other.ntfRoles == this.ntfRoles &&
other.smpRoles == this.smpRoles &&
other.xftpRoles == this.xftpRoles
}
@@ -4330,6 +4348,7 @@ data class ServerOperator(
result = 31 * result + serverDomains.hashCode()
result = 31 * result + conditionsAcceptance.hashCode()
result = 31 * result + enabled.hashCode()
+ result = 31 * result + ntfRoles.hashCode()
result = 31 * result + smpRoles.hashCode()
result = 31 * result + xftpRoles.hashCode()
return result
@@ -4368,6 +4387,7 @@ data class ServerRoles(
@Serializable
data class UserOperatorServers(
val operator: ServerOperator?,
+ val ntfServers: List = listOf(),
val smpServers: List,
val xftpServers: List
) {
@@ -4383,6 +4403,7 @@ data class UserOperatorServers(
serverDomains = emptyList(),
conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false),
enabled = false,
+ ntfRoles = ServerRoles(storage = true, proxy = true),
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
)
@@ -4390,12 +4411,14 @@ data class UserOperatorServers(
companion object {
val sampleData1 = UserOperatorServers(
operator = ServerOperator.sampleData1,
+ ntfServers = listOf(),
smpServers = listOf(UserServer.sampleData.preset),
xftpServers = listOf(UserServer.sampleData.xftpPreset)
)
val sampleDataNilOperator = UserOperatorServers(
operator = null,
+ ntfServers = listOf(),
smpServers = listOf(UserServer.sampleData.preset),
xftpServers = listOf(UserServer.sampleData.xftpPreset)
)
@@ -4411,6 +4434,7 @@ sealed class UserServersError {
val globalError: String?
get() = when (this.protocol_) {
+ ServerProtocol.NTF -> globalNTFError
ServerProtocol.SMP -> globalSMPError
ServerProtocol.XFTP -> globalXFTPError
}
@@ -4459,6 +4483,24 @@ sealed class UserServersError {
null
}
+ val globalNTFError: String?
+ get() = if (this.protocol_ == ServerProtocol.SMP) {
+ when (this) {
+ is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_notif_servers_configured)}" }
+ ?: generalGetString(MR.strings.no_notif_servers_configured)
+
+ is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_notif_servers_configured_for_receiving)}" }
+ ?: generalGetString(MR.strings.no_notif_servers_configured_for_receiving)
+
+ is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_notif_servers_configured_for_receiving)}" }
+ ?: generalGetString(MR.strings.no_notif_servers_configured_for_receiving)
+
+ else -> null
+ }
+ } else {
+ null
+ }
+
private fun userStr(user: UserRef): String {
return String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName)
}
@@ -7320,7 +7362,7 @@ sealed class AgentErrorType {
is CMD -> "CMD ${cmdErr.string} $errContext"
is CONN -> "CONN ${connErr.string} $errContext"
is SMP -> "SMP ${smpErr.string}"
- // is NTF -> "NTF ${ntfErr.string}"
+ is NTF -> "NTF ${ntfErr.string}"
is XFTP -> "XFTP ${xftpErr.string}"
is PROXY -> "PROXY $proxyServer $relayServer ${proxyErr.string}"
is RCP -> "RCP ${rcpErr.string}"
@@ -7333,7 +7375,7 @@ sealed class AgentErrorType {
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType()
- // @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
+ @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType()
@Serializable @SerialName("PROXY") class PROXY(val proxyServer: String, val relayServer: String, val proxyErr: ProxyClientError): AgentErrorType()
@Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType()
@@ -7691,7 +7733,7 @@ sealed class RemoteCtrlError {
}
enum class NotificationsMode() {
- OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */;
+ OFF, PERIODIC, SERVICE, INSTANT;
companion object {
val default: NotificationsMode = SERVICE
@@ -7918,6 +7960,7 @@ enum class AppSettingsNotificationMode {
companion object {
fun from(mode: NotificationsMode): AppSettingsNotificationMode =
when (mode) {
+ NotificationsMode.INSTANT -> INSTANT
NotificationsMode.SERVICE -> INSTANT
NotificationsMode.PERIODIC -> PERIODIC
NotificationsMode.OFF -> OFF
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
index 448100bc17..47d9b5a1ad 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt
@@ -33,6 +33,7 @@ interface PlatformInterface {
@Composable fun androidLockPortraitOrientation() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
@Composable fun desktopShowAppUpdateNotice() {}
+ val supportsPushNotifications: Boolean get() = false
}
/**
* Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them.
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt
index 2fc427cd2e..88a6dc3e4f 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt
@@ -133,6 +133,15 @@ private fun notificationModes(): List> {
AnnotatedString(generalGetString(MR.strings.notifications_mode_service_desc)),
)
)
+ if (platform.supportsPushNotifications) {
+ res.add(
+ ValueTitleDesc(
+ NotificationsMode.INSTANT,
+ generalGetString(MR.strings.notifications_mode_instant),
+ AnnotatedString(generalGetString(MR.strings.notifications_mode_instant_desc))
+ )
+ )
+ }
return res
}
@@ -164,6 +173,10 @@ fun notificationPreviewModes(): List> {
}
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
- chatModel.controller.appPrefs.notificationsMode.set(mode)
- platform.androidNotificationsModeChanged(mode)
+ // the preference is updated in androidNotificationsModeChanged for Android
+ if (appPlatform.isAndroid) {
+ platform.androidNotificationsModeChanged(mode)
+ } else {
+ chatModel.controller.appPrefs.notificationsMode.set(mode)
+ }
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt
index 26ecf151ff..8a6665e760 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt
@@ -932,6 +932,15 @@ fun globalXFTPServersError(serverErrors: List): String? {
return null
}
+fun globalNTFServersError(serverErrors: List): String? {
+ for (err in serverErrors) {
+ if (err.globalNTFError != null) {
+ return err.globalNTFError
+ }
+ }
+ return null
+}
+
fun findDuplicateHosts(serverErrors: List): Set {
val duplicateHostsList = serverErrors.mapNotNull { err ->
if (err is UserServersError.DuplicateServer) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt
index 1ec2534ab1..c2f659ba0f 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt
@@ -107,12 +107,15 @@ fun addServer(
val operatorServers = updatedUserServers[operatorIndex]
// Create a mutable copy of the smpServers or xftpServers and add the server
when (serverProtocol) {
+ ServerProtocol.NTF -> {
+ // We use a single ntf server
+ updatedUserServers[operatorIndex] = operatorServers.copy(ntfServers = listOf(server))
+ }
ServerProtocol.SMP -> {
val updatedSMPServers = operatorServers.smpServers.toMutableList()
updatedSMPServers.add(server)
updatedUserServers[operatorIndex] = operatorServers.copy(smpServers = updatedSMPServers)
}
-
ServerProtocol.XFTP -> {
val updatedXFTPServers = operatorServers.xftpServers.toMutableList()
updatedXFTPServers.add(server)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt
index cc72387875..3220f66868 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt
@@ -101,27 +101,26 @@ fun navigateToProtocolView(
userServers = userServers,
serverErrors = serverErrors,
onDelete = {
- if (protocol == ServerProtocol.SMP) {
- deleteSMPServer(userServers, operatorIndex, serverIndex)
- } else {
- deleteXFTPServer(userServers, operatorIndex, serverIndex)
+ when (protocol) {
+ ServerProtocol.NTF -> deleteNTFServer(userServers, operatorIndex, serverIndex)
+ ServerProtocol.SMP -> deleteSMPServer(userServers, operatorIndex, serverIndex)
+ ServerProtocol.XFTP -> deleteXFTPServer(userServers, operatorIndex, serverIndex)
}
close()
},
onUpdate = { updatedServer ->
userServers.value = userServers.value.toMutableList().apply {
- this[operatorIndex] = this[operatorIndex].copy(
- smpServers = if (protocol == ServerProtocol.SMP) {
- this[operatorIndex].smpServers.toMutableList().apply {
- this[serverIndex] = updatedServer
- }
- } else this[operatorIndex].smpServers,
- xftpServers = if (protocol == ServerProtocol.XFTP) {
- this[operatorIndex].xftpServers.toMutableList().apply {
- this[serverIndex] = updatedServer
- }
- } else this[operatorIndex].xftpServers
- )
+ if (platform.supportsPushNotifications && protocol == ServerProtocol.NTF && updatedServer.enabled) {
+ // We keep a single ntf server, if the updatedServer is enabled, we disable all other ntf servers first
+ this.replaceAll { op ->
+ op.copy(ntfServers = op.ntfServers.map { server -> server.copy(enabled = false).also { s -> Log.d(TAG, "ntf: $s")} })
+ }
+ }
+ this[operatorIndex] = when (protocol) {
+ ServerProtocol.NTF -> this[operatorIndex].copy(ntfServers = this[operatorIndex].ntfServers.toMutableList().apply { this[serverIndex] = updatedServer })
+ ServerProtocol.SMP -> this[operatorIndex].copy(smpServers = this[operatorIndex].smpServers.toMutableList().apply { this[serverIndex] = updatedServer })
+ ServerProtocol.XFTP -> this[operatorIndex].copy(xftpServers = this[operatorIndex].xftpServers.toMutableList().apply { this[serverIndex] = updatedServer })
+ }
}
},
close = close,
@@ -387,14 +386,19 @@ fun OperatorViewLayout(
testing = testing,
smpServers = userServers.value[operatorIndex].smpServers,
xftpServers = userServers.value[operatorIndex].xftpServers,
+ ntfServers = userServers.value[operatorIndex].ntfServers,
) { p, l ->
when (p) {
+ ServerProtocol.NTF -> userServers.value = userServers.value.toMutableList().apply {
+ this[operatorIndex] = this[operatorIndex].copy(
+ ntfServers = l
+ )
+ }
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
-
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt
index 8626fd3143..fe0281c52c 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt
@@ -115,7 +115,11 @@ private fun ProtocolServerLayout(
onDelete: () -> Unit,
) {
ColumnWithScrollBar {
- AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.XFTP) MR.strings.xftp_server else MR.strings.smp_server))
+ AppBarTitle(stringResource(when (serverProtocol) {
+ ServerProtocol.NTF -> MR.strings.ntf_server
+ ServerProtocol.XFTP -> MR.strings.xftp_server
+ ServerProtocol.SMP -> MR.strings.smp_server
+ }))
if (server.value.preset) {
PresetServer(server, testing, testServer)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt
index 63bf8b1dc4..0c39bd372f 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt
@@ -148,13 +148,41 @@ fun YourServersViewLayout(
}
}
+ if (platform.supportsPushNotifications) {
+ if (userServers.value[operatorIndex].ntfServers.any { !it.deleted }) {
+ SectionDividerSpaced()
+ SectionView(generalGetString(MR.strings.notif_servers).uppercase()) {
+ userServers.value[operatorIndex].ntfServers.forEachIndexed { i, server ->
+ if (server.deleted) return@forEachIndexed
+ SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.NTF) }) {
+ ProtocolServerViewLink(
+ srv = server,
+ serverProtocol = ServerProtocol.NTF,
+ duplicateHosts = duplicateHosts
+ )
+ }
+ }
+ }
+ val ntfErr = globalNTFServersError(serverErrors.value)
+ if (ntfErr != null) {
+ SectionCustomFooter {
+ ServersErrorFooter(ntfErr)
+ }
+ } else {
+ SectionTextFooter(generalGetString(MR.strings.ntf_servers_per_user))
+ }
+ }
+ }
+
if (
+ userServers.value[operatorIndex].ntfServers.any { !it.deleted } && platform.supportsPushNotifications ||
userServers.value[operatorIndex].smpServers.any { !it.deleted } ||
userServers.value[operatorIndex].xftpServers.any { !it.deleted }
) {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
}
+
SectionView {
SettingsActionItem(
painterResource(MR.images.ic_add),
@@ -178,14 +206,19 @@ fun YourServersViewLayout(
testing = testing,
smpServers = userServers.value[operatorIndex].smpServers,
xftpServers = userServers.value[operatorIndex].xftpServers,
+ ntfServers = userServers.value[operatorIndex].ntfServers,
) { p, l ->
when (p) {
+ ServerProtocol.NTF -> userServers.value = userServers.value.toMutableList().apply {
+ this[operatorIndex] = this[operatorIndex].copy(
+ ntfServers = l
+ )
+ }
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
-
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
@@ -204,16 +237,17 @@ fun YourServersViewLayout(
fun TestServersButton(
smpServers: List,
xftpServers: List,
+ ntfServers: List,
testing: MutableState,
onUpdate: (ServerProtocol, List) -> Unit
) {
val scope = rememberCoroutineScope()
- val disabled = derivedStateOf { (smpServers.none { it.enabled } && xftpServers.none { it.enabled }) || testing.value }
+ val disabled = derivedStateOf { (smpServers.none { it.enabled } && xftpServers.none { it.enabled } && ntfServers.none { it.enabled }) || testing.value }
SectionItemView(
{
scope.launch {
- testServers(testing, smpServers, xftpServers, chatModel, onUpdate)
+ testServers(testing, smpServers, xftpServers, ntfServers, chatModel, onUpdate)
}
},
disabled = disabled.value
@@ -303,6 +337,7 @@ private suspend fun testServers(
testing: MutableState,
smpServers: List,
xftpServers: List,
+ ntfServers: List,
m: ChatModel,
onUpdate: (ServerProtocol, List) -> Unit
) {
@@ -310,11 +345,14 @@ private suspend fun testServers(
onUpdate(ServerProtocol.SMP, smpResetStatus)
val xftpResetStatus = resetTestStatus(xftpServers)
onUpdate(ServerProtocol.XFTP, xftpResetStatus)
+ val ntfResetStatus = resetTestStatus(ntfServers)
+ onUpdate(ServerProtocol.NTF, ntfResetStatus)
testing.value = true
val smpFailures = runServersTest(smpResetStatus, m) { onUpdate(ServerProtocol.SMP, it) }
val xftpFailures = runServersTest(xftpResetStatus, m) { onUpdate(ServerProtocol.XFTP, it) }
+ val ntfFailures = runServersTest(ntfResetStatus, m) { onUpdate(ServerProtocol.NTF, it) }
testing.value = false
- val fs = smpFailures + xftpFailures
+ val fs = smpFailures + xftpFailures + ntfFailures
if (fs.isNotEmpty()) {
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
AlertManager.shared.showAlertMsg(
@@ -354,6 +392,32 @@ private suspend fun runServersTest(servers: List, m: ChatModel, onUp
return fs
}
+fun deleteNTFServer(
+ userServers: MutableState>,
+ operatorServersIndex: Int,
+ serverIndex: Int
+) {
+ val serverIsSaved = userServers.value[operatorServersIndex].ntfServers[serverIndex].serverId != null
+
+ if (serverIsSaved) {
+ userServers.value = userServers.value.toMutableList().apply {
+ this[operatorServersIndex] = this[operatorServersIndex].copy(
+ ntfServers = this[operatorServersIndex].ntfServers.toMutableList().apply {
+ this[serverIndex] = this[serverIndex].copy(deleted = true)
+ }
+ )
+ }
+ } else {
+ userServers.value = userServers.value.toMutableList().apply {
+ this[operatorServersIndex] = this[operatorServersIndex].copy(
+ ntfServers = this[operatorServersIndex].ntfServers.toMutableList().apply {
+ this.removeAt(serverIndex)
+ }
+ )
+ }
+ }
+}
+
fun deleteXFTPServer(
userServers: MutableState>,
operatorServersIndex: Int,
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
index b1a9e39157..e34f75787a 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -137,6 +137,8 @@
No media & file servers.
No servers to send files.
No servers to receive files.
+ No notification servers.
+ No servers to notify.
For chat profile %s:
Errors in servers configuration.
Error accepting conditions
@@ -270,9 +272,11 @@
Runs when app is open
Starts periodically
Always on
+ Instant
App can receive notifications only when it\'s running, no background service will be started
Checks new messages every 10 minutes for up to 1 minute
Background service is always running – notifications will be shown as soon as the messages are available.
+ Receive notifications instantly using an external push service.
Message text
Contact name
Hidden
@@ -284,6 +288,16 @@
New contact request
Connected
Error showing notification, contact developers.
+ You first need to set a NTF server to get push notifications.\n\nIt will send the notifications to your push provider.
+ Your NTF server doesn\'t support VAPID, which is required to get push notifications.
+ You don\'t have any push service installed on your device.\n\nPlease installed one and try again.\n\nFor more information, visit\
+ Your NTF server has been removed or has changed and doesn\'t support VAPID, which is required to get push notifications.
+ Select Push Service
+ Multiple push services are installed on your system, please select the service you wish to use.
+ SimpleX is no longer registered with your UnifiedPush distributor. The distributor may have been uninstalled or been logged out. You should reset your notifications settings.
+ Registration with your UnifiedPush distributor failed due to a network issue. Please try again when your network is back.
+ Registration with your UnifiedPush distributor failed due to a missing requirement from your distributor.
+ Registration with your UnifiedPush distributor failed due to an unknown issue. This could be a missing requirement from your distributor.
SimpleX Lock
@@ -949,6 +963,7 @@
The servers for new connections of your current chat profile
Save servers?
Media & file servers
+ Notifications servers
XFTP servers
Configured XFTP servers
Other XFTP servers
@@ -1995,6 +2010,7 @@
Use for files
To send
The servers for new files of your current chat profile
+ The servers for your notifications.
Added media & file servers
Open conditions
Open changes
@@ -2734,6 +2750,7 @@
Starting from %s.
SMP server
XFTP server
+ NTF server
Reconnect
attempts
Sent directly
diff --git a/cabal.project b/cabal.project
index 05457262b7..3eb7d516f8 100644
--- a/cabal.project
+++ b/cabal.project
@@ -11,8 +11,13 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
- location: https://github.com/simplex-chat/simplexmq.git
- tag: a2d35281b2e20022e75a4dcbecf16a7970d52e8f
+ location: https://codeberg.org/s1m/sxmq.git
+ tag: f28a2052ae88c59653993ca0e409231b5ed00b11
+
+-- source-repository-package
+-- type: git
+-- location: https://github.com/simplex-chat/simplexmq.git
+-- tag: d352d518c2b3a42bc7a298954dde799422e1457f
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index df4ed32f05..e4a0d027b2 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,6 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."a2d35281b2e20022e75a4dcbecf16a7970d52e8f" = "0sbq5mmsp8q4p527b286anbjidk2mlfdjsqrbiw90bc1rqwjwpm2";
+ "https://codeberg.org/s1m/sxmq.git"."f28a2052ae88c59653993ca0e409231b5ed00b11" = "86128d5cdb952e28460b4450042622807e0b683de5598817cac80e85ecee8c3f";
+ "https://github.com/simplex-chat/simplexmq.git"."d352d518c2b3a42bc7a298954dde799422e1457f" = "1rha84pfpaqx3mf218szkfra334vhijqf17hanxqmp1sicfbf1x3";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 9b9c5a9581..64eb72943d 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -255,6 +255,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests
Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri
Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type
+ Simplex.Chat.Store.SQLite.Migrations.M20250815_extras
other-modules:
Paths_simplex_chat
hs-source-dirs:
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index e14275b75c..d6292ec0b0 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -73,17 +73,20 @@ defaultChatConfig =
smp = simplexChatSMPServers,
useSMP = 4,
xftp = map (presetServer True) $ L.toList defaultXFTPServers,
- useXFTP = 3
+ useXFTP = 3,
+ ntf = map (presetServer True) $ L.toList _defaultNtfServers,
+ useNTF = 3
},
PresetOperator
{ operator = Just operatorFlux,
smp = fluxSMPServers,
useSMP = 3,
xftp = fluxXFTPServers,
- useXFTP = 3
+ useXFTP = 3,
+ ntf = fluxNTFServers,
+ useNTF = 3
}
],
- ntf = _defaultNtfServers,
netCfg = defaultNetworkConfig
},
-- please note: if these servers are changed, this option needs to be split to two,
@@ -126,7 +129,7 @@ newChatController
ChatDatabase {chatStore, agentStore}
user
cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations}
- ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
+ ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, ntfServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
backgroundMode = do
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
@@ -139,7 +142,8 @@ newChatController
opDomains = operatorDomains $ mapMaybe operatorWithId $ zip [1 ..] rndSrvs
agentSMP <- randomServerCfgs "agent SMP servers" SPSMP opDomains rndSrvs
agentXFTP <- randomServerCfgs "agent XFTP servers" SPXFTP opDomains rndSrvs
- let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP}
+ agentNTF <- randomServerCfgs "agent NTF servers" SPNTF opDomains rndSrvs
+ let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP, ntfServers = agentNTF}
currentRemoteHost <- newTVarIO Nothing
servers <- withTransaction chatStore $ \db -> agentServers db config randomPresetServers randomAgentServers
smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode
@@ -221,47 +225,56 @@ newChatController
where
PresetServers {operators, netCfg} = presetServers
netCfg' = updateNetworkConfig netCfg simpleNetCfg
- operators' = case (smpServers, xftpServers) of
- ([], []) -> operators
- (smpSrvs, []) -> L.map disableSMP operators <> [custom smpSrvs []]
- ([], xftpSrvs) -> L.map disableXFTP operators <> [custom [] xftpSrvs]
- (smpSrvs, xftpSrvs) -> [custom smpSrvs xftpSrvs]
+ operators' = case (smpServers, xftpServers, ntfServers) of
+ ([], [], []) -> operators
+ (smpSrvs, [], []) -> L.map disableSMP operators <> [custom smpSrvs [] []]
+ ([], xftpSrvs, []) -> L.map disableXFTP operators <> [custom [] xftpSrvs []]
+ (smpSrvs, xftpSrvs, []) -> L.map disableSMP operators <> L.map disableXFTP operators <> [custom smpSrvs xftpSrvs []]
+ ([], [], ntfSrvs) -> L.map disableNTF operators <> [custom [] [] ntfSrvs]
+ (smpSrvs, [], ntfSrvs) -> L.map disableSMP operators <> L.map disableNTF operators <> [custom smpSrvs [] ntfSrvs]
+ ([], xftpSrvs, ntfSrvs) -> L.map disableXFTP operators <> L.map disableNTF operators <> [custom [] xftpSrvs ntfSrvs]
+ (smpSrvs, xftpSrvs, ntfSrvs) -> [custom smpSrvs xftpSrvs ntfSrvs]
disableSMP op@PresetOperator {smp} = (op :: PresetOperator) {smp = map disableSrv smp}
disableXFTP op@PresetOperator {xftp} = (op :: PresetOperator) {xftp = map disableSrv xftp}
+ disableNTF op@PresetOperator {ntf} = (op :: PresetOperator) {ntf = map disableSrv ntf}
disableSrv :: forall p. NewUserServer p -> NewUserServer p
disableSrv srv = (srv :: NewUserServer p) {enabled = False}
- custom smpSrvs xftpSrvs =
+ custom smpSrvs xftpSrvs ntfSrvs =
PresetOperator
{ operator = Nothing,
smp = map newUserServer smpSrvs,
useSMP = 0,
xftp = map newUserServer xftpSrvs,
- useXFTP = 0
+ useXFTP = 0,
+ ntf = map newUserServer ntfSrvs,
+ useNTF = 0
}
randomServerCfgs :: UserProtocol p => String -> SProtocolType p -> [(Text, ServerOperator)] -> [PresetOperator] -> IO (NonEmpty (ServerCfg p))
randomServerCfgs name p opDomains rndSrvs =
toJustOrError name $ L.nonEmpty $ agentServerCfgs p opDomains $ concatMap (pServers p) rndSrvs
agentServers :: DB.Connection -> ChatConfig -> NonEmpty PresetOperator -> RandomAgentServers -> IO InitialAgentServers
- agentServers db ChatConfig {presetServers = PresetServers {ntf, netCfg}, presetDomains} presetOps as = do
+ agentServers db ChatConfig {presetServers = PresetServers {netCfg}, presetDomains} presetOps as = do
users <- getUsers db
ops <- getUpdateServerOperators db presetOps (null users)
let opDomains = operatorDomains $ mapMaybe snd ops
- (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users
- pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains}
+ (smp', xftp', ntf') <- unzip3 <$> mapM (getServers ops opDomains) users
+ pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf = M.fromList (optServers ntf' ntfServers), netCfg, presetDomains}
where
optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))]
optServers srvs overrides_ = case L.nonEmpty overrides_ of
Just overrides -> map (second $ const $ L.map (presetServerCfg True allRoles Nothing) overrides) srvs
Nothing -> srvs
- getServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [(Text, ServerOperator)] -> User -> IO ((UserId, NonEmpty (ServerCfg 'PSMP)), (UserId, NonEmpty (ServerCfg 'PXFTP)))
+ getServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [(Text, ServerOperator)] -> User -> IO ((UserId, NonEmpty (ServerCfg 'PSMP)), (UserId, NonEmpty (ServerCfg 'PXFTP)), (UserId, NonEmpty (ServerCfg 'PNTF)))
getServers ops opDomains user' = do
smpSrvs <- getProtocolServers db SPSMP user'
xftpSrvs <- getProtocolServers db SPXFTP user'
- uss <- groupByOperator' (ops, smpSrvs, xftpSrvs)
+ ntfSrvs <- getProtocolServers db SPNTF user'
+ uss <- groupByOperator' (ops, smpSrvs, xftpSrvs, ntfSrvs)
ts <- getCurrentTime
uss' <- mapM (setUserServers' db user' ts . updatedUserServers) uss
let auId = aUserId user'
- pure $ bimap (auId,) (auId,) $ useServers as opDomains uss'
+ pure $ auIdMap auId $ useServers as opDomains uss'
+ auIdMap auId (a, b, c) = ((auId, a), (auId, b), (auId, c))
chooseRandomServers :: PresetServers -> IO (NonEmpty PresetOperator)
chooseRandomServers PresetServers {operators} =
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index eb7ed53283..d58ec0e7a0 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -162,7 +162,8 @@ data ChatConfig = ChatConfig
data RandomAgentServers = RandomAgentServers
{ smpServers :: NonEmpty (ServerCfg 'PSMP),
- xftpServers :: NonEmpty (ServerCfg 'PXFTP)
+ xftpServers :: NonEmpty (ServerCfg 'PXFTP),
+ ntfServers :: NonEmpty (ServerCfg 'PNTF)
}
deriving (Show)
@@ -184,7 +185,6 @@ defaultChatHooks = ChatHooks Nothing Nothing Nothing
data PresetServers = PresetServers
{ operators :: NonEmpty PresetOperator,
- ntf :: [NtfServer],
netCfg :: NetworkConfig
}
deriving (Show)
@@ -354,8 +354,10 @@ data ChatCommand
| APIGetNtfToken
| APIRegisterToken DeviceToken NotificationsMode
| APIVerifyToken DeviceToken C.CbNonce ByteString
+ | APIVerifySavedToken ByteString
| APICheckToken DeviceToken
| APIDeleteToken DeviceToken
+ | APIDeleteSavedToken
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
| APIGetConnNtfMessages (NonEmpty ConnMsgReq)
| APIAddMember {groupId :: GroupId, contactId :: ContactId, memberRole :: GroupMemberRole}
diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs
index 4ae58d8a29..a181875448 100644
--- a/src/Simplex/Chat/Library/Commands.hs
+++ b/src/Simplex/Chat/Library/Commands.hs
@@ -103,7 +103,7 @@ import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
-import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
+import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol, NtfServerWithAuth)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
@@ -135,7 +135,7 @@ import Simplex.Messaging.Agent.Store.Common (withConnection)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
#endif
-_defaultNtfServers :: [NtfServer]
+_defaultNtfServers :: NonEmpty NtfServerWithAuth
_defaultNtfServers =
[ -- "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion"
"ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion",
@@ -267,11 +267,12 @@ updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredH
nt t = NetworkTimeout {backgroundTimeout = t * 3, interactiveTimeout = t}
in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPortServers, logTLSErrors}
-useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))
+useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP), NonEmpty (ServerCfg 'PNTF))
useServers as opDomains uss =
let smp' = useServerCfgs SPSMP as opDomains $ concatMap (servers' SPSMP) uss
xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss
- in (smp', xftp')
+ ntf' = useServerCfgs SPNTF as opDomains $ concatMap (servers' SPNTF) uss
+ in (smp', xftp', ntf')
execChatCommand :: Maybe RemoteHostId -> ByteString -> Int -> CM' (Either ChatError ChatResponse)
execChatCommand rh s retryNum =
@@ -318,8 +319,8 @@ processChatCommand vr nm = \case
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
when (n == displayName) . throwChatError $
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
- (uss, (smp', xftp')) <- chooseServers =<< readTVarIO u
- auId <- withAgent $ \a -> createUser a smp' xftp'
+ (uss, (smp', xftp', ntf')) <- chooseServers =<< readTVarIO u
+ auId <- withAgent $ \a -> createUser a smp' xftp' ntf'
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
user <- withFastStore $ \db -> do
user <- createUserRecordAt db (AgentUserId auId) p True ts
@@ -334,7 +335,7 @@ processChatCommand vr nm = \case
createPresetContactCards db user = do
createContact db user simplexStatusContactProfile
createContact db user simplexTeamContactProfile
- chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
+ chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP), NonEmpty (ServerCfg 'PNTF)))
chooseServers user_ = do
as <- asks randomAgentServers
mapM (withFastStore . flip getUserServers >=> liftIO . groupByOperator) user_ >>= \case
@@ -345,12 +346,12 @@ processChatCommand vr nm = \case
Nothing -> do
ps <- asks randomPresetServers
uss <- presetUserServers <$> withFastStore' (\db -> getUpdateServerOperators db ps True)
- let RandomAgentServers {smpServers = smp', xftpServers = xftp'} = as
- pure (uss, (smp', xftp'))
+ let RandomAgentServers {smpServers = smp', xftpServers = xftp', ntfServers = ntf'} = as
+ pure (uss, (smp', xftp', ntf'))
copyServers :: UserOperatorServers -> UpdatedUserOperatorServers
- copyServers UserOperatorServers {operator, smpServers, xftpServers} =
+ copyServers UserOperatorServers {operator, smpServers, xftpServers, ntfServers} =
let new srv = AUS SDBNew srv {serverId = DBNewEntity}
- in UpdatedUserOperatorServers {operator, smpServers = map new smpServers, xftpServers = map new xftpServers}
+ in UpdatedUserOperatorServers {operator, smpServers = map new smpServers, xftpServers = map new xftpServers, ntfServers = map new ntfServers}
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
day = 86400
ListUsers -> CRUsersList <$> withFastStore' getUsersInfo
@@ -1350,9 +1351,11 @@ processChatCommand vr nm = \case
APIRegisterToken token mode -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a nm token mode)
APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a nm token nonce code) >> ok_
+ APIVerifySavedToken code -> withUser $ \_u@User {userId} -> withAgent (\a -> verifySavedNtfToken a userId code) >> ok_
APICheckToken token -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (\a -> checkNtfToken a nm token)
APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_
+ APIDeleteSavedToken -> withUser $ \_ -> withAgent deleteSavedNtfToken >> ok_
APIGetNtfConns nonce encNtfInfo -> withUser $ \_ -> do
ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo
(errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos))
@@ -1400,16 +1403,18 @@ processChatCommand vr nm = \case
ops' = map Just ops <> [Nothing]
opDomains = operatorDomains ops
liftIO $ fmap (opsConds,) . mapM (getServers db as ops' opDomains) =<< getUsers db
- lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp')) -> do
+ lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp', ntf')) -> do
setProtocolServers a auId smp'
setProtocolServers a auId xftp'
+ setProtocolServers a auId ntf'
pure $ CRServerOperatorConditions opsConds
where
- getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
+ getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP), NonEmpty (ServerCfg 'PNTF)))
getServers db as ops opDomains user = do
smpSrvs <- getProtocolServers db SPSMP user
xftpSrvs <- getProtocolServers db SPXFTP user
- uss <- groupByOperator (ops, smpSrvs, xftpSrvs)
+ ntfSrvs <- getProtocolServers db SPNTF user
+ uss <- groupByOperator (ops, smpSrvs, xftpSrvs, ntfSrvs)
pure $ (aUserId user,) $ useServers as opDomains uss
SetServerOperators operatorsRoles -> do
ops <- serverOperators <$> withFastStore getServerOperators
@@ -1433,9 +1438,10 @@ processChatCommand vr nm = \case
lift $ withAgent' $ \a -> do
let auId = aUserId user
opDomains = operatorDomains $ mapMaybe operator' $ L.toList uss
- (smp', xftp') = useServers as opDomains uss
+ (smp', xftp', ntf') = useServers as opDomains uss
setProtocolServers a auId smp'
setProtocolServers a auId xftp'
+ setProtocolServers a auId ntf'
ok_
APIValidateServers userId userServers -> withUserId userId $ \user ->
CRUserServersValidation user <$> validateAllUsersServers userId userServers
@@ -2913,7 +2919,8 @@ processChatCommand vr nm = \case
users <- getUsers db
smpServers <- getServers db user SPSMP
xftpServers <- getServers db user SPXFTP
- let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers
+ ntfServers <- getServers db user SPNTF
+ let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers ntfServers
pure $ CRAgentServersSummary user presentedServersSummary
where
getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p]
@@ -3986,18 +3993,21 @@ data ConnectViaContactResult
= CVRConnectedContact Contact
| CVRSentInvitation Connection (Maybe Profile)
-protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP])
-protocolServers p (operators, smpServers, xftpServers) = case p of
- SPSMP -> (operators, smpServers, [])
- SPXFTP -> (operators, [], xftpServers)
+protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF])
+protocolServers p (operators, smpServers, xftpServers, ntfServers) = case p of
+ SPSMP -> (operators, smpServers, [], [])
+ SPXFTP -> (operators, [], xftpServers, [])
+ SPNTF -> (operators, [], [], ntfServers)
-- disable preset and replace custom servers (groupByOperator always adds custom)
updatedServers :: forall p. UserProtocol p => SProtocolType p -> [AUserServer p] -> UserOperatorServers -> UpdatedUserOperatorServers
-updatedServers p' srvs UserOperatorServers {operator, smpServers, xftpServers} = case p' of
- SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers)
- SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers)
+updatedServers p' srvs UserOperatorServers {operator, smpServers, xftpServers, ntfServers} = case p' of
+ SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers, map (AUS SDBStored) ntfServers)
+ SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers, map (AUS SDBStored) ntfServers)
+ SPNTF -> u (map (AUS SDBStored) smpServers, map (AUS SDBStored) xftpServers, updateSrvs ntfServers)
where
- u = uncurry $ UpdatedUserOperatorServers operator
+ u :: ([AUserServer 'PSMP], [AUserServer 'PXFTP], [AUserServer 'PNTF]) -> UpdatedUserOperatorServers
+ u (smp, xftp, ntf) = UpdatedUserOperatorServers operator smp xftp ntf
updateSrvs :: [UserServer p] -> [AUserServer p]
updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs (const []) operator
disableSrv srv@UserServer {preset} =
@@ -4502,8 +4512,10 @@ chatCommandP =
"/_ntf get" $> APIGetNtfToken,
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
"/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP),
+ "/_ntf verify " *> (APIVerifySavedToken <$> strP),
"/_ntf check " *> (APICheckToken <$> strP),
"/_ntf delete " *> (APIDeleteToken <$> strP),
+ "/_ntf delete saved" $> APIDeleteSavedToken,
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
"/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP),
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
@@ -4523,8 +4535,10 @@ chatCommandP =
"/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP),
"/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP),
"/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP),
+ "/ntf " *> (SetUserProtoServers (AProtocolType SPNTF) . map (AProtoServerWithAuth SPNTF) <$> protocolServersP),
"/smp" $> GetUserProtoServers (AProtocolType SPSMP),
"/xftp" $> GetUserProtoServers (AProtocolType SPXFTP),
+ "/ntf" $> GetUserProtoServers (AProtocolType SPNTF),
"/_operators" $> APIGetServerOperators,
"/_operators " *> (APISetServerOperators <$> jsonP),
"/operators " *> (SetServerOperators . L.fromList <$> operatorRolesP `A.sepBy1` A.char ','),
diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs
index ff60c6c32f..6421619a5a 100644
--- a/src/Simplex/Chat/Library/Internal.hs
+++ b/src/Simplex/Chat/Library/Internal.hs
@@ -151,13 +151,14 @@ withFileLock name = withEntityLock name . CLFile
{-# INLINE withFileLock #-}
useServerCfgs :: forall p. UserProtocol p => SProtocolType p -> RandomAgentServers -> [(Text, ServerOperator)] -> [UserServer p] -> NonEmpty (ServerCfg p)
-useServerCfgs p RandomAgentServers {smpServers, xftpServers} opDomains =
+useServerCfgs p RandomAgentServers {smpServers, xftpServers, ntfServers} opDomains =
fromMaybe (rndAgentServers p) . L.nonEmpty . agentServerCfgs p opDomains
where
rndAgentServers :: SProtocolType p -> NonEmpty (ServerCfg p)
rndAgentServers = \case
SPSMP -> smpServers
SPXFTP -> xftpServers
+ SPNTF -> ntfServers
contactCITimed :: Contact -> CM (Maybe CITimed)
contactCITimed ct = sndContactCITimed False ct Nothing
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index f18a3900da..6ca1be0ff3 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -245,6 +245,7 @@ mobileChatOpts dbOptions =
{ dbOptions,
smpServers = [],
xftpServers = [],
+ ntfServers = [],
simpleNetCfg = defaultSimpleNetCfg,
logLevel = CLLImportant,
logConnections = False,
diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs
index c9b5e020d9..683172790a 100644
--- a/src/Simplex/Chat/Operators.hs
+++ b/src/Simplex/Chat/Operators.hs
@@ -172,6 +172,7 @@ operatorRoles :: UserProtocol p => SProtocolType p -> ServerOperator -> ServerRo
operatorRoles p op = case p of
SPSMP -> smpRoles op
SPXFTP -> xftpRoles op
+ SPNTF -> ServerRoles{proxy = False, storage = False}
conditionsAccepted :: ServerOperator -> Bool
conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of
@@ -181,14 +182,16 @@ conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAccept
data UserOperatorServers = UserOperatorServers
{ operator :: Maybe ServerOperator,
smpServers :: [UserServer 'PSMP],
- xftpServers :: [UserServer 'PXFTP]
+ xftpServers :: [UserServer 'PXFTP],
+ ntfServers :: [UserServer 'PNTF]
}
deriving (Show)
data UpdatedUserOperatorServers = UpdatedUserOperatorServers
{ operator :: Maybe ServerOperator,
smpServers :: [AUserServer 'PSMP],
- xftpServers :: [AUserServer 'PXFTP]
+ xftpServers :: [AUserServer 'PXFTP],
+ ntfServers :: [AUserServer 'PNTF]
}
deriving (Show)
@@ -205,17 +208,19 @@ instance UserServersClass UserOperatorServers where
type AServer UserOperatorServers = UserServer' 'DBStored
operator' UserOperatorServers {operator} = operator
aUserServer' = AUS SDBStored
- servers' p UserOperatorServers {smpServers, xftpServers} = case p of
+ servers' p UserOperatorServers {smpServers, xftpServers, ntfServers} = case p of
SPSMP -> smpServers
SPXFTP -> xftpServers
+ SPNTF -> ntfServers
instance UserServersClass UpdatedUserOperatorServers where
type AServer UpdatedUserOperatorServers = AUserServer
operator' UpdatedUserOperatorServers {operator} = operator
aUserServer' = id
- servers' p UpdatedUserOperatorServers {smpServers, xftpServers} = case p of
+ servers' p UpdatedUserOperatorServers {smpServers, xftpServers, ntfServers} = case p of
SPSMP -> smpServers
SPXFTP -> xftpServers
+ SPNTF -> ntfServers
type UserServer p = UserServer' 'DBStored p
@@ -244,7 +249,9 @@ data PresetOperator = PresetOperator
smp :: [NewUserServer 'PSMP],
useSMP :: Int,
xftp :: [NewUserServer 'PXFTP],
- useXFTP :: Int
+ useXFTP :: Int,
+ ntf :: [NewUserServer 'PNTF],
+ useNTF :: Int
}
deriving (Show)
@@ -252,14 +259,16 @@ pOperator :: PresetOperator -> Maybe NewServerOperator
pOperator PresetOperator {operator} = operator
pServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p]
-pServers p PresetOperator {smp, xftp} = case p of
+pServers p PresetOperator {smp, xftp, ntf} = case p of
SPSMP -> smp
SPXFTP -> xftp
+ SPNTF -> ntf
operatorServersToUse :: UserProtocol p => SProtocolType p -> PresetOperator -> Int
-operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of
+operatorServersToUse p PresetOperator {useSMP, useXFTP, useNTF} = case p of
SPSMP -> useSMP
SPXFTP -> useXFTP
+ SPNTF -> useNTF
presetServer' :: Bool -> ProtocolServer p -> NewUserServer p
presetServer' enabled = presetServer enabled . (`ProtoServerWithAuth` Nothing)
@@ -301,8 +310,8 @@ usageConditionsToAdd' prevCommit sourceCommit newUser createdAt = \case
presetUserServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [UpdatedUserOperatorServers]
presetUserServers = mapMaybe $ \(presetOp_, op) -> mkUS op <$> presetOp_
where
- mkUS op PresetOperator {smp, xftp} =
- UpdatedUserOperatorServers op (map (AUS SDBNew) smp) (map (AUS SDBNew) xftp)
+ mkUS op PresetOperator {smp, xftp, ntf} =
+ UpdatedUserOperatorServers op (map (AUS SDBNew) smp) (map (AUS SDBNew) xftp) (map (AUS SDBNew) ntf)
-- This function should be used inside DB transaction to update operators.
-- It allows to add/remove/update preset operators in the database preserving enabled and roles settings,
@@ -322,13 +331,13 @@ updatedServerOperators presetOps storedOps =
-- This function should be used inside DB transaction to update servers.
updatedUserServers :: (Maybe PresetOperator, UserOperatorServers) -> UpdatedUserOperatorServers
-updatedUserServers (presetOp_, UserOperatorServers {operator, smpServers, xftpServers}) =
- UpdatedUserOperatorServers {operator, smpServers = smp', xftpServers = xftp'}
+updatedUserServers (presetOp_, UserOperatorServers {operator, smpServers, xftpServers, ntfServers}) =
+ UpdatedUserOperatorServers {operator, smpServers = smp', xftpServers = xftp', ntfServers = ntf'}
where
stored = map (AUS SDBStored)
- (smp', xftp') = case presetOp_ of
- Nothing -> (stored smpServers, stored xftpServers)
- Just presetOp -> (updated SPSMP smpServers, updated SPXFTP xftpServers)
+ (smp', xftp', ntf') = case presetOp_ of
+ Nothing -> (stored smpServers, stored xftpServers, stored ntfServers)
+ Just presetOp -> (updated SPSMP smpServers, updated SPXFTP xftpServers, updated SPNTF ntfServers)
where
updated :: forall p. UserProtocol p => SProtocolType p -> [UserServer p] -> [AUserServer p]
updated p srvs = map userServer presetSrvs <> stored (filter customServer srvs)
@@ -379,34 +388,36 @@ instance Box ((,) (Maybe a)) where
box = (Nothing,)
unbox = snd
-groupByOperator :: ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers]
-groupByOperator (ops, smpSrvs, xftpSrvs) = map runIdentity <$> groupByOperator_ (map Identity ops, smpSrvs, xftpSrvs)
+groupByOperator :: ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> IO [UserOperatorServers]
+groupByOperator (ops, smpSrvs, xftpSrvs, ntfSrvs) = map runIdentity <$> groupByOperator_ (map Identity ops, smpSrvs, xftpSrvs, ntfSrvs)
-- For the initial app start this function relies on tuple being Functor/Box
-- to preserve the information about operator being DBNew or DBStored
-groupByOperator' :: ([(Maybe PresetOperator, Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [(Maybe PresetOperator, UserOperatorServers)]
+groupByOperator' :: ([(Maybe PresetOperator, Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> IO [(Maybe PresetOperator, UserOperatorServers)]
groupByOperator' = groupByOperator_
{-# INLINE groupByOperator' #-}
-groupByOperator_ :: forall f. (Box f, Traversable f) => ([f (Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [f UserOperatorServers]
-groupByOperator_ (ops, smpSrvs, xftpSrvs) = do
+groupByOperator_ :: forall f. (Box f, Traversable f) => ([f (Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> IO [f UserOperatorServers]
+groupByOperator_ (ops, smpSrvs, xftpSrvs, ntfSrvs) = do
let ops' = mapMaybe sequence ops
customOp_ = find (isNothing . unbox) ops
ss <- mapM ((\op -> (serverDomains (unbox op),) <$> newIORef (mkUS . Just <$> op))) ops'
custom <- newIORef $ maybe (box $ mkUS Nothing) (mkUS <$>) customOp_
mapM_ (addServer ss custom addSMP) (reverse smpSrvs)
mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs)
+ mapM_ (addServer ss custom addNTF) (reverse ntfSrvs)
opSrvs <- mapM (readIORef . snd) ss
customSrvs <- readIORef custom
pure $ opSrvs <> [customSrvs]
where
- mkUS op = UserOperatorServers op [] []
+ mkUS op = UserOperatorServers op [] [] []
addServer :: [([Text], IORef (f UserOperatorServers))] -> IORef (f UserOperatorServers) -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO ()
addServer ss custom add srv =
let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss
in atomicModifyIORef'_ v (add srv <$>)
addSMP srv s@UserOperatorServers {smpServers} = (s :: UserOperatorServers) {smpServers = srv : smpServers}
addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers}
+ addNTF srv s@UserOperatorServers {ntfServers} = (s :: UserOperatorServers) {ntfServers = srv : ntfServers}
data UserServersError
= USENoServers {protocol :: AProtocolType, user :: Maybe User}
diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs
index d3d727ea05..3c1421733c 100644
--- a/src/Simplex/Chat/Operators/Presets.hs
+++ b/src/Simplex/Chat/Operators/Presets.hs
@@ -116,3 +116,6 @@ fluxXFTPServers =
"xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion",
"xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion"
]
+
+fluxNTFServers :: [NewUserServer 'PNTF]
+fluxNTFServers = []
diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs
index 9411c66175..959437c36a 100644
--- a/src/Simplex/Chat/Options.hs
+++ b/src/Simplex/Chat/Options.hs
@@ -33,7 +33,7 @@ import Simplex.FileTransfer.Description (mb)
import Simplex.Messaging.Client (HostMode (..), SMPWebPortServers (..), SocksMode (..), textToHostMode)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
-import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
+import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth, NtfServerWithAuth)
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth)
import Simplex.Chat.Options.DB
@@ -58,6 +58,7 @@ data CoreChatOpts = CoreChatOpts
{ dbOptions :: ChatDbOpts,
smpServers :: [SMPServerWithAuth],
xftpServers :: [XFTPServerWithAuth],
+ ntfServers :: [NtfServerWithAuth],
simpleNetCfg :: SimpleNetCfg,
logLevel :: ChatLogLevel,
logConnections :: Bool,
@@ -114,6 +115,18 @@ coreChatOptsP appDir defaultDbName = do
)
<> value []
)
+ ntfServers <-
+ option
+ parseProtocolServers
+ ( long "ntf-server"
+ <> metavar "SERVER"
+ <> help
+ ( ("Space-separated list of NTF server(s) to use (each server can have more than one hostname)." <> "\n")
+ <> ("If you pass multiple servers, surround the entire list in quotes." <> "\n")
+ <> "Examples: ntf1.example.com, \"ntf1.example.com ntf2.example.com ntf3.example.com\""
+ )
+ <> value []
+ )
socksProxy <-
flag' (Just defaultSocksProxyWithAuth) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
<|> option
@@ -248,6 +261,7 @@ coreChatOptsP appDir defaultDbName = do
{ dbOptions,
smpServers,
xftpServers,
+ ntfServers,
simpleNetCfg =
SimpleNetCfg
{ socksProxy,
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index 64fbbddda0..7f44d59d2e 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -114,7 +114,7 @@ import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON)
-import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode)
+import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, Extras)
import Simplex.Messaging.Agent.Store.Entity
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
@@ -589,15 +589,15 @@ getProtocolServers db p User {userId} =
<$> DB.query
db
[sql|
- SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled
+ SELECT smp_server_id, host, port, key_hash, extras, basic_auth, preset, tested, enabled
FROM protocol_servers
WHERE user_id = ? AND protocol = ?
|]
(userId, decodeLatin1 $ strEncode p)
where
- toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) -> UserServer p
- toUserServer (serverId, host, port, keyHash, auth_, BI preset, tested, BI enabled) =
- let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
+ toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Extras, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) -> UserServer p
+ toUserServer (serverId, host, port, keyHash, extras, auth_, BI preset, tested, BI enabled) =
+ let server = ProtoServerWithAuth (ProtocolServer p host port keyHash extras) (BasicAuth . encodeUtf8 <$> auth_)
in UserServer {serverId, server, preset, tested = unBI <$> tested, enabled, deleted = False}
insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p)
@@ -606,8 +606,8 @@ insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, teste
db
[sql|
INSERT INTO protocol_servers
- (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
- VALUES (?,?,?,?,?,?,?,?,?,?,?)
+ (protocol, host, port, key_hash, extras, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|]
(serverColumns p server :. (BI preset, BI <$> tested, BI enabled, userId, ts, ts))
sId <- insertedRowId db
@@ -619,17 +619,17 @@ updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabl
db
[sql|
UPDATE protocol_servers
- SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?,
+ SET protocol = ?, host = ?, port = ?, key_hash = ?, extras = ?, basic_auth = ?,
preset = ?, tested = ?, enabled = ?, updated_at = ?
WHERE smp_server_id = ?
|]
(serverColumns p server :. (BI preset, BI <$> tested, BI enabled, ts, serverId))
-serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text)
-serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) =
+serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Extras, Maybe Text)
+serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash, extras} auth_) =
let protocol = decodeLatin1 $ strEncode p
auth = safeDecodeUtf8 . unBasicAuth <$> auth_
- in (protocol, host, port, keyHash, auth)
+ in (protocol, host, port, keyHash, extras, auth)
getServerOperators :: DB.Connection -> ExceptT StoreError IO ServerOperatorConditions
getServerOperators db = do
@@ -642,12 +642,13 @@ getServerOperators db = do
let conditionsAction = usageConditionsAction ops currentConditions now
pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction}
-getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP])
+getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF])
getUserServers db user =
- (,,)
+ (,,,)
<$> (map Just . serverOperators <$> getServerOperators db)
<*> liftIO (getProtocolServers db SPSMP user)
<*> liftIO (getProtocolServers db SPXFTP user)
+ <*> liftIO (getProtocolServers db SPNTF user)
setServerOperators :: DB.Connection -> NonEmpty ServerOperator -> IO ()
setServerOperators db ops = do
@@ -860,11 +861,12 @@ setUserServers :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers
setUserServers db user ts = checkConstraint SEUniqueID . liftIO . setUserServers' db user ts
setUserServers' :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers -> IO UserOperatorServers
-setUserServers' db user@User {userId} ts UpdatedUserOperatorServers {operator, smpServers, xftpServers} = do
+setUserServers' db user@User {userId} ts UpdatedUserOperatorServers {operator, smpServers, xftpServers, ntfServers} = do
mapM_ (updateServerOperator db ts) operator
smpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPSMP) smpServers
xftpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPXFTP) xftpServers
- pure UserOperatorServers {operator, smpServers = smpSrvs', xftpServers = xftpSrvs'}
+ ntfSrvs' <- catMaybes <$> mapM (upsertOrDelete SPNTF) ntfServers
+ pure UserOperatorServers {operator, smpServers = smpSrvs', xftpServers = xftpSrvs', ntfServers = ntfSrvs'}
where
upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> AUserServer p -> IO (Maybe (UserServer p))
upsertOrDelete p (AUS _ s@UserServer {serverId, deleted}) = case serverId of
diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs
index 3d5fc307ee..eddfce58c8 100644
--- a/src/Simplex/Chat/Store/SQLite/Migrations.hs
+++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs
@@ -139,6 +139,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250721_indexes
import Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests
import Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri
import Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type
+import Simplex.Chat.Store.SQLite.Migrations.M20250815_extras
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -277,7 +278,8 @@ schemaMigrations =
("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes),
("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests),
("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri),
- ("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type)
+ ("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type),
+ ("20250815_extras", m20250815_extras, Just down_m20250815_extras)
]
-- | The list of migrations in ascending order by date
diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250815_extras.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250815_extras.hs
new file mode 100644
index 0000000000..fb6233af40
--- /dev/null
+++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250815_extras.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Store.SQLite.Migrations.M20250815_extras where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20250815_extras :: Query
+m20250815_extras =
+ [sql|
+ALTER TABLE protocol_servers ADD COLUMN extras TEXT;
+|]
+
+down_m20250815_extras :: Query
+down_m20250815_extras =
+ [sql|
+ALTER TABLE protocol_servers DROP COLUMN extras;
+|]
diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql
index 952d0541d9..4a47f1523b 100644
--- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql
@@ -494,6 +494,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"(
host TEXT NOT NULL,
port TEXT NOT NULL,
key_hash BLOB NOT NULL,
+ extras TEXT,
basic_auth TEXT,
preset INTEGER NOT NULL DEFAULT 0,
tested INTEGER,
diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs
index e432343839..ed50b1a14b 100644
--- a/src/Simplex/Chat/Terminal.hs
+++ b/src/Simplex/Chat/Terminal.hs
@@ -50,10 +50,11 @@ terminalChatConfig =
],
useSMP = 3,
xftp = map (presetServer True) $ L.toList defaultXFTPServers,
- useXFTP = 3
+ useXFTP = 3,
+ ntf = map (presetServer True) $ L.toList _defaultNtfServers,
+ useNTF = 3
}
],
- ntf = _defaultNtfServers,
netCfg =
defaultNetworkConfig
{ smpProxyMode = SPMUnknown,
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 6fef6cc204..5b9c744dda 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -1459,11 +1459,12 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho
]
viewUserServers :: UserOperatorServers -> [StyledString]
-viewUserServers (UserOperatorServers _ [] []) = []
-viewUserServers UserOperatorServers {operator, smpServers, xftpServers} =
+viewUserServers (UserOperatorServers _ [] [] []) = []
+viewUserServers UserOperatorServers {operator, smpServers, xftpServers, ntfServers} =
[plain $ maybe "Your servers" shortViewOperator operator]
<> viewServers SPSMP smpServers
<> viewServers SPXFTP xftpServers
+ <> viewServers SPNTF ntfServers
where
viewServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServer p] -> [StyledString]
viewServers _ [] = []