mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 17:13:10 +00:00
Merge branch 'feat/up' of github.com:p1gp1g/simplex-chat into p1gp1g-feat/up
This commit is contained in:
35 files changed
+917
-136
No files matched your search
@@ -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")
|
||||
|
||||
|
||||
@@ -186,6 +186,13 @@
|
||||
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
|
||||
/>
|
||||
|
||||
<service android:name=".PushService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".CallService$CallActionReceiver"
|
||||
android:enabled="true"
|
||||
|
||||
+77
-16
@@ -15,15 +15,30 @@ import java.util.concurrent.TimeUnit
|
||||
object MessagesFetcherWorker {
|
||||
private const val UNIQUE_WORK_TAG = BuildConfig.APPLICATION_ID + ".UNIQUE_MESSAGES_FETCHER"
|
||||
|
||||
fun scheduleWork(intervalSec: Int = 600, durationSec: Int = 60) {
|
||||
val initialDelaySec = intervalSec.toLong()
|
||||
Log.d(TAG, "Worker: scheduling work to run at ${Date(System.currentTimeMillis() + initialDelaySec * 1000)} for $durationSec sec")
|
||||
/**
|
||||
* Schedule [MessagesFetcherWork]
|
||||
*
|
||||
* @param initialDelayMs Delay in milliseconds before starting the jobs, usually = [intervalMs]
|
||||
* @param intervalMs Interval in milliseconds between the end of a jobs and the start of a new one.
|
||||
* If set to `0`, the job isn't restarted
|
||||
* @param timeoutMs Timeout in milliseconds until we stop a job, if there are still new messages, a new job is started directly
|
||||
* @param durationMs Duration in milliseconds to wait for new messages
|
||||
*/
|
||||
fun scheduleWork(
|
||||
initialDelayMs: Int = MessagesFetcherWork.DEFAULT_INTERVAL_MS,
|
||||
intervalMs: Int = MessagesFetcherWork.DEFAULT_INTERVAL_MS,
|
||||
timeoutMs: Int = MessagesFetcherWork.DEFAULT_TIMEOUT_MS,
|
||||
durationMs: Int = MessagesFetcherWork.DEFAULT_DURATION_MS
|
||||
) {
|
||||
Int.MAX_VALUE
|
||||
Log.d(TAG, "Worker: scheduling work to run at ${Date(System.currentTimeMillis() + initialDelayMs)} for $timeoutMs ms max")
|
||||
val periodicWorkRequest = OneTimeWorkRequest.Builder(MessagesFetcherWork::class.java)
|
||||
.setInitialDelay(initialDelaySec, TimeUnit.SECONDS)
|
||||
.setInitialDelay(initialDelayMs.toLong(), TimeUnit.MILLISECONDS)
|
||||
.setInputData(
|
||||
Data.Builder()
|
||||
.putInt(MessagesFetcherWork.INPUT_DATA_INTERVAL, intervalSec)
|
||||
.putInt(MessagesFetcherWork.INPUT_DATA_DURATION, durationSec)
|
||||
.putInt(MessagesFetcherWork.INPUT_DATA_INTERVAL, intervalMs)
|
||||
.putInt(MessagesFetcherWork.INPUT_DATA_TIMEOUT, timeoutMs)
|
||||
.putInt(MessagesFetcherWork.INPUT_DATA_DURATION, durationMs)
|
||||
.build()
|
||||
)
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
@@ -45,25 +60,49 @@ class MessagesFetcherWork(
|
||||
workerParams: WorkerParameters
|
||||
): CoroutineWorker(context, workerParams) {
|
||||
companion object {
|
||||
/**
|
||||
* Interval, in milliseconds, between the end of a job and the start of a new one
|
||||
*/
|
||||
const val INPUT_DATA_INTERVAL = "interval"
|
||||
|
||||
/**
|
||||
* Timeout, in milliseconds, until we force stop the jobs, if there are new messages, a new job is started directly
|
||||
*/
|
||||
const val INPUT_DATA_TIMEOUT = "timeout"
|
||||
|
||||
/**
|
||||
* Minimum duration, in milliseconds, to wait for new messages
|
||||
*/
|
||||
const val INPUT_DATA_DURATION = "duration"
|
||||
private const val WAIT_AFTER_LAST_MESSAGE: Long = 10_000
|
||||
const val DEFAULT_DURATION_MS = 10_000
|
||||
const val DEFAULT_TIMEOUT_MS = 60_000
|
||||
const val DEFAULT_INTERVAL_MS = 600_000
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
// Skip when Simplex service is currently working
|
||||
val durationMs = inputData.getInt(INPUT_DATA_DURATION, DEFAULT_DURATION_MS)
|
||||
val timeoutMs = inputData.getInt(INPUT_DATA_TIMEOUT, DEFAULT_TIMEOUT_MS)
|
||||
val intervalMs = inputData.getInt(INPUT_DATA_INTERVAL, DEFAULT_INTERVAL_MS)
|
||||
// initialDelayMs may be = 0
|
||||
// in this case, reschedule won't start a worker.
|
||||
var initialDelayMs = intervalMs
|
||||
if (SimplexService.getServiceState(SimplexApp.context) == SimplexService.ServiceState.STARTED) {
|
||||
reschedule()
|
||||
reschedule(
|
||||
initialDelayMs = initialDelayMs,
|
||||
intervalMs = intervalMs,
|
||||
timeoutMs = timeoutMs,
|
||||
durationMs = durationMs
|
||||
)
|
||||
return Result.success()
|
||||
}
|
||||
val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60)
|
||||
var shouldReschedule = true
|
||||
try {
|
||||
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||
initChatControllerOnStart()
|
||||
}
|
||||
withTimeout(durationSeconds * 1000L) {
|
||||
withTimeout(timeoutMs.toLong()) {
|
||||
val chatController = ChatController
|
||||
SimplexService.waitDbMigrationEnds(chatController)
|
||||
val chatDbStatus = chatController.chatModel.chatDbStatus.value
|
||||
@@ -75,26 +114,48 @@ class MessagesFetcherWork(
|
||||
}
|
||||
Log.w(TAG, "Worker: starting work")
|
||||
// Give some time to start receiving messages
|
||||
delay(10_000)
|
||||
delay(durationMs + 200L)
|
||||
while (!isStopped) {
|
||||
if (chatController.lastMsgReceivedTimestamp + WAIT_AFTER_LAST_MESSAGE < System.currentTimeMillis()) {
|
||||
val lastMsgMs = System.currentTimeMillis() - chatController.lastMsgReceivedTimestamp
|
||||
if (lastMsgMs > 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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<UserOperatorServers>.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<UserOperatorServers>) = 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<UserOperatorServers>) = 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<UserOperatorServers>) {
|
||||
val userServersState = mutableStateOf(userServers)
|
||||
val serverErrors = mutableStateOf(listOf<UserServersError>())
|
||||
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<String>, 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
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
|
||||
+46
-3
@@ -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<String>,
|
||||
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<UserServer> = listOf(),
|
||||
val smpServers: List<UserServer>,
|
||||
val xftpServers: List<UserServer>
|
||||
) {
|
||||
@@ -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
|
||||
|
||||
+1
@@ -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.
|
||||
|
||||
+15
-2
@@ -133,6 +133,15 @@ private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
|
||||
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<ValueTitleDesc<NotificationPreviewMode>> {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+9
@@ -932,6 +932,15 @@ fun globalXFTPServersError(serverErrors: List<UserServersError>): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun globalNTFServersError(serverErrors: List<UserServersError>): String? {
|
||||
for (err in serverErrors) {
|
||||
if (err.globalNTFError != null) {
|
||||
return err.globalNTFError
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun findDuplicateHosts(serverErrors: List<UserServersError>): Set<String> {
|
||||
val duplicateHostsList = serverErrors.mapNotNull { err ->
|
||||
if (err is UserServersError.DuplicateServer) {
|
||||
|
||||
+4
-1
@@ -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)
|
||||
|
||||
+21
-17
@@ -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
|
||||
|
||||
+5
-1
@@ -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)
|
||||
|
||||
+68
-4
@@ -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<UserServer>,
|
||||
xftpServers: List<UserServer>,
|
||||
ntfServers: List<UserServer>,
|
||||
testing: MutableState<Boolean>,
|
||||
onUpdate: (ServerProtocol, List<UserServer>) -> 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<Boolean>,
|
||||
smpServers: List<UserServer>,
|
||||
xftpServers: List<UserServer>,
|
||||
ntfServers: List<UserServer>,
|
||||
m: ChatModel,
|
||||
onUpdate: (ServerProtocol, List<UserServer>) -> 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<UserServer>, m: ChatModel, onUp
|
||||
return fs
|
||||
}
|
||||
|
||||
fun deleteNTFServer(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
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<List<UserOperatorServers>>,
|
||||
operatorServersIndex: Int,
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
<string name="no_media_servers_configured">No media & file servers.</string>
|
||||
<string name="no_media_servers_configured_for_sending">No servers to send files.</string>
|
||||
<string name="no_media_servers_configured_for_private_routing">No servers to receive files.</string>
|
||||
<string name="no_notif_servers_configured">No notification servers.</string>
|
||||
<string name="no_notif_servers_configured_for_receiving">No servers to notify.</string>
|
||||
<string name="for_chat_profile">For chat profile %s:</string>
|
||||
<string name="errors_in_servers_configuration">Errors in servers configuration.</string>
|
||||
<string name="error_accepting_operator_conditions">Error accepting conditions</string>
|
||||
@@ -270,9 +272,11 @@
|
||||
<string name="notifications_mode_off">Runs when app is open</string>
|
||||
<string name="notifications_mode_periodic">Starts periodically</string>
|
||||
<string name="notifications_mode_service">Always on</string>
|
||||
<string name="notifications_mode_instant">Instant</string>
|
||||
<string name="notifications_mode_off_desc">App can receive notifications only when it\'s running, no background service will be started</string>
|
||||
<string name="notifications_mode_periodic_desc">Checks new messages every 10 minutes for up to 1 minute</string>
|
||||
<string name="notifications_mode_service_desc">Background service is always running – notifications will be shown as soon as the messages are available.</string>
|
||||
<string name="notifications_mode_instant_desc">Receive notifications instantly using an external push service.</string>
|
||||
<string name="notification_preview_mode_message">Message text</string>
|
||||
<string name="notification_preview_mode_contact">Contact name</string>
|
||||
<string name="notification_preview_mode_hidden">Hidden</string>
|
||||
@@ -284,6 +288,16 @@
|
||||
<string name="notification_new_contact_request">New contact request</string>
|
||||
<string name="notification_contact_connected">Connected</string>
|
||||
<string name="error_showing_desktop_notification">Error showing notification, contact developers.</string>
|
||||
<string name="warning_push_needs_ntf_server">You first need to set a NTF server to get push notifications.\n\nIt will send the notifications to your push provider.</string>
|
||||
<string name="warning_push_needs_ntf_server_with_vapid">Your NTF server doesn\'t support VAPID, which is required to get push notifications.</string>
|
||||
<string name="warning_push_needs_push_service">You don\'t have any push service installed on your device.\n\nPlease installed one and try again.\n\nFor more information, visit\ </string>
|
||||
<string name="warning_ntf_server_changed">Your NTF server has been removed or has changed and doesn\'t support VAPID, which is required to get push notifications.</string>
|
||||
<string name="select_push_service">Select Push Service</string>
|
||||
<string name="select_push_service_intro">Multiple push services are installed on your system, please select the service you wish to use.</string>
|
||||
<string name="unifiedpush_unregistered">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.</string>
|
||||
<string name="unifiedpush_registration_failed_network">Registration with your UnifiedPush distributor failed due to a network issue. Please try again when your network is back.</string>
|
||||
<string name="unifiedpush_registration_failed_action">Registration with your UnifiedPush distributor failed due to a missing requirement from your distributor.</string>
|
||||
<string name="unifiedpush_registration_failed_unknown">Registration with your UnifiedPush distributor failed due to an unknown issue. This could be a missing requirement from your distributor.</string>
|
||||
|
||||
<!-- local authentication notice - SimpleXAPI.kt -->
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
|
||||
@@ -949,6 +963,7 @@
|
||||
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
|
||||
<string name="smp_save_servers_question">Save servers?</string>
|
||||
<string name="media_and_file_servers">Media & file servers</string>
|
||||
<string name="notif_servers">Notifications servers</string>
|
||||
<string name="xftp_servers">XFTP servers</string>
|
||||
<string name="xftp_servers_configured">Configured XFTP servers</string>
|
||||
<string name="xftp_servers_other">Other XFTP servers</string>
|
||||
@@ -1995,6 +2010,7 @@
|
||||
<string name="operator_use_for_files">Use for files</string>
|
||||
<string name="operator_use_for_sending">To send</string>
|
||||
<string name="xftp_servers_per_user">The servers for new files of your current chat profile</string>
|
||||
<string name="ntf_servers_per_user">The servers for your notifications.</string>
|
||||
<string name="operator_added_xftp_servers">Added media & file servers</string>
|
||||
<string name="operator_open_conditions">Open conditions</string>
|
||||
<string name="operator_open_changes">Open changes</string>
|
||||
@@ -2734,6 +2750,7 @@
|
||||
<string name="servers_info_starting_from">Starting from %s.</string>
|
||||
<string name="smp_server">SMP server</string>
|
||||
<string name="xftp_server">XFTP server</string>
|
||||
<string name="ntf_server">NTF server</string>
|
||||
<string name="reconnect">Reconnect</string>
|
||||
<string name="attempts_label">attempts</string>
|
||||
<string name="sent_directly">Sent directly</string>
|
||||
|
||||
+7
-2
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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:
|
||||
|
||||
+31
-18
@@ -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} =
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ','),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -245,6 +245,7 @@ mobileChatOpts dbOptions =
|
||||
{ dbOptions,
|
||||
smpServers = [],
|
||||
xftpServers = [],
|
||||
ntfServers = [],
|
||||
simpleNetCfg = defaultSimpleNetCfg,
|
||||
logLevel = CLLImportant,
|
||||
logConnections = False,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -116,3 +116,6 @@ fluxXFTPServers =
|
||||
"xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion",
|
||||
"xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion"
|
||||
]
|
||||
|
||||
fluxNTFServers :: [NewUserServer 'PNTF]
|
||||
fluxNTFServers = []
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|]
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 _ [] = []
|
||||
|
||||
Reference in new issue
Block a user