mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-16 21:51:59 +00:00
desktop, android: server operators (#5212)
* api and types * whats new view * new package and movements * move network and servers to new package * network and servers view * wip * api update * build * conditions modal in settings * network and servers fns * save server fixes * more servers * move protocol servers view * message servers with validation * added message servers * use for files * fix error by server type * list xftp servers * android: add server view (#5221) * android add server wip * test servers button * fix save of custom servers * remove unused code * edit and view servers * fix * allow to enable untested * show all test errors in the end * android: custom servers view (#5224) * cleanup * validation footers * operator enabled validation * var -> val * reuse onboarding button * AppBarTitle without alpha * remove non scrollable title * change in AppBarTitle * changes in AppBar * bold strings + bordered text view * ChooseServerOperators * fix * new server view wip * fix * scan * rename * fix roles toggle texts * UsageConditionsView * aligned texts * more texts * replace hard coded logos with object ref * use snapshot state to recalculate errors * align views; fix accept * remove extra snapshots * fix ts * fix whatsnew * stage * animation on onboarding * refactor and fix * remember * fix start chat alert * show notice in chat list * refactor * fix validation * open conditions * whats new view updates * icon for navigation improvements * remove debug * simplify * fix * handle click when have unsaved changes * fix * Revert "fix" This reverts commitd49c373641. * Revert "handle click when have unsaved changes" This reverts commit39ca03f9c0. * fixed close of modals in whats new view * grouping * android: conditions view paddings (#5228) * revert padding * refresh operators on save * fixed modals in different views for desktop * ios: fix enabling operator model update * fix modals --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
co-authored by
spaced4ndy
Avently
parent
49d1b26bba
commit
396fa7f988
@@ -395,7 +395,9 @@ func saveServers(_ currUserServers: Binding<[UserOperatorServers]>, _ userServer
|
||||
// Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers)
|
||||
do {
|
||||
let updatedServers = try await getUserServers()
|
||||
let updatedOperators = try await getServerOperators()
|
||||
await MainActor.run {
|
||||
ChatModel.shared.conditions = updatedOperators
|
||||
currUserServers.wrappedValue = updatedServers
|
||||
userServers.wrappedValue = updatedServers
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import chat.simplex.common.model.ServerCfg
|
||||
import chat.simplex.common.model.UserServer
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) {
|
||||
actual fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
@@ -15,7 +15,6 @@ import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -42,7 +41,6 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun AppScreen() {
|
||||
@@ -194,6 +192,13 @@ fun MainScreen() {
|
||||
OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
|
||||
OnboardingStage.LinkAMobile -> LinkAMobile()
|
||||
OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
|
||||
OnboardingStage.Step3_ChooseServerOperators -> {
|
||||
val modalData = remember { ModalData() }
|
||||
modalData.ChooseServerOperators(true)
|
||||
if (appPlatform.isDesktop) {
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
}
|
||||
// Ensure backwards compatibility with old onboarding stage for address creation, otherwise notification setup would be skipped
|
||||
OnboardingStage.Step3_CreateSimpleXAddress -> SetNotificationsMode(chatModel)
|
||||
OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
|
||||
@@ -145,6 +145,8 @@ object ChatModel {
|
||||
val clipboardHasText = mutableStateOf(false)
|
||||
val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true))
|
||||
|
||||
val conditions = mutableStateOf(ServerOperatorConditionsDetail.empty)
|
||||
|
||||
val updatingProgress = mutableStateOf(null as Float?)
|
||||
var updatingRequest: Closeable? = null
|
||||
|
||||
@@ -2567,6 +2569,13 @@ fun localTimestamp(t: Instant): String {
|
||||
return ts.toJavaLocalDateTime().format(dateFormatter)
|
||||
}
|
||||
|
||||
fun localDate(t: Instant): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val ts: LocalDateTime = t.toLocalDateTime(tz)
|
||||
val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
||||
return ts.toJavaLocalDateTime().format(dateFormatter)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CIStatus {
|
||||
@Serializable @SerialName("sndNew") class SndNew: CIStatus()
|
||||
|
||||
+498
-59
@@ -26,10 +26,12 @@ import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert
|
||||
import chat.simplex.common.views.migration.MigrationFileLinkData
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.serverHostname
|
||||
import com.charleskorn.kaml.Yaml
|
||||
import com.charleskorn.kaml.YamlConfiguration
|
||||
import chat.simplex.res.MR
|
||||
import com.russhwolf.settings.Settings
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -963,36 +965,6 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun getUserProtoServers(rh: Long?, serverProtocol: ServerProtocol): UserProtocolServers? {
|
||||
val userId = kotlin.runCatching { currentUserId("getUserProtoServers") }.getOrElse { return null }
|
||||
val r = sendCmd(rh, CC.APIGetUserProtoServers(userId, serverProtocol))
|
||||
return if (r is CR.UserProtoServers) { if (rh == null) r.servers else r.servers.copy(protoServers = r.servers.protoServers.map { it.copy(remoteHostId = rh) }) }
|
||||
else {
|
||||
Log.e(TAG, "getUserProtoServers bad response: ${r.responseType} ${r.details}")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.error_loading_smp_servers else MR.strings.error_loading_xftp_servers),
|
||||
"${r.responseType}: ${r.details}"
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setUserProtoServers(rh: Long?, serverProtocol: ServerProtocol, servers: List<ServerCfg>): Boolean {
|
||||
val userId = kotlin.runCatching { currentUserId("setUserProtoServers") }.getOrElse { return false }
|
||||
val r = sendCmd(rh, CC.APISetUserProtoServers(userId, serverProtocol, servers))
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
else -> {
|
||||
Log.e(TAG, "setUserProtoServers bad response: ${r.responseType} ${r.details}")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.error_saving_smp_servers else MR.strings.error_saving_xftp_servers),
|
||||
generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.ensure_smp_server_address_are_correct_format_and_unique else MR.strings.ensure_xftp_server_address_are_correct_format_and_unique)
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun testProtoServer(rh: Long?, server: String): ProtocolTestFailure? {
|
||||
val userId = currentUserId("testProtoServer")
|
||||
val r = sendCmd(rh, CC.APITestProtoServer(userId, server))
|
||||
@@ -1005,6 +977,106 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getServerOperators(rh: Long?): ServerOperatorConditionsDetail? {
|
||||
val r = sendCmd(rh, CC.ApiGetServerOperators())
|
||||
|
||||
return when (r) {
|
||||
is CR.ServerOperatorConditions -> r.conditions
|
||||
else -> {
|
||||
Log.e(TAG, "getServerOperators bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setServerOperators(rh: Long?, operators: List<ServerOperator>): ServerOperatorConditionsDetail? {
|
||||
val r = sendCmd(rh, CC.ApiSetServerOperators(operators))
|
||||
return when (r) {
|
||||
is CR.ServerOperatorConditions -> r.conditions
|
||||
else -> {
|
||||
Log.e(TAG, "setServerOperators bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUserServers(rh: Long?): List<UserOperatorServers>? {
|
||||
val userId = currentUserId("getUserServers")
|
||||
val r = sendCmd(rh, CC.ApiGetUserServers(userId))
|
||||
return when (r) {
|
||||
is CR.UserServers -> r.userServers
|
||||
else -> {
|
||||
Log.e(TAG, "getUserServers bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setUserServers(rh: Long?, userServers: List<UserOperatorServers>): Boolean {
|
||||
val userId = currentUserId("setUserServers")
|
||||
val r = sendCmd(rh, CC.ApiSetUserServers(userId, userServers))
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
else -> {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.failed_to_save_servers),
|
||||
"${r.responseType}: ${r.details}"
|
||||
)
|
||||
Log.e(TAG, "setUserServers bad response: ${r.responseType} ${r.details}")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun validateServers(rh: Long?, userServers: List<UserOperatorServers>): List<UserServersError>? {
|
||||
val userId = currentUserId("validateServers")
|
||||
val r = sendCmd(rh, CC.ApiValidateServers(userId, userServers))
|
||||
return when (r) {
|
||||
is CR.UserServersValidation -> r.serverErrors
|
||||
else -> {
|
||||
Log.e(TAG, "validateServers bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUsageConditions(rh: Long?): Triple<UsageConditionsDetail, String?, UsageConditionsDetail?>? {
|
||||
val r = sendCmd(rh, CC.ApiGetUsageConditions())
|
||||
return when (r) {
|
||||
is CR.UsageConditions -> Triple(r.usageConditions, r.conditionsText, r.acceptedConditions)
|
||||
else -> {
|
||||
Log.e(TAG, "getUsageConditions bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setConditionsNotified(rh: Long?, conditionsId: Long): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiSetConditionsNotified(conditionsId))
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
else -> {
|
||||
Log.e(TAG, "setConditionsNotified bad response: ${r.responseType} ${r.details}")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun acceptConditions(rh: Long?, conditionsId: Long, operatorIds: List<Long>): ServerOperatorConditionsDetail? {
|
||||
val r = sendCmd(rh, CC.ApiAcceptConditions(conditionsId, operatorIds))
|
||||
return when (r) {
|
||||
is CR.ServerOperatorConditions -> r.conditions
|
||||
else -> {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.error_accepting_operator_conditions),
|
||||
"${r.responseType}: ${r.details}"
|
||||
)
|
||||
Log.e(TAG, "acceptConditions bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChatItemTTL(rh: Long?): ChatItemTTL {
|
||||
val userId = currentUserId("getChatItemTTL")
|
||||
val r = sendCmd(rh, CC.APIGetChatItemTTL(userId))
|
||||
@@ -3037,9 +3109,15 @@ sealed class CC {
|
||||
class APIGetGroupLink(val groupId: Long): CC()
|
||||
class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC()
|
||||
class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC()
|
||||
class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC()
|
||||
class APITestProtoServer(val userId: Long, val server: String): CC()
|
||||
class ApiGetServerOperators(): CC()
|
||||
class ApiSetServerOperators(val operators: List<ServerOperator>): CC()
|
||||
class ApiGetUserServers(val userId: Long): CC()
|
||||
class ApiSetUserServers(val userId: Long, val userServers: List<UserOperatorServers>): CC()
|
||||
class ApiValidateServers(val userId: Long, val userServers: List<UserOperatorServers>): CC()
|
||||
class ApiGetUsageConditions(): CC()
|
||||
class ApiSetConditionsNotified(val conditionsId: Long): CC()
|
||||
class ApiAcceptConditions(val conditionsId: Long, val operatorIds: List<Long>): CC()
|
||||
class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC()
|
||||
class APIGetChatItemTTL(val userId: Long): CC()
|
||||
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
|
||||
@@ -3197,9 +3275,15 @@ sealed class CC {
|
||||
is APIGetGroupLink -> "/_get link #$groupId"
|
||||
is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId"
|
||||
is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}"
|
||||
is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}"
|
||||
is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}"
|
||||
is APITestProtoServer -> "/_server test $userId $server"
|
||||
is ApiGetServerOperators -> "/_operators"
|
||||
is ApiSetServerOperators -> "/_operators ${json.encodeToString(operators)}"
|
||||
is ApiGetUserServers -> "/_servers $userId"
|
||||
is ApiSetUserServers -> "/_servers $userId ${json.encodeToString(userServers)}"
|
||||
is ApiValidateServers -> "/_validate_servers $userId ${json.encodeToString(userServers)}"
|
||||
is ApiGetUsageConditions -> "/_conditions"
|
||||
is ApiSetConditionsNotified -> "/_conditions_notified ${conditionsId}"
|
||||
is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}"
|
||||
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
|
||||
is APIGetChatItemTTL -> "/_ttl $userId"
|
||||
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
|
||||
@@ -3342,9 +3426,15 @@ sealed class CC {
|
||||
is APIGetGroupLink -> "apiGetGroupLink"
|
||||
is APICreateMemberContact -> "apiCreateMemberContact"
|
||||
is APISendMemberContactInvitation -> "apiSendMemberContactInvitation"
|
||||
is APIGetUserProtoServers -> "apiGetUserProtoServers"
|
||||
is APISetUserProtoServers -> "apiSetUserProtoServers"
|
||||
is APITestProtoServer -> "testProtoServer"
|
||||
is ApiGetServerOperators -> "apiGetServerOperators"
|
||||
is ApiSetServerOperators -> "apiSetServerOperators"
|
||||
is ApiGetUserServers -> "apiGetUserServers"
|
||||
is ApiSetUserServers -> "apiSetUserServers"
|
||||
is ApiValidateServers -> "apiValidateServers"
|
||||
is ApiGetUsageConditions -> "apiGetUsageConditions"
|
||||
is ApiSetConditionsNotified -> "apiSetConditionsNotified"
|
||||
is ApiAcceptConditions -> "apiAcceptConditions"
|
||||
is APISetChatItemTTL -> "apiSetChatItemTTL"
|
||||
is APIGetChatItemTTL -> "apiGetChatItemTTL"
|
||||
is APISetNetworkConfig -> "apiSetNetworkConfig"
|
||||
@@ -3459,8 +3549,6 @@ sealed class CC {
|
||||
|
||||
companion object {
|
||||
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
||||
|
||||
fun protoServersStr(servers: List<ServerCfg>) = json.encodeToString(ProtoServersConfig(servers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3510,24 +3598,350 @@ enum class ServerProtocol {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ProtoServersConfig(
|
||||
val servers: List<ServerCfg>
|
||||
enum class OperatorTag {
|
||||
@SerialName("simplex") SimpleX,
|
||||
@SerialName("flux") Flux,
|
||||
@SerialName("xyz") XYZ,
|
||||
@SerialName("demo") Demo
|
||||
}
|
||||
|
||||
data class ServerOperatorInfo(
|
||||
val description: List<String>,
|
||||
val website: String,
|
||||
val logo: ImageResource,
|
||||
val largeLogo: ImageResource,
|
||||
val logoDarkMode: ImageResource,
|
||||
val largeLogoDarkMode: ImageResource
|
||||
)
|
||||
val operatorsInfo: Map<OperatorTag, ServerOperatorInfo> = mapOf(
|
||||
OperatorTag.SimpleX to ServerOperatorInfo(
|
||||
description = listOf(
|
||||
"SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or keys that identify the users.",
|
||||
"SimpleX Chat Ltd develops the communication software for SimpleX network."
|
||||
),
|
||||
website = "https://simplex.chat",
|
||||
logo = MR.images.decentralized,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.decentralized_light,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
),
|
||||
OperatorTag.Flux to ServerOperatorInfo(
|
||||
description = listOf(
|
||||
"Flux is the largest decentralized cloud infrastructure, leveraging a global network of user-operated computational nodes.",
|
||||
"Flux offers a powerful, scalable, and affordable platform designed to support individuals, businesses, and cutting-edge technologies like AI. With high uptime and worldwide distribution, Flux ensures reliable, accessible cloud computing for all."
|
||||
),
|
||||
website = "https://runonflux.com",
|
||||
logo = MR.images.flux_logo_symbol,
|
||||
largeLogo = MR.images.flux_logo,
|
||||
logoDarkMode = MR.images.flux_logo_symbol,
|
||||
largeLogoDarkMode = MR.images.flux_logo_light
|
||||
),
|
||||
OperatorTag.XYZ to ServerOperatorInfo(
|
||||
description = listOf("XYZ servers"),
|
||||
website = "XYZ website",
|
||||
logo = MR.images.shield,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.shield,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
),
|
||||
OperatorTag.Demo to ServerOperatorInfo(
|
||||
description = listOf("Demo operator"),
|
||||
website = "Demo website",
|
||||
logo = MR.images.decentralized,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.decentralized_light,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserProtocolServers(
|
||||
val serverProtocol: ServerProtocol,
|
||||
val protoServers: List<ServerCfg>,
|
||||
val presetServers: List<ServerCfg>,
|
||||
data class UsageConditionsDetail(
|
||||
val conditionsId: Long,
|
||||
val conditionsCommit: String,
|
||||
val notifiedAt: Instant?,
|
||||
val createdAt: Instant
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = UsageConditionsDetail(
|
||||
conditionsId = 1,
|
||||
conditionsCommit = "11a44dc1fd461a93079f897048b46998db55da5c",
|
||||
notifiedAt = null,
|
||||
createdAt = Clock.System.now()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class UsageConditionsAction {
|
||||
@Serializable @SerialName("review") data class Review(val operators: List<ServerOperator>, val deadline: Instant?, val showNotice: Boolean) : UsageConditionsAction()
|
||||
@Serializable @SerialName("accepted") data class Accepted(val operators: List<ServerOperator>) : UsageConditionsAction()
|
||||
|
||||
val shouldShowNotice: Boolean
|
||||
get() = when (this) {
|
||||
is Review -> showNotice
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerOperatorConditionsDetail(
|
||||
val serverOperators: List<ServerOperator>,
|
||||
val currentConditions: UsageConditionsDetail,
|
||||
val conditionsAction: UsageConditionsAction?
|
||||
) {
|
||||
companion object {
|
||||
val empty = ServerOperatorConditionsDetail(
|
||||
serverOperators = emptyList(),
|
||||
currentConditions = UsageConditionsDetail(conditionsId = 0, conditionsCommit = "empty", notifiedAt = null, createdAt = Clock.System.now()),
|
||||
conditionsAction = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable()
|
||||
sealed class ConditionsAcceptance {
|
||||
@Serializable @SerialName("accepted") data class Accepted(val acceptedAt: Instant?) : ConditionsAcceptance()
|
||||
@Serializable @SerialName("required") data class Required(val deadline: Instant?) : ConditionsAcceptance()
|
||||
|
||||
val conditionsAccepted: Boolean
|
||||
get() = when (this) {
|
||||
is Accepted -> true
|
||||
is Required -> false
|
||||
}
|
||||
|
||||
val usageAllowed: Boolean
|
||||
get() = when (this) {
|
||||
is Accepted -> true
|
||||
is Required -> this.deadline != null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerOperator(
|
||||
val operatorId: Long,
|
||||
val operatorTag: OperatorTag?,
|
||||
val tradeName: String,
|
||||
val legalName: String?,
|
||||
val serverDomains: List<String>,
|
||||
val conditionsAcceptance: ConditionsAcceptance,
|
||||
val enabled: Boolean,
|
||||
val smpRoles: ServerRoles,
|
||||
val xftpRoles: ServerRoles,
|
||||
) {
|
||||
companion object {
|
||||
val dummyOperatorInfo = ServerOperatorInfo(
|
||||
description = listOf("Default"),
|
||||
website = "Default",
|
||||
logo = MR.images.decentralized,
|
||||
largeLogo = MR.images.logo,
|
||||
logoDarkMode = MR.images.decentralized_light,
|
||||
largeLogoDarkMode = MR.images.logo_light
|
||||
)
|
||||
|
||||
val sampleData1 = ServerOperator(
|
||||
operatorId = 1,
|
||||
operatorTag = OperatorTag.SimpleX,
|
||||
tradeName = "SimpleX Chat",
|
||||
legalName = "SimpleX Chat Ltd",
|
||||
serverDomains = listOf("simplex.im"),
|
||||
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null),
|
||||
enabled = true,
|
||||
smpRoles = ServerRoles(storage = true, proxy = true),
|
||||
xftpRoles = ServerRoles(storage = true, proxy = true)
|
||||
)
|
||||
|
||||
val sampleData2 = ServerOperator(
|
||||
operatorId = 2,
|
||||
operatorTag = OperatorTag.XYZ,
|
||||
tradeName = "XYZ",
|
||||
legalName = null,
|
||||
serverDomains = listOf("xyz.com"),
|
||||
conditionsAcceptance = ConditionsAcceptance.Required(deadline = null),
|
||||
enabled = false,
|
||||
smpRoles = ServerRoles(storage = false, proxy = true),
|
||||
xftpRoles = ServerRoles(storage = false, proxy = true)
|
||||
)
|
||||
|
||||
val sampleData3 = ServerOperator(
|
||||
operatorId = 3,
|
||||
operatorTag = OperatorTag.Demo,
|
||||
tradeName = "Demo",
|
||||
legalName = null,
|
||||
serverDomains = listOf("demo.com"),
|
||||
conditionsAcceptance = ConditionsAcceptance.Required(deadline = null),
|
||||
enabled = false,
|
||||
smpRoles = ServerRoles(storage = true, proxy = false),
|
||||
xftpRoles = ServerRoles(storage = true, proxy = false)
|
||||
)
|
||||
}
|
||||
|
||||
val id: Long
|
||||
get() = operatorId
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ServerOperator) return false
|
||||
return other.operatorId == this.operatorId &&
|
||||
other.operatorTag == this.operatorTag &&
|
||||
other.tradeName == this.tradeName &&
|
||||
other.legalName == this.legalName &&
|
||||
other.serverDomains == this.serverDomains &&
|
||||
other.conditionsAcceptance == this.conditionsAcceptance &&
|
||||
other.enabled == this.enabled &&
|
||||
other.smpRoles == this.smpRoles &&
|
||||
other.xftpRoles == this.xftpRoles
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = operatorId.hashCode()
|
||||
result = 31 * result + (operatorTag?.hashCode() ?: 0)
|
||||
result = 31 * result + tradeName.hashCode()
|
||||
result = 31 * result + (legalName?.hashCode() ?: 0)
|
||||
result = 31 * result + serverDomains.hashCode()
|
||||
result = 31 * result + conditionsAcceptance.hashCode()
|
||||
result = 31 * result + enabled.hashCode()
|
||||
result = 31 * result + smpRoles.hashCode()
|
||||
result = 31 * result + xftpRoles.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
val legalName_: String
|
||||
get() = legalName ?: tradeName
|
||||
|
||||
val info: ServerOperatorInfo get() {
|
||||
return if (this.operatorTag != null) {
|
||||
operatorsInfo[this.operatorTag] ?: dummyOperatorInfo
|
||||
} else {
|
||||
dummyOperatorInfo
|
||||
}
|
||||
}
|
||||
|
||||
val logo: ImageResource
|
||||
@Composable
|
||||
get() {
|
||||
return if (isInDarkTheme()) info.logoDarkMode else info.logo
|
||||
}
|
||||
|
||||
val largeLogo: ImageResource
|
||||
@Composable
|
||||
get() {
|
||||
return if (isInDarkTheme()) info.largeLogoDarkMode else info.largeLogo
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerRoles(
|
||||
val storage: Boolean,
|
||||
val proxy: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerCfg(
|
||||
data class UserOperatorServers(
|
||||
val operator: ServerOperator?,
|
||||
val smpServers: List<UserServer>,
|
||||
val xftpServers: List<UserServer>
|
||||
) {
|
||||
val id: String
|
||||
get() = operator?.operatorId?.toString() ?: "nil operator"
|
||||
|
||||
val operator_: ServerOperator
|
||||
get() = operator ?: ServerOperator(
|
||||
operatorId = 0,
|
||||
operatorTag = null,
|
||||
tradeName = "",
|
||||
legalName = null,
|
||||
serverDomains = emptyList(),
|
||||
conditionsAcceptance = ConditionsAcceptance.Accepted(null),
|
||||
enabled = false,
|
||||
smpRoles = ServerRoles(storage = true, proxy = true),
|
||||
xftpRoles = ServerRoles(storage = true, proxy = true)
|
||||
)
|
||||
|
||||
companion object {
|
||||
val sampleData1 = UserOperatorServers(
|
||||
operator = ServerOperator.sampleData1,
|
||||
smpServers = listOf(UserServer.sampleData.preset),
|
||||
xftpServers = listOf(UserServer.sampleData.xftpPreset)
|
||||
)
|
||||
|
||||
val sampleDataNilOperator = UserOperatorServers(
|
||||
operator = null,
|
||||
smpServers = listOf(UserServer.sampleData.preset),
|
||||
xftpServers = listOf(UserServer.sampleData.xftpPreset)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class UserServersError {
|
||||
@Serializable @SerialName("noServers") data class NoServers(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
|
||||
@Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError()
|
||||
|
||||
val globalError: String?
|
||||
get() = when (this.protocol_) {
|
||||
ServerProtocol.SMP -> globalSMPError
|
||||
ServerProtocol.XFTP -> globalXFTPError
|
||||
}
|
||||
|
||||
private val protocol_: ServerProtocol
|
||||
get() = when (this) {
|
||||
is NoServers -> this.protocol
|
||||
is StorageMissing -> this.protocol
|
||||
is ProxyMissing -> this.protocol
|
||||
is DuplicateServer -> this.protocol
|
||||
}
|
||||
|
||||
val globalSMPError: String?
|
||||
get() = if (this.protocol_ == ServerProtocol.SMP) {
|
||||
when (this) {
|
||||
is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured)}" }
|
||||
?: generalGetString(MR.strings.no_message_servers_configured)
|
||||
|
||||
is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_receiving)}" }
|
||||
?: generalGetString(MR.strings.no_message_servers_configured_for_receiving)
|
||||
|
||||
is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_private_routing)}" }
|
||||
?: generalGetString(MR.strings.no_message_servers_configured_for_private_routing)
|
||||
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val globalXFTPError: String?
|
||||
get() = if (this.protocol_ == ServerProtocol.XFTP) {
|
||||
when (this) {
|
||||
is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured)}" }
|
||||
?: generalGetString(MR.strings.no_media_servers_configured)
|
||||
|
||||
is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured_for_sending)}" }
|
||||
?: generalGetString(MR.strings.no_media_servers_configured_for_sending)
|
||||
|
||||
is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured_for_private_routing)}" }
|
||||
?: generalGetString(MR.strings.no_media_servers_configured_for_private_routing)
|
||||
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private fun userStr(user: UserRef): String {
|
||||
return String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserServer(
|
||||
val remoteHostId: Long?,
|
||||
val serverId: Long?,
|
||||
val server: String,
|
||||
val preset: Boolean,
|
||||
val tested: Boolean? = null,
|
||||
val enabled: Boolean
|
||||
val enabled: Boolean,
|
||||
val deleted: Boolean
|
||||
) {
|
||||
@Transient
|
||||
private val createdAt: Date = Date()
|
||||
@@ -3541,35 +3955,51 @@ data class ServerCfg(
|
||||
get() = server.isBlank()
|
||||
|
||||
companion object {
|
||||
val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = false)
|
||||
val empty = UserServer(remoteHostId = null, serverId = null, server = "", preset = false, tested = null, enabled = false, deleted = false)
|
||||
|
||||
class SampleData(
|
||||
val preset: ServerCfg,
|
||||
val custom: ServerCfg,
|
||||
val untested: ServerCfg
|
||||
val preset: UserServer,
|
||||
val custom: UserServer,
|
||||
val untested: UserServer,
|
||||
val xftpPreset: UserServer
|
||||
)
|
||||
|
||||
val sampleData = SampleData(
|
||||
preset = ServerCfg(
|
||||
preset = UserServer(
|
||||
remoteHostId = null,
|
||||
serverId = 1,
|
||||
server = "smp://abcd@smp8.simplex.im",
|
||||
preset = true,
|
||||
tested = true,
|
||||
enabled = true
|
||||
enabled = true,
|
||||
deleted = false
|
||||
),
|
||||
custom = ServerCfg(
|
||||
custom = UserServer(
|
||||
remoteHostId = null,
|
||||
serverId = 2,
|
||||
server = "smp://abcd@smp9.simplex.im",
|
||||
preset = false,
|
||||
tested = false,
|
||||
enabled = false
|
||||
enabled = false,
|
||||
deleted = false
|
||||
),
|
||||
untested = ServerCfg(
|
||||
untested = UserServer(
|
||||
remoteHostId = null,
|
||||
serverId = 3,
|
||||
server = "smp://abcd@smp10.simplex.im",
|
||||
preset = false,
|
||||
tested = null,
|
||||
enabled = true
|
||||
enabled = true,
|
||||
deleted = false
|
||||
),
|
||||
xftpPreset = UserServer(
|
||||
remoteHostId = null,
|
||||
serverId = 4,
|
||||
server = "xftp://abcd@xftp8.simplex.im",
|
||||
preset = true,
|
||||
tested = true,
|
||||
enabled = true,
|
||||
deleted = false
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -4928,8 +5358,11 @@ sealed class CR {
|
||||
@Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR()
|
||||
@Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR()
|
||||
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
|
||||
@Serializable @SerialName("userProtoServers") class UserProtoServers(val user: UserRef, val servers: UserProtocolServers): CR()
|
||||
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
|
||||
@Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR()
|
||||
@Serializable @SerialName("userServers") class UserServers(val user: UserRef, val userServers: List<UserOperatorServers>): CR()
|
||||
@Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List<UserServersError>): CR()
|
||||
@Serializable @SerialName("usageConditions") class UsageConditions(val usageConditions: UsageConditionsDetail, val conditionsText: String?, val acceptedConditions: UsageConditionsDetail?): CR()
|
||||
@Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: UserRef, val chatItemTTL: Long? = null): CR()
|
||||
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
|
||||
@Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR()
|
||||
@@ -5108,8 +5541,11 @@ sealed class CR {
|
||||
is ApiChats -> "apiChats"
|
||||
is ApiChat -> "apiChat"
|
||||
is ApiChatItemInfo -> "chatItemInfo"
|
||||
is UserProtoServers -> "userProtoServers"
|
||||
is ServerTestResult -> "serverTestResult"
|
||||
is ServerOperatorConditions -> "serverOperatorConditions"
|
||||
is UserServers -> "userServers"
|
||||
is UserServersValidation -> "userServersValidation"
|
||||
is UsageConditions -> "usageConditions"
|
||||
is ChatItemTTL -> "chatItemTTL"
|
||||
is NetworkConfig -> "networkConfig"
|
||||
is ContactInfo -> "contactInfo"
|
||||
@@ -5278,8 +5714,11 @@ sealed class CR {
|
||||
is ApiChats -> withUser(user, json.encodeToString(chats))
|
||||
is ApiChat -> withUser(user, "chat: ${json.encodeToString(chat)}\nnavInfo: ${navInfo}")
|
||||
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
||||
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
|
||||
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
||||
is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}"
|
||||
is UserServers -> withUser(user, "userServers: ${json.encodeToString(userServers)}")
|
||||
is UserServersValidation -> withUser(user, "serverErrors: ${json.encodeToString(serverErrors)}")
|
||||
is UsageConditions -> "usageConditions: ${json.encodeToString(usageConditions)}\nnacceptedConditions: ${json.encodeToString(acceptedConditions)}"
|
||||
is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL))
|
||||
is NetworkConfig -> json.encodeToString(networkConfig)
|
||||
is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats_)}")
|
||||
|
||||
+6
-6
@@ -118,6 +118,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
val user = chatController.apiGetActiveUser(null)
|
||||
chatModel.currentUser.value = user
|
||||
chatModel.conditions.value = chatController.getServerOperators(null) ?: ServerOperatorConditionsDetail.empty
|
||||
if (user == null) {
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = null
|
||||
@@ -137,13 +138,12 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
}
|
||||
} else if (startChat().await()) {
|
||||
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
||||
val next = if (appPlatform.isAndroid) {
|
||||
OnboardingStage.Step4_SetNotificationsMode
|
||||
} else {
|
||||
OnboardingStage.OnboardingComplete
|
||||
}
|
||||
val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
next
|
||||
if (appPlatform.isAndroid) {
|
||||
OnboardingStage.Step4_SetNotificationsMode
|
||||
} else {
|
||||
OnboardingStage.OnboardingComplete
|
||||
}
|
||||
} else {
|
||||
savedOnboardingStage
|
||||
}
|
||||
|
||||
+2
-2
@@ -165,7 +165,7 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) {
|
||||
if (!chatModel.connectedToRemote()) {
|
||||
chatModel.localUserCreated.value = true
|
||||
}
|
||||
controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
|
||||
controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators)
|
||||
controller.startChat(user)
|
||||
controller.switchUIRemoteHost(null)
|
||||
close()
|
||||
@@ -204,7 +204,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: ()
|
||||
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) {
|
||||
OnboardingStage.Step2_5_SetupDatabasePassphrase
|
||||
} else {
|
||||
OnboardingStage.Step4_SetNotificationsMode
|
||||
OnboardingStage.Step3_ChooseServerOperators
|
||||
})
|
||||
} else {
|
||||
// the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen,
|
||||
|
||||
+1
-1
@@ -1249,7 +1249,7 @@ fun BoxScope.ChatItemsList(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val showAvatar = if (merged is MergedItem.Grouped) shouldShowAvatar(item, listItem.nextItem) else true
|
||||
val showAvatar = shouldShowAvatar(item, listItem.nextItem)
|
||||
val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
|
||||
val itemSeparation: ItemSeparation
|
||||
val prevItemSeparationLargeGap: Boolean
|
||||
|
||||
+22
-4
@@ -25,19 +25,21 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.AppLock
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.setConditionsNotified
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.mkValidName
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.common.views.onboarding.*
|
||||
import chat.simplex.common.views.showInvalidNameAlert
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.ConditionsLinkButton
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -115,10 +117,26 @@ fun ToggleChatListCard() {
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val rhId = chatModel.remoteHostId()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
val showWhatsNew = shouldShowWhatsNew(chatModel)
|
||||
val showUpdatedConditions = chatModel.conditions.value.conditionsAction?.shouldShowNotice ?: false
|
||||
if (showWhatsNew) {
|
||||
delay(1000L)
|
||||
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) }
|
||||
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close, updatedConditions = showUpdatedConditions) }
|
||||
} else if (showUpdatedConditions) {
|
||||
ModalManager.center.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close ->
|
||||
LaunchedEffect(Unit) {
|
||||
val conditionsId = chatModel.conditions.value.currentConditions.conditionsId
|
||||
try {
|
||||
setConditionsNotified(rh = rhId, conditionsId = conditionsId)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "UsageConditionsView setConditionsNotified error: ${e.message}")
|
||||
}
|
||||
}
|
||||
UsageConditionsView(userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), close = close, rhId = rhId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-16
@@ -48,7 +48,6 @@ import chat.simplex.common.model.localTimestamp
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.ProtocolServersView
|
||||
import chat.simplex.common.views.usersettings.SettingsPreferenceItem
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -540,15 +539,8 @@ fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant,
|
||||
)
|
||||
)
|
||||
}
|
||||
if (summary.known == true) {
|
||||
SectionItemView(click = {
|
||||
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.XFTP, close) }
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.open_server_settings_button))
|
||||
}
|
||||
if (summary.stats != null || summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
if (summary.stats != null || summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
if (summary.stats != null) {
|
||||
@@ -579,12 +571,7 @@ fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, r
|
||||
)
|
||||
)
|
||||
}
|
||||
if (summary.known == true) {
|
||||
SectionItemView(click = {
|
||||
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.SMP, close) }
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.open_server_settings_button))
|
||||
}
|
||||
if (summary.stats != null || summary.subs != null || summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
|
||||
+15
-4
@@ -17,11 +17,21 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) {
|
||||
fun AppBarTitle(
|
||||
title: String,
|
||||
hostDevice: Pair<Long?, String>? = null,
|
||||
withPadding: Boolean = true,
|
||||
bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp,
|
||||
enableAlphaChanges: Boolean = true
|
||||
) {
|
||||
val handler = LocalAppBarHandler.current
|
||||
val connection = handler?.connection
|
||||
val connection = if (enableAlphaChanges) handler?.connection else null
|
||||
LaunchedEffect(title) {
|
||||
handler?.title?.value = title
|
||||
if (enableAlphaChanges) {
|
||||
handler?.title?.value = title
|
||||
} else {
|
||||
handler?.connection?.scrollTrackingEnabled = false
|
||||
}
|
||||
}
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val titleColor = MaterialTheme.appColors.title
|
||||
@@ -54,7 +64,8 @@ fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPad
|
||||
}
|
||||
|
||||
private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) =
|
||||
if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
|
||||
if (connection?.scrollTrackingEnabled == false) 1f
|
||||
else if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f
|
||||
else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx
|
||||
|
||||
@Composable
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ class AppBarHandler(
|
||||
}
|
||||
|
||||
class CollapsingAppBarNestedScrollConnection(): NestedScrollConnection {
|
||||
var scrollTrackingEnabled = true
|
||||
var appBarOffset: Float by mutableFloatStateOf(0f)
|
||||
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
|
||||
+2
-1
@@ -258,7 +258,8 @@ private fun AppBarCenterAligned(
|
||||
}
|
||||
|
||||
private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) =
|
||||
if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
|
||||
if (!connection.scrollTrackingEnabled) 0f
|
||||
else if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f
|
||||
else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha)
|
||||
|
||||
val AppBarHeight = 56.dp
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.OnionRelatedLayout
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
package chat.simplex.common.views.onboarding
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionTextFooter
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ServerOperator
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun ModalData.ChooseServerOperators(
|
||||
onboarding: Boolean,
|
||||
close: (() -> Unit) = { ModalManager.fullscreen.closeModals() },
|
||||
modalManager: ModalManager = ModalManager.fullscreen
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
prepareChatBeforeFinishingOnboarding()
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
ModalView({}, showClose = false, endButtons = {
|
||||
IconButton({ modalManager.showModal { ChooseServerOperatorsInfoView() } }) {
|
||||
Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}) {
|
||||
val serverOperators = remember { derivedStateOf { chatModel.conditions.value.serverOperators } }
|
||||
val selectedOperatorIds = remember { stateGetOrPut("selectedOperatorIds") { serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() } }
|
||||
val selectedOperators = remember { derivedStateOf { serverOperators.value.filter { selectedOperatorIds.value.contains(it.operatorId) } } }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer),
|
||||
maxIntrinsicSize = true
|
||||
) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
AppBarTitle(stringResource(MR.strings.onboarding_choose_server_operators))
|
||||
}
|
||||
Column((
|
||||
if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier)
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_select_network_operators_to_use))
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
Column((
|
||||
if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
serverOperators.value.forEachIndexed { index, srvOperator ->
|
||||
OperatorCheckView(srvOperator, selectedOperatorIds)
|
||||
if (index != serverOperators.value.lastIndex) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.onboarding_network_operators_configure_via_settings), textAlign = TextAlign.Center)
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
val reviewForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted }
|
||||
val canReviewLater = reviewForOperators.all { it.conditionsAcceptance.usageAllowed }
|
||||
val currEnabledOperatorIds = serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet()
|
||||
|
||||
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
val enabled = selectedOperatorIds.value.isNotEmpty()
|
||||
when {
|
||||
reviewForOperators.isNotEmpty() -> ReviewConditionsButton(enabled, onboarding, selectedOperators, selectedOperatorIds, modalManager)
|
||||
selectedOperatorIds.value != currEnabledOperatorIds && enabled -> SetOperatorsButton(true, onboarding, serverOperators, selectedOperatorIds, close)
|
||||
else -> ContinueButton(enabled, onboarding, close)
|
||||
}
|
||||
if (onboarding && reviewForOperators.isEmpty()) {
|
||||
TextButtonBelowOnboardingButton(stringResource(MR.strings.operator_conditions_of_use)) {
|
||||
modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close ->
|
||||
UsageConditionsView(
|
||||
currUserServers = remember { mutableStateOf(emptyList()) },
|
||||
userServers = remember { mutableStateOf(emptyList()) },
|
||||
close = close,
|
||||
rhId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (onboarding || reviewForOperators.isEmpty()) {
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
if (!onboarding && reviewForOperators.isNotEmpty()) {
|
||||
ReviewLaterButton(canReviewLater, close)
|
||||
SectionTextFooter(
|
||||
annotatedStringResource(MR.strings.onboarding_network_operators_conditions_will_be_accepted) +
|
||||
AnnotatedString(" ") +
|
||||
annotatedStringResource(MR.strings.onboarding_network_operators_conditions_you_can_configure),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OperatorCheckView(serverOperator: ServerOperator, selectedOperatorIds: MutableState<Set<Long>>) {
|
||||
val checked = selectedOperatorIds.value.contains(serverOperator.operatorId)
|
||||
TextButton({
|
||||
if (checked) {
|
||||
selectedOperatorIds.value -= serverOperator.operatorId
|
||||
} else {
|
||||
selectedOperatorIds.value += serverOperator.operatorId
|
||||
}
|
||||
},
|
||||
border = BorderStroke(1.dp, color = if (checked) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
) {
|
||||
Row(Modifier.padding(DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(painterResource(serverOperator.largeLogo), null, Modifier.height(48.dp))
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF).weight(1f))
|
||||
CircleCheckbox(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CircleCheckbox(checked: Boolean) {
|
||||
if (checked) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_circle_filled),
|
||||
null,
|
||||
Modifier.size(26.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
Icon(
|
||||
painterResource(MR.images.ic_check_filled),
|
||||
null,
|
||||
Modifier.size(20.dp), tint = MaterialTheme.colors.background
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_circle),
|
||||
null,
|
||||
Modifier.size(26.dp),
|
||||
tint = MaterialTheme.colors.secondary.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewConditionsButton(
|
||||
enabled: Boolean,
|
||||
onboarding: Boolean,
|
||||
selectedOperators: State<List<ServerOperator>>,
|
||||
selectedOperatorIds: State<Set<Long>>,
|
||||
modalManager: ModalManager
|
||||
) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.operator_review_conditions,
|
||||
onboarding = null,
|
||||
enabled = enabled,
|
||||
onclick = {
|
||||
modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close ->
|
||||
ReviewConditionsView(onboarding, selectedOperators, selectedOperatorIds, close)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOperators: State<List<ServerOperator>>, selectedOperatorIds: State<Set<Long>>, close: () -> Unit) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.onboarding_network_operators_update,
|
||||
onboarding = null,
|
||||
enabled = enabled,
|
||||
onclick = {
|
||||
withBGApi {
|
||||
val enabledOperators = enabledOperators(serverOperators.value, selectedOperatorIds.value)
|
||||
if (enabledOperators != null) {
|
||||
val r = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators)
|
||||
if (r != null) {
|
||||
chatModel.conditions.value = r
|
||||
}
|
||||
continueToNextStep(onboarding, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(enabled: Boolean, onboarding: Boolean, close: () -> Unit) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.onboarding_network_operators_continue,
|
||||
onboarding = null,
|
||||
enabled = enabled,
|
||||
onclick = {
|
||||
continueToNextStep(onboarding, close)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewLaterButton(enabled: Boolean, close: () -> Unit) {
|
||||
TextButtonBelowOnboardingButton(
|
||||
stringResource(MR.strings.onboarding_network_operators_review_later),
|
||||
onClick = if (!enabled) null else {{ continueToNextStep(false, close) }}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewConditionsView(
|
||||
onboarding: Boolean,
|
||||
selectedOperators: State<List<ServerOperator>>,
|
||||
selectedOperatorIds: State<Set<Long>>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
// remembering both since we don't want to reload the view after the user accepts conditions
|
||||
val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } }
|
||||
val acceptForOperators = remember { selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } }
|
||||
ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false)
|
||||
if (operatorsWithConditionsAccepted.isNotEmpty()) {
|
||||
ReadableText(MR.strings.operator_conditions_accepted_for_some, args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ })
|
||||
ReadableText(MR.strings.operator_same_conditions_will_apply_to_operators, args = acceptForOperators.joinToString(", ") { it.legalName_ })
|
||||
} else {
|
||||
ReadableText(MR.strings.operator_conditions_will_be_accepted_for_some, args = acceptForOperators.joinToString(", ") { it.legalName_ })
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF)) {
|
||||
ConditionsTextView(chatModel.remoteHostId())
|
||||
}
|
||||
Column(Modifier.padding(top = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
AcceptConditionsButton(onboarding, selectedOperators, selectedOperatorIds, close)
|
||||
// Reserve space
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AcceptConditionsButton(
|
||||
onboarding: Boolean,
|
||||
selectedOperators: State<List<ServerOperator>>,
|
||||
selectedOperatorIds: State<Set<Long>>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
fun continueOnAccept() {
|
||||
if (appPlatform.isDesktop || !onboarding) {
|
||||
if (onboarding) { close() }
|
||||
continueToNextStep(onboarding, close)
|
||||
} else {
|
||||
continueToSetNotificationsAfterAccept()
|
||||
}
|
||||
}
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier,
|
||||
labelId = MR.strings.accept_conditions,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
withBGApi {
|
||||
val conditionsId = chatModel.conditions.value.currentConditions.conditionsId
|
||||
val acceptForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted }
|
||||
val operatorIds = acceptForOperators.map { it.operatorId }
|
||||
val r = chatController.acceptConditions(chatModel.remoteHostId(), conditionsId = conditionsId, operatorIds = operatorIds)
|
||||
if (r != null) {
|
||||
chatModel.conditions.value = r
|
||||
val enabledOperators = enabledOperators(r.serverOperators, selectedOperatorIds.value)
|
||||
if (enabledOperators != null) {
|
||||
val r2 = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators)
|
||||
if (r2 != null) {
|
||||
chatModel.conditions.value = r2
|
||||
continueOnAccept()
|
||||
}
|
||||
} else {
|
||||
continueOnAccept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun continueToNextStep(onboarding: Boolean, close: () -> Unit) {
|
||||
if (onboarding) {
|
||||
appPrefs.onboardingStage.set(if (appPlatform.isAndroid) OnboardingStage.Step4_SetNotificationsMode else OnboardingStage.OnboardingComplete)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueToSetNotificationsAfterAccept() {
|
||||
appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
|
||||
ModalManager.fullscreen.showModalCloseable(showClose = false) { SetNotificationsMode(chatModel) }
|
||||
}
|
||||
|
||||
private fun enabledOperators(operators: List<ServerOperator>, selectedOperatorIds: Set<Long>): List<ServerOperator>? {
|
||||
val ops = ArrayList(operators)
|
||||
if (ops.isNotEmpty()) {
|
||||
for (i in ops.indices) {
|
||||
val op = ops[i]
|
||||
ops[i] = op.copy(enabled = selectedOperatorIds.contains(op.operatorId))
|
||||
}
|
||||
val haveSMPStorage = ops.any { it.enabled && it.smpRoles.storage }
|
||||
val haveSMPProxy = ops.any { it.enabled && it.smpRoles.proxy }
|
||||
val haveXFTPStorage = ops.any { it.enabled && it.xftpRoles.storage }
|
||||
val haveXFTPProxy = ops.any { it.enabled && it.xftpRoles.proxy }
|
||||
val firstEnabledIndex = ops.indexOfFirst { it.enabled }
|
||||
if (haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy) {
|
||||
return ops
|
||||
} else if (firstEnabledIndex != -1) {
|
||||
var op = ops[firstEnabledIndex]
|
||||
if (!haveSMPStorage) op = op.copy(smpRoles = op.smpRoles.copy(storage = true))
|
||||
if (!haveSMPProxy) op = op.copy(smpRoles = op.smpRoles.copy(proxy = true))
|
||||
if (!haveXFTPStorage) op = op.copy(xftpRoles = op.xftpRoles.copy(storage = true))
|
||||
if (!haveXFTPProxy) op = op.copy(xftpRoles = op.xftpRoles.copy(proxy = true))
|
||||
ops[firstEnabledIndex] = op
|
||||
return ops
|
||||
} else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChooseServerOperatorsInfoView() {
|
||||
ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.onboarding_network_operators), withPadding = false)
|
||||
ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_different_operators))
|
||||
ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_for_routing))
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -48,8 +48,8 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>?
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), style: TextStyle = LocalTextStyle.current) {
|
||||
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp, style = style)
|
||||
fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), style: TextStyle = LocalTextStyle.current, args: Any? = null) {
|
||||
Text(annotatedStringResource(stringResId, args), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp, style = style)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ enum class OnboardingStage {
|
||||
Step2_CreateProfile,
|
||||
LinkAMobile,
|
||||
Step2_5_SetupDatabasePassphrase,
|
||||
Step3_ChooseServerOperators,
|
||||
Step3_CreateSimpleXAddress,
|
||||
Step4_SetNotificationsMode,
|
||||
OnboardingComplete
|
||||
|
||||
+4
-4
@@ -16,8 +16,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.NotificationsMode
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.changeNotificationsMode
|
||||
@@ -26,7 +25,7 @@ import chat.simplex.res.MR
|
||||
@Composable
|
||||
fun SetNotificationsMode(m: ChatModel) {
|
||||
LaunchedEffect(Unit) {
|
||||
prepareChatBeforeNotificationsSetup(m)
|
||||
prepareChatBeforeFinishingOnboarding()
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
|
||||
@@ -57,6 +56,7 @@ fun SetNotificationsMode(m: ChatModel) {
|
||||
onboarding = OnboardingStage.OnboardingComplete,
|
||||
onclick = {
|
||||
changeNotificationsMode(currentMode.value, m)
|
||||
ModalManager.fullscreen.closeModals()
|
||||
}
|
||||
)
|
||||
// Reserve space
|
||||
@@ -99,7 +99,7 @@ fun <T> SelectableCard(currentValue: State<T>, newValue: T, title: String, descr
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
|
||||
private fun prepareChatBeforeNotificationsSetup(chatModel: ChatModel) {
|
||||
fun prepareChatBeforeFinishingOnboarding() {
|
||||
// No visible users but may have hidden. In this case chat should be started anyway because it's stopped on this stage with hidden users
|
||||
if (chatModel.users.any { u -> !u.user.hidden }) return
|
||||
withBGApi {
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.database.*
|
||||
@@ -36,7 +35,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
||||
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
||||
fun nextStep() {
|
||||
if (appPlatform.isAndroid || chatModel.currentUser.value != null) {
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators)
|
||||
} else {
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile)
|
||||
}
|
||||
|
||||
+3
-2
@@ -164,14 +164,15 @@ fun OnboardingActionButton(
|
||||
@Composable
|
||||
fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) {
|
||||
val state = getKeyboardState()
|
||||
val enabled = onClick != null
|
||||
val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING)
|
||||
val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING * 2)
|
||||
if ((appPlatform.isAndroid && state.value == KeyboardState.Closed) || topPadding > 0.dp) {
|
||||
TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = onClick != null) {
|
||||
TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = enabled) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp),
|
||||
color = MaterialTheme.colors.primary,
|
||||
color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
+170
-91
@@ -8,7 +8,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -17,17 +16,32 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.setConditionsNotified
|
||||
import chat.simplex.common.model.ServerOperator.Companion.dummyOperatorInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@Composable
|
||||
fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Boolean = false, close: () -> Unit) {
|
||||
val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) }
|
||||
val rhId = chatModel.remoteHostId()
|
||||
|
||||
if (updatedConditions) {
|
||||
LaunchedEffect(Unit) {
|
||||
val conditionsId = chatModel.conditions.value.currentConditions.conditionsId
|
||||
try {
|
||||
setConditionsNotified(rh = rhId, conditionsId = conditionsId)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "WhatsNewView setConditionsNotified error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun featureDescription(icon: ImageResource?, titleId: StringResource, descrId: StringResource?, link: String?, subfeatures: List<Pair<ImageResource, StringResource>>) {
|
||||
@@ -124,9 +138,18 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
) {
|
||||
AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), withPadding = false, bottomPadding = DEFAULT_PADDING)
|
||||
|
||||
val modalManager = if (viaSettings) ModalManager.start else ModalManager.center
|
||||
|
||||
v.features.forEach { feature ->
|
||||
if (feature.show) {
|
||||
featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link, feature.subfeatures)
|
||||
when (feature) {
|
||||
is VersionFeature.FeatureDescription -> {
|
||||
if (feature.show) {
|
||||
featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link, feature.subfeatures)
|
||||
}
|
||||
}
|
||||
is VersionFeature.FeatureView -> {
|
||||
feature.view(modalManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +157,18 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
ReadMoreButton(v.post)
|
||||
}
|
||||
|
||||
if (updatedConditions) {
|
||||
Text(
|
||||
stringResource(MR.strings.view_updated_conditions),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable {
|
||||
modalManager.showModalCloseable {
|
||||
close -> UsageConditionsView(userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), close = close, rhId = rhId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!viaSettings) {
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(
|
||||
@@ -141,7 +176,9 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.ok),
|
||||
modifier = Modifier.clickable(onClick = close),
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
close()
|
||||
}),
|
||||
style = MaterialTheme.typography.h3,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
@@ -166,18 +203,26 @@ fun ReadMoreButton(url: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private data class FeatureDescription(
|
||||
val icon: ImageResource?,
|
||||
val titleId: StringResource,
|
||||
val descrId: StringResource?,
|
||||
var subfeatures: List<Pair<ImageResource, StringResource>> = listOf(),
|
||||
val link: String? = null,
|
||||
val show: Boolean = true
|
||||
)
|
||||
private sealed class VersionFeature {
|
||||
class FeatureDescription(
|
||||
val icon: ImageResource?,
|
||||
val titleId: StringResource,
|
||||
val descrId: StringResource?,
|
||||
var subfeatures: List<Pair<ImageResource, StringResource>> = listOf(),
|
||||
val link: String? = null,
|
||||
val show: Boolean = true
|
||||
): VersionFeature()
|
||||
|
||||
class FeatureView(
|
||||
val icon: ImageResource?,
|
||||
val titleId: StringResource,
|
||||
val view: @Composable (modalManager: ModalManager) -> Unit
|
||||
): VersionFeature()
|
||||
}
|
||||
|
||||
private data class VersionDescription(
|
||||
val version: String,
|
||||
val features: List<FeatureDescription>,
|
||||
val features: List<VersionFeature>,
|
||||
val post: String? = null,
|
||||
)
|
||||
|
||||
@@ -186,18 +231,18 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v4.2",
|
||||
post = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_verified_user,
|
||||
titleId = MR.strings.v4_2_security_assessment,
|
||||
descrId = MR.strings.v4_2_security_assessment_desc,
|
||||
link = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_group,
|
||||
titleId = MR.strings.v4_2_group_links,
|
||||
descrId = MR.strings.v4_2_group_links_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_check,
|
||||
titleId = MR.strings.v4_2_auto_accept_contact_requests,
|
||||
descrId = MR.strings.v4_2_auto_accept_contact_requests_desc
|
||||
@@ -208,22 +253,22 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v4.3",
|
||||
post = "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_mic,
|
||||
titleId = MR.strings.v4_3_voice_messages,
|
||||
descrId = MR.strings.v4_3_voice_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_delete_forever,
|
||||
titleId = MR.strings.v4_3_irreversible_message_deletion,
|
||||
descrId = MR.strings.v4_3_irreversible_message_deletion_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_wifi_tethering,
|
||||
titleId = MR.strings.v4_3_improved_server_configuration,
|
||||
descrId = MR.strings.v4_3_improved_server_configuration_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_visibility_off,
|
||||
titleId = MR.strings.v4_3_improved_privacy_and_security,
|
||||
descrId = MR.strings.v4_3_improved_privacy_and_security_desc
|
||||
@@ -234,22 +279,22 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v4.4",
|
||||
post = "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_timer,
|
||||
titleId = MR.strings.v4_4_disappearing_messages,
|
||||
descrId = MR.strings.v4_4_disappearing_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_pending,
|
||||
titleId = MR.strings.v4_4_live_messages,
|
||||
descrId = MR.strings.v4_4_live_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_verified_user,
|
||||
titleId = MR.strings.v4_4_verify_connection_security,
|
||||
descrId = MR.strings.v4_4_verify_connection_security_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v4_4_french_interface,
|
||||
descrId = MR.strings.v4_4_french_interface_descr
|
||||
@@ -260,33 +305,33 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v4.5",
|
||||
post = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_manage_accounts,
|
||||
titleId = MR.strings.v4_5_multiple_chat_profiles,
|
||||
descrId = MR.strings.v4_5_multiple_chat_profiles_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_edit_note,
|
||||
titleId = MR.strings.v4_5_message_draft,
|
||||
descrId = MR.strings.v4_5_message_draft_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_safety_divider,
|
||||
titleId = MR.strings.v4_5_transport_isolation,
|
||||
descrId = MR.strings.v4_5_transport_isolation_descr,
|
||||
link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation"
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_task,
|
||||
titleId = MR.strings.v4_5_private_filenames,
|
||||
descrId = MR.strings.v4_5_private_filenames_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_battery_2_bar,
|
||||
titleId = MR.strings.v4_5_reduced_battery_usage,
|
||||
descrId = MR.strings.v4_5_reduced_battery_usage_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v4_5_italian_interface,
|
||||
descrId = MR.strings.v4_5_italian_interface_descr,
|
||||
@@ -297,32 +342,32 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v4.6",
|
||||
post = "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_lock,
|
||||
titleId = MR.strings.v4_6_hidden_chat_profiles,
|
||||
descrId = MR.strings.v4_6_hidden_chat_profiles_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_flag,
|
||||
titleId = MR.strings.v4_6_group_moderation,
|
||||
descrId = MR.strings.v4_6_group_moderation_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_maps_ugc,
|
||||
titleId = MR.strings.v4_6_group_welcome_message,
|
||||
descrId = MR.strings.v4_6_group_welcome_message_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_call,
|
||||
titleId = MR.strings.v4_6_audio_video_calls,
|
||||
descrId = MR.strings.v4_6_audio_video_calls_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_battery_3_bar,
|
||||
titleId = MR.strings.v4_6_reduced_battery_usage,
|
||||
descrId = MR.strings.v4_6_reduced_battery_usage_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v4_6_chinese_spanish_interface,
|
||||
descrId = MR.strings.v4_6_chinese_spanish_interface_descr,
|
||||
@@ -333,17 +378,17 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.0",
|
||||
post = "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_upload_file,
|
||||
titleId = MR.strings.v5_0_large_files_support,
|
||||
descrId = MR.strings.v5_0_large_files_support_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_lock,
|
||||
titleId = MR.strings.v5_0_app_passcode,
|
||||
descrId = MR.strings.v5_0_app_passcode_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v5_0_polish_interface,
|
||||
descrId = MR.strings.v5_0_polish_interface_descr,
|
||||
@@ -354,27 +399,27 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.1",
|
||||
post = "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_add_reaction,
|
||||
titleId = MR.strings.v5_1_message_reactions,
|
||||
descrId = MR.strings.v5_1_message_reactions_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_chat,
|
||||
titleId = MR.strings.v5_1_better_messages,
|
||||
descrId = MR.strings.v5_1_better_messages_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_light_mode,
|
||||
titleId = MR.strings.v5_1_custom_themes,
|
||||
descrId = MR.strings.v5_1_custom_themes_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_lock,
|
||||
titleId = MR.strings.v5_1_self_destruct_passcode,
|
||||
descrId = MR.strings.v5_1_self_destruct_passcode_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v5_1_japanese_portuguese_interface,
|
||||
descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate,
|
||||
@@ -385,27 +430,27 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.2",
|
||||
post = "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_check,
|
||||
titleId = MR.strings.v5_2_message_delivery_receipts,
|
||||
descrId = MR.strings.v5_2_message_delivery_receipts_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_star,
|
||||
titleId = MR.strings.v5_2_favourites_filter,
|
||||
descrId = MR.strings.v5_2_favourites_filter_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_sync_problem,
|
||||
titleId = MR.strings.v5_2_fix_encryption,
|
||||
descrId = MR.strings.v5_2_fix_encryption_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_timer,
|
||||
titleId = MR.strings.v5_2_disappear_one_message,
|
||||
descrId = MR.strings.v5_2_disappear_one_message_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_redeem,
|
||||
titleId = MR.strings.v5_2_more_things,
|
||||
descrId = MR.strings.v5_2_more_things_descr
|
||||
@@ -416,29 +461,29 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.3",
|
||||
post = "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_desktop,
|
||||
titleId = MR.strings.v5_3_new_desktop_app,
|
||||
descrId = MR.strings.v5_3_new_desktop_app_descr,
|
||||
link = "https://simplex.chat/downloads/"
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_lock,
|
||||
titleId = MR.strings.v5_3_encrypt_local_files,
|
||||
descrId = MR.strings.v5_3_encrypt_local_files_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_search,
|
||||
titleId = MR.strings.v5_3_discover_join_groups,
|
||||
descrId = MR.strings.v5_3_discover_join_groups_descr,
|
||||
link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_theater_comedy,
|
||||
titleId = MR.strings.v5_3_simpler_incognito_mode,
|
||||
descrId = MR.strings.v5_3_simpler_incognito_mode_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v5_3_new_interface_languages,
|
||||
descrId = MR.strings.v5_3_new_interface_languages_descr,
|
||||
@@ -449,27 +494,27 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.4",
|
||||
post = "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_desktop,
|
||||
titleId = MR.strings.v5_4_link_mobile_desktop,
|
||||
descrId = MR.strings.v5_4_link_mobile_desktop_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_group,
|
||||
titleId = MR.strings.v5_4_better_groups,
|
||||
descrId = MR.strings.v5_4_better_groups_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_theater_comedy,
|
||||
titleId = MR.strings.v5_4_incognito_groups,
|
||||
descrId = MR.strings.v5_4_incognito_groups_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_back_hand,
|
||||
titleId = MR.strings.v5_4_block_group_members,
|
||||
descrId = MR.strings.v5_4_block_group_members_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_redeem,
|
||||
titleId = MR.strings.v5_2_more_things,
|
||||
descrId = MR.strings.v5_4_more_things_descr
|
||||
@@ -480,28 +525,28 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.5",
|
||||
post = "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_folder_pen,
|
||||
titleId = MR.strings.v5_5_private_notes,
|
||||
descrId = MR.strings.v5_5_private_notes_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_link,
|
||||
titleId = MR.strings.v5_5_simpler_connect_ui,
|
||||
descrId = MR.strings.v5_5_simpler_connect_ui_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_forum,
|
||||
titleId = MR.strings.v5_5_join_group_conversation,
|
||||
descrId = MR.strings.v5_5_join_group_conversation_descr,
|
||||
link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_battery_3_bar,
|
||||
titleId = MR.strings.v5_5_message_delivery,
|
||||
descrId = MR.strings.v5_5_message_delivery_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v5_5_new_interface_languages,
|
||||
descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate,
|
||||
@@ -512,22 +557,22 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.6",
|
||||
post = "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_vpn_key_filled,
|
||||
titleId = MR.strings.v5_6_quantum_resistant_encryption,
|
||||
descrId = MR.strings.v5_6_quantum_resistant_encryption_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_ios_share,
|
||||
titleId = MR.strings.v5_6_app_data_migration,
|
||||
descrId = MR.strings.v5_6_app_data_migration_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_call,
|
||||
titleId = MR.strings.v5_6_picture_in_picture_calls,
|
||||
descrId = MR.strings.v5_6_picture_in_picture_calls_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_back_hand,
|
||||
titleId = MR.strings.v5_6_safer_groups,
|
||||
descrId = MR.strings.v5_6_safer_groups_descr
|
||||
@@ -538,32 +583,32 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.7",
|
||||
post = "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_vpn_key_filled,
|
||||
titleId = MR.strings.v5_6_quantum_resistant_encryption,
|
||||
descrId = MR.strings.v5_7_quantum_resistant_encryption_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_forward,
|
||||
titleId = MR.strings.v5_7_forward,
|
||||
descrId = MR.strings.v5_7_forward_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_music_note,
|
||||
titleId = MR.strings.v5_7_call_sounds,
|
||||
descrId = MR.strings.v5_7_call_sounds_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_account_box,
|
||||
titleId = MR.strings.v5_7_shape_profile_images,
|
||||
descrId = MR.strings.v5_7_shape_profile_images_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_wifi_tethering,
|
||||
titleId = MR.strings.v5_7_network,
|
||||
descrId = MR.strings.v5_7_network_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v5_7_new_interface_languages,
|
||||
descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate,
|
||||
@@ -574,27 +619,27 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v5.8",
|
||||
post = "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_settings_ethernet,
|
||||
titleId = MR.strings.v5_8_private_routing,
|
||||
descrId = MR.strings.v5_8_private_routing_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_palette,
|
||||
titleId = MR.strings.v5_8_chat_themes,
|
||||
descrId = MR.strings.v5_8_chat_themes_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_security,
|
||||
titleId = MR.strings.v5_8_safe_files,
|
||||
descrId = MR.strings.v5_8_safe_files_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_battery_3_bar,
|
||||
titleId = MR.strings.v5_8_message_delivery,
|
||||
descrId = MR.strings.v5_8_message_delivery_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_translate,
|
||||
titleId = MR.strings.v5_8_persian_ui,
|
||||
descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate
|
||||
@@ -605,7 +650,7 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v6.0",
|
||||
post = "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = null,
|
||||
titleId = MR.strings.v6_0_new_chat_experience,
|
||||
descrId = null,
|
||||
@@ -616,7 +661,7 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
MR.images.ic_match_case to MR.strings.v6_0_increase_font_size
|
||||
)
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = null,
|
||||
titleId = MR.strings.v6_0_new_media_options,
|
||||
descrId = null,
|
||||
@@ -625,23 +670,23 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
MR.images.ic_blur_on to MR.strings.v6_0_privacy_blur,
|
||||
)
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_toast,
|
||||
titleId = MR.strings.v6_0_reachable_chat_toolbar,
|
||||
descrId = MR.strings.v6_0_reachable_chat_toolbar_descr,
|
||||
show = appPlatform.isAndroid
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_settings_ethernet,
|
||||
titleId = MR.strings.v5_8_private_routing,
|
||||
descrId = MR.strings.v6_0_private_routing_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_wifi_tethering,
|
||||
titleId = MR.strings.v6_0_connection_servers_status,
|
||||
descrId = MR.strings.v6_0_connection_servers_status_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_upgrade,
|
||||
titleId = MR.strings.v6_0_upgrade_app,
|
||||
descrId = MR.strings.v6_0_upgrade_app_descr,
|
||||
@@ -653,18 +698,18 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
version = "v6.1",
|
||||
post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_verified_user,
|
||||
titleId = MR.strings.v6_1_better_security,
|
||||
descrId = MR.strings.v6_1_better_security_descr,
|
||||
link = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_videocam,
|
||||
titleId = MR.strings.v6_1_better_calls,
|
||||
descrId = MR.strings.v6_1_better_calls_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = null,
|
||||
titleId = MR.strings.v6_1_better_user_experience,
|
||||
descrId = null,
|
||||
@@ -678,6 +723,39 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
),
|
||||
),
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v6.2 (beta.1)",
|
||||
post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html",
|
||||
features = listOf(
|
||||
VersionFeature.FeatureView(
|
||||
icon = null,
|
||||
titleId = MR.strings.v6_2_network_decentralization,
|
||||
view = { modalManager ->
|
||||
Column {
|
||||
val src = (operatorsInfo[OperatorTag.Flux] ?: dummyOperatorInfo).largeLogo
|
||||
Image(painterResource(src), null, modifier = Modifier.height(48.dp))
|
||||
Text(stringResource(MR.strings.v6_2_network_decentralization_descr), modifier = Modifier.padding(top = 8.dp))
|
||||
Row {
|
||||
Text(
|
||||
stringResource(MR.strings.v6_2_network_decentralization_enable_flux),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable {
|
||||
modalManager.showModalCloseable { close -> ChooseServerOperators(onboarding = false, close, modalManager) }
|
||||
}
|
||||
)
|
||||
Text(" ")
|
||||
Text(stringResource(MR.strings.v6_2_network_decentralization_enable_flux_reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_chat,
|
||||
titleId = MR.strings.v6_2_improved_chat_navigation,
|
||||
descrId = MR.strings.v6_2_improved_chat_navigation_descr
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
@@ -700,7 +778,8 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean {
|
||||
@Composable
|
||||
fun PreviewWhatsNewView() {
|
||||
SimpleXTheme {
|
||||
WhatsNewView(
|
||||
val data = remember { ModalData() }
|
||||
data.WhatsNewView(
|
||||
viaSettings = true,
|
||||
close = {}
|
||||
)
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCode
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.validPort
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
-383
@@ -1,383 +0,0 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
|
||||
var presetServers by remember(rhId) { mutableStateOf(emptyList<ServerCfg>()) }
|
||||
var servers by remember { stateGetOrPut("servers") { emptyList<ServerCfg>() } }
|
||||
var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } }
|
||||
val currServers = remember(rhId) { mutableStateOf(servers) }
|
||||
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
|
||||
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
|
||||
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
|
||||
val saveDisabled = remember(servers) {
|
||||
derivedStateOf {
|
||||
servers.isEmpty() ||
|
||||
servers == currServers.value ||
|
||||
testing.value ||
|
||||
servers.none { srv ->
|
||||
val address = parseServerAddress(srv.server)
|
||||
address != null && uniqueAddress(srv, address, servers)
|
||||
} ||
|
||||
allServersDisabled.value
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(rhId) {
|
||||
servers = emptyList()
|
||||
serversAlreadyLoaded = false
|
||||
}
|
||||
|
||||
LaunchedEffect(rhId) {
|
||||
withApi {
|
||||
val res = m.controller.getUserProtoServers(rhId, serverProtocol)
|
||||
if (res != null) {
|
||||
currServers.value = res.protoServers
|
||||
presetServers = res.presetServers
|
||||
if (servers.isEmpty() && !serversAlreadyLoaded) {
|
||||
servers = currServers.value
|
||||
serversAlreadyLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val testServersJob = CancellableOnGoneJob()
|
||||
fun showServer(server: ServerCfg) {
|
||||
ModalManager.start.showModalCloseable(true) { close ->
|
||||
var old by remember { mutableStateOf(server) }
|
||||
val index = servers.indexOf(old)
|
||||
ProtocolServerView(
|
||||
m,
|
||||
old,
|
||||
serverProtocol,
|
||||
onUpdate = { updated ->
|
||||
val newServers = ArrayList(servers)
|
||||
newServers.removeAt(index)
|
||||
newServers.add(index, updated)
|
||||
old = updated
|
||||
servers = newServers
|
||||
},
|
||||
onDelete = {
|
||||
val newServers = ArrayList(servers)
|
||||
newServers.removeAt(index)
|
||||
servers = newServers
|
||||
close()
|
||||
})
|
||||
}
|
||||
}
|
||||
ModalView(
|
||||
close = {
|
||||
if (saveDisabled.value) close()
|
||||
else showUnsavedChangesAlert({ saveServers(rhId, serverProtocol, currServers, servers, m, close) }, close)
|
||||
},
|
||||
) {
|
||||
ProtocolServersLayout(
|
||||
serverProtocol,
|
||||
testing = testing.value,
|
||||
servers = servers,
|
||||
serversUnchanged = serversUnchanged.value,
|
||||
saveDisabled = saveDisabled.value,
|
||||
allServersDisabled = allServersDisabled.value,
|
||||
m.currentUser.value,
|
||||
addServer = {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.smp_servers_add),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = servers + ServerCfg.empty
|
||||
// No saving until something will be changed on the next screen to prevent blank servers on the list
|
||||
showServer(servers.last())
|
||||
}) {
|
||||
Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ScanProtocolServer(rhId) {
|
||||
close()
|
||||
servers = servers + it
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
val hasAllPresets = hasAllPresets(presetServers, servers, m)
|
||||
if (!hasAllPresets) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = (servers + addAllPresets(rhId, presetServers, servers, m)).sortedByDescending { it.preset }
|
||||
}) {
|
||||
Text(stringResource(MR.strings.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
testServers = {
|
||||
testServersJob.value = withLongRunningApi {
|
||||
testServers(testing, servers, m) {
|
||||
servers = it
|
||||
}
|
||||
}
|
||||
},
|
||||
resetServers = {
|
||||
servers = currServers.value
|
||||
},
|
||||
saveSMPServers = {
|
||||
saveServers(rhId, serverProtocol, currServers, servers, m)
|
||||
},
|
||||
showServer = ::showServer,
|
||||
)
|
||||
|
||||
if (testing.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProtocolServersLayout(
|
||||
serverProtocol: ServerProtocol,
|
||||
testing: Boolean,
|
||||
servers: List<ServerCfg>,
|
||||
serversUnchanged: Boolean,
|
||||
saveDisabled: Boolean,
|
||||
allServersDisabled: Boolean,
|
||||
currentUser: User?,
|
||||
addServer: () -> Unit,
|
||||
testServers: () -> Unit,
|
||||
resetServers: () -> Unit,
|
||||
saveSMPServers: () -> Unit,
|
||||
showServer: (ServerCfg) -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers))
|
||||
|
||||
val configuredServers = servers.filter { it.preset || it.enabled }
|
||||
val otherServers = servers.filter { !(it.preset || it.enabled) }
|
||||
|
||||
if (configuredServers.isNotEmpty()) {
|
||||
SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_configured else MR.strings.xftp_servers_configured).uppercase()) {
|
||||
for (srv in configuredServers) {
|
||||
SectionItemView({ showServer(srv) }, disabled = testing) {
|
||||
ProtocolServerView(serverProtocol, srv, servers, testing)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.smp_servers_per_user) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
}
|
||||
|
||||
if (otherServers.isNotEmpty()) {
|
||||
SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_other else MR.strings.xftp_servers_other).uppercase()) {
|
||||
for (srv in otherServers.filter { !(it.preset || it.enabled) }) {
|
||||
SectionItemView({ showServer(srv) }, disabled = testing) {
|
||||
ProtocolServerView(serverProtocol, srv, servers, testing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionView {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_add),
|
||||
stringResource(MR.strings.smp_servers_add),
|
||||
addServer,
|
||||
disabled = testing,
|
||||
textColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
iconColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
}
|
||||
|
||||
SectionView {
|
||||
SectionItemView(resetServers, disabled = serversUnchanged) {
|
||||
Text(stringResource(MR.strings.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
}
|
||||
val testServersDisabled = testing || allServersDisabled
|
||||
SectionItemView(testServers, disabled = testServersDisabled) {
|
||||
Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
}
|
||||
SectionItemView(saveSMPServers, disabled = saveDisabled) {
|
||||
Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionView {
|
||||
HowToButton()
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
|
||||
val address = parseServerAddress(srv.server)
|
||||
when {
|
||||
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
|
||||
!srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
|
||||
else -> ShowTestStatus(srv)
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
val text = address?.hostnames?.firstOrNull() ?: srv.server
|
||||
if (srv.enabled) {
|
||||
Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1)
|
||||
} else {
|
||||
Text(text, maxLines = 1, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowToButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_open_in_new),
|
||||
stringResource(MR.strings.how_to_use_your_servers),
|
||||
{ uriHandler.openUriCatching("https://simplex.chat/docs/server.html") },
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InvalidServer() {
|
||||
Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error)
|
||||
}
|
||||
|
||||
private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<ServerCfg>): Boolean = servers.all { srv ->
|
||||
address.hostnames.all { host ->
|
||||
srv.id == s.id || !srv.server.contains(host)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllPresets(presetServers: List<ServerCfg>, servers: List<ServerCfg>, m: ChatModel): Boolean =
|
||||
presetServers.all { hasPreset(it, servers) } ?: true
|
||||
|
||||
private fun addAllPresets(rhId: Long?, presetServers: List<ServerCfg>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
|
||||
val toAdd = ArrayList<ServerCfg>()
|
||||
for (srv in presetServers) {
|
||||
if (!hasPreset(srv, servers)) {
|
||||
toAdd.add(srv)
|
||||
}
|
||||
}
|
||||
return toAdd
|
||||
}
|
||||
|
||||
private fun hasPreset(srv: ServerCfg, servers: List<ServerCfg>): Boolean =
|
||||
servers.any { it.server == srv.server }
|
||||
|
||||
private suspend fun testServers(testing: MutableState<Boolean>, servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit) {
|
||||
val resetStatus = resetTestStatus(servers)
|
||||
onUpdated(resetStatus)
|
||||
testing.value = true
|
||||
val fs = runServersTest(resetStatus, m) { onUpdated(it) }
|
||||
testing.value = false
|
||||
if (fs.isNotEmpty()) {
|
||||
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_test_failed),
|
||||
text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
|
||||
val copy = ArrayList(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
copy.removeAt(index)
|
||||
copy.add(index, server.copy(tested = null))
|
||||
}
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, ProtocolTestFailure> {
|
||||
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
|
||||
val updatedServers = ArrayList<ServerCfg>(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
interruptIfCancelled()
|
||||
val (updatedServer, f) = testServerConnection(server, m)
|
||||
updatedServers.removeAt(index)
|
||||
updatedServers.add(index, updatedServer)
|
||||
// toList() is important. Otherwise, Compose will not redraw the screen after first update
|
||||
onUpdated(updatedServers.toList())
|
||||
if (f != null) {
|
||||
fs[serverHostname(updatedServer.server)] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
|
||||
withBGApi {
|
||||
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
|
||||
currServers.value = servers
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.smp_save_servers_question),
|
||||
confirmText = generalGetString(MR.strings.save_verb),
|
||||
dismissText = generalGetString(MR.strings.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
}
|
||||
+3
-4
@@ -25,14 +25,13 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.CreateProfile
|
||||
import chat.simplex.common.views.database.DatabaseView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.migration.MigrateFromDeviceView
|
||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.NetworkAndServersView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) {
|
||||
@@ -102,7 +101,7 @@ fun SettingsLayout(
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) })
|
||||
@@ -118,7 +117,7 @@ fun SettingsLayout(
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_help)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close = close) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
@@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.usersettings.SettingsPreferenceItem
|
||||
import chat.simplex.res.MR
|
||||
import java.text.DecimalFormat
|
||||
|
||||
+465
-75
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
@@ -20,119 +21,245 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.getServerOperators
|
||||
import chat.simplex.common.model.ChatController.getUserServers
|
||||
import chat.simplex.common.model.ChatController.setUserServers
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.common.views.onboarding.ReadableText
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NetworkAndServersView() {
|
||||
fun ModalData.NetworkAndServersView(close: () -> Unit) {
|
||||
val currentRemoteHost by remember { chatModel.currentRemoteHost }
|
||||
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
|
||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
val currUserServers = remember { stateGetOrPut("currUserServers") { emptyList<UserOperatorServers>() } }
|
||||
val userServers = remember { stateGetOrPut("userServers") { emptyList<UserOperatorServers>() } }
|
||||
val serverErrors = remember { stateGetOrPut("serverErrors") { emptyList<UserServersError>() } }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } }
|
||||
NetworkAndServersLayout(
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
|
||||
toggleSocksProxy = { enable ->
|
||||
val def = NetCfg.defaults
|
||||
val proxyDef = NetCfg.proxyDefaults
|
||||
if (enable) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.network_enable_socks),
|
||||
text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value),
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get())
|
||||
if (conf.tcpConnectTimeout == def.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout)
|
||||
}
|
||||
if (conf.tcpTimeout == def.tcpTimeout) {
|
||||
conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout)
|
||||
}
|
||||
if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) {
|
||||
conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb)
|
||||
}
|
||||
if (conf.rcvConcurrency == def.rcvConcurrency) {
|
||||
conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency)
|
||||
}
|
||||
chatModel.controller.apiSetNetworkConfig(conf)
|
||||
chatModel.controller.setNetCfg(conf)
|
||||
networkUseSocksProxy.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
ModalView(
|
||||
close = {
|
||||
if (!serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)) {
|
||||
close()
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.network_disable_socks),
|
||||
text = generalGetString(MR.strings.network_disable_socks_info),
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
var conf = controller.getNetCfg().copy(socksProxy = null)
|
||||
if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout)
|
||||
}
|
||||
if (conf.tcpTimeout == proxyDef.tcpTimeout) {
|
||||
conf = conf.copy(tcpTimeout = def.tcpTimeout)
|
||||
}
|
||||
if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) {
|
||||
conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb)
|
||||
}
|
||||
if (conf.rcvConcurrency == proxyDef.rcvConcurrency) {
|
||||
conf = conf.copy(rcvConcurrency = def.rcvConcurrency)
|
||||
}
|
||||
chatModel.controller.apiSetNetworkConfig(conf)
|
||||
chatModel.controller.setNetCfg(conf)
|
||||
networkUseSocksProxy.value = false
|
||||
}
|
||||
}
|
||||
showUnsavedChangesAlert(
|
||||
{ scope.launch { saveServers(currentRemoteHost?.remoteHostId, currUserServers, userServers) }},
|
||||
close
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
NetworkAndServersLayout(
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
|
||||
currUserServers = currUserServers,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
toggleSocksProxy = { enable ->
|
||||
val def = NetCfg.defaults
|
||||
val proxyDef = NetCfg.proxyDefaults
|
||||
if (enable) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.network_enable_socks),
|
||||
text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value),
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get())
|
||||
if (conf.tcpConnectTimeout == def.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout)
|
||||
}
|
||||
if (conf.tcpTimeout == def.tcpTimeout) {
|
||||
conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout)
|
||||
}
|
||||
if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) {
|
||||
conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb)
|
||||
}
|
||||
if (conf.rcvConcurrency == def.rcvConcurrency) {
|
||||
conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency)
|
||||
}
|
||||
chatModel.controller.apiSetNetworkConfig(conf)
|
||||
chatModel.controller.setNetCfg(conf)
|
||||
networkUseSocksProxy.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.network_disable_socks),
|
||||
text = generalGetString(MR.strings.network_disable_socks_info),
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
var conf = controller.getNetCfg().copy(socksProxy = null)
|
||||
if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout)
|
||||
}
|
||||
if (conf.tcpTimeout == proxyDef.tcpTimeout) {
|
||||
conf = conf.copy(tcpTimeout = def.tcpTimeout)
|
||||
}
|
||||
if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) {
|
||||
conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb)
|
||||
}
|
||||
if (conf.rcvConcurrency == proxyDef.rcvConcurrency) {
|
||||
conf = conf.copy(rcvConcurrency = def.rcvConcurrency)
|
||||
}
|
||||
chatModel.controller.apiSetNetworkConfig(conf)
|
||||
chatModel.controller.setNetCfg(conf)
|
||||
networkUseSocksProxy.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun NetworkAndServersLayout(
|
||||
currentRemoteHost: RemoteHostInfo?,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
val conditionsAction = remember { m.conditions.value.conditionsAction }
|
||||
val anyOperatorEnabled = remember { derivedStateOf { userServers.value.any { it.operator?.enabled == true } } }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (currUserServers.value.isNotEmpty() || userServers.value.isNotEmpty()) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
try {
|
||||
val servers = getUserServers(rh = currentRemoteHost?.remoteHostId)
|
||||
if (servers != null) {
|
||||
currUserServers.value = servers
|
||||
userServers.value = servers
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConditionsButton(conditionsAction: UsageConditionsAction, rhId: Long?) {
|
||||
SectionItemView(
|
||||
click = { ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> UsageConditionsView(currUserServers, userServers, close, rhId) } },
|
||||
) {
|
||||
Text(
|
||||
stringResource(if (conditionsAction is UsageConditionsAction.Review) MR.strings.operator_review_conditions else MR.strings.operator_conditions_accepted),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }}
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
|
||||
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) } }
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
||||
// TODO: Review this and socks.
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.message_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } })
|
||||
SectionView(generalGetString(MR.strings.network_preset_servers_title).uppercase()) {
|
||||
userServers.value.forEachIndexed { index, srv ->
|
||||
srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, currentRemoteHost?.remoteHostId) }
|
||||
}
|
||||
}
|
||||
if (conditionsAction != null && anyOperatorEnabled.value) {
|
||||
ConditionsButton(conditionsAction, rhId = currentRemoteHost?.remoteHostId)
|
||||
}
|
||||
val footerText = if (conditionsAction is UsageConditionsAction.Review && conditionsAction.deadline != null && anyOperatorEnabled.value) {
|
||||
String.format(generalGetString(MR.strings.operator_conditions_will_be_accepted_on), localDate(conditionsAction.deadline))
|
||||
} else null
|
||||
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.media_and_file_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
|
||||
if (footerText != null) {
|
||||
SectionTextFooter(footerText)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) }})
|
||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } })
|
||||
if (networkUseSocksProxy.value) {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
} else {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||
val nullOperatorIndex = userServers.value.indexOfFirst { it.operator == null }
|
||||
|
||||
if (nullOperatorIndex != -1) {
|
||||
SectionItemView({
|
||||
ModalManager.start.showModal {
|
||||
YourServersView(
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
operatorIndex = nullOperatorIndex,
|
||||
rhId = currentRemoteHost?.remoteHostId
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_dns),
|
||||
stringResource(MR.strings.your_servers),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
TextIconSpaced()
|
||||
Text(stringResource(MR.strings.your_servers), color = MaterialTheme.colors.onBackground)
|
||||
|
||||
if (currUserServers.value.getOrNull(nullOperatorIndex) != userServers.value.getOrNull(nullOperatorIndex)) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
UnsavedChangesIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) } })
|
||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } })
|
||||
if (networkUseSocksProxy.value) {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
} else {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)
|
||||
|
||||
SectionItemView(
|
||||
{ scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } },
|
||||
disabled = saveDisabled,
|
||||
) {
|
||||
Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
}
|
||||
val serversErr = globalServersError(serverErrors.value)
|
||||
if (serversErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(serversErr)
|
||||
}
|
||||
} else if (serverErrors.value.isNotEmpty()) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(generalGetString(MR.strings.errors_in_servers_configuration))
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
||||
@@ -504,6 +631,165 @@ fun showWrongProxyConfigAlert() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable()
|
||||
private fun ServerOperatorRow(
|
||||
index: Int,
|
||||
operator: ServerOperator,
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
rhId: Long?
|
||||
) {
|
||||
SectionItemView(
|
||||
{
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
OperatorView(
|
||||
currUserServers,
|
||||
userServers,
|
||||
serverErrors,
|
||||
index,
|
||||
rhId
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painterResource(operator.logo),
|
||||
operator.tradeName,
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = if (operator.enabled) null else ColorFilter.colorMatrix(ColorMatrix().apply {
|
||||
setToSaturation(0f)
|
||||
})
|
||||
)
|
||||
TextIconSpaced()
|
||||
Text(operator.tradeName, color = if (operator.enabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
|
||||
if (currUserServers.value.getOrNull(index) != userServers.value.getOrNull(index)) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
UnsavedChangesIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnsavedChangesIndicator() {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_edit_filled),
|
||||
stringResource(MR.strings.icon_descr_edited),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UsageConditionsView(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
close: () -> Unit,
|
||||
rhId: Long?
|
||||
) {
|
||||
suspend fun acceptForOperators(rhId: Long?, operatorIds: List<Long>, close: () -> Unit) {
|
||||
try {
|
||||
val conditionsId = chatModel.conditions.value.currentConditions.conditionsId
|
||||
val r = chatController.acceptConditions(rhId, conditionsId, operatorIds) ?: return
|
||||
chatModel.conditions.value = r
|
||||
updateOperatorsConditionsAcceptance(currUserServers, r.serverOperators)
|
||||
updateOperatorsConditionsAcceptance(userServers, r.serverOperators)
|
||||
close()
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, ex.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AcceptConditionsButton(operatorIds: List<Long>, close: () -> Unit, bottomPadding: Dp = DEFAULT_PADDING * 2) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(Modifier.fillMaxWidth().padding(bottom = bottomPadding), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
OnboardingActionButton(
|
||||
labelId = MR.strings.accept_conditions,
|
||||
onboarding = null,
|
||||
enabled = operatorIds.isNotEmpty(),
|
||||
onclick = {
|
||||
scope.launch {
|
||||
acceptForOperators(rhId, operatorIds, close)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false)
|
||||
when (val conditionsAction = chatModel.conditions.value.conditionsAction) {
|
||||
is UsageConditionsAction.Review -> {
|
||||
if (conditionsAction.operators.isNotEmpty()) {
|
||||
ReadableText(MR.strings.operators_conditions_will_be_accepted_for, args = conditionsAction.operators.joinToString(", ") { it.legalName_ })
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) {
|
||||
ConditionsTextView(rhId)
|
||||
}
|
||||
AcceptConditionsButton(conditionsAction.operators.map { it.operatorId }, close, if (conditionsAction.deadline != null) DEFAULT_PADDING_HALF else DEFAULT_PADDING * 2)
|
||||
if (conditionsAction.deadline != null) {
|
||||
SectionTextFooter(
|
||||
text = AnnotatedString(String.format(generalGetString(MR.strings.operator_conditions_accepted_for_enabled_operators_on), localDate(conditionsAction.deadline))),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().height(DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
|
||||
is UsageConditionsAction.Accepted -> {
|
||||
if (conditionsAction.operators.isNotEmpty()) {
|
||||
ReadableText(MR.strings.operators_conditions_accepted_for, args = conditionsAction.operators.joinToString(", ") { it.legalName_ })
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) {
|
||||
ConditionsTextView(rhId)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) {
|
||||
ConditionsTextView(rhId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServersErrorFooter(errStr: String) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_error),
|
||||
contentDescription = stringResource(MR.strings.server_error),
|
||||
tint = Color.Red,
|
||||
modifier = Modifier
|
||||
.size(19.sp.toDp())
|
||||
.offset(x = 2.sp.toDp())
|
||||
)
|
||||
TextIconSpaced()
|
||||
Text(
|
||||
errStr,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
lineHeight = 18.sp,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.smp_save_servers_question),
|
||||
confirmText = generalGetString(MR.strings.save_verb),
|
||||
dismissText = generalGetString(MR.strings.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
}
|
||||
|
||||
fun showUpdateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
@@ -521,6 +807,107 @@ fun showUpdateNetworkSettingsDialog(
|
||||
)
|
||||
}
|
||||
|
||||
fun updateOperatorsConditionsAcceptance(usvs: MutableState<List<UserOperatorServers>>, updatedOperators: List<ServerOperator>) {
|
||||
val modified = ArrayList(usvs.value)
|
||||
for (i in modified.indices) {
|
||||
val updatedOperator = updatedOperators.firstOrNull { it.operatorId == modified[i].operator?.operatorId } ?: continue
|
||||
modified[i] = modified[i].copy(operator = modified[i].operator?.copy(conditionsAcceptance = updatedOperator.conditionsAcceptance))
|
||||
}
|
||||
usvs.value = modified
|
||||
}
|
||||
|
||||
suspend fun validateServers_(
|
||||
rhId: Long?,
|
||||
userServersToValidate: List<UserOperatorServers>,
|
||||
serverErrors: MutableState<List<UserServersError>>
|
||||
) {
|
||||
try {
|
||||
val errors = chatController.validateServers(rhId, userServersToValidate) ?: return
|
||||
serverErrors.value = errors
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, ex.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
fun serversCanBeSaved(
|
||||
currUserServers: List<UserOperatorServers>,
|
||||
userServers: List<UserOperatorServers>,
|
||||
serverErrors: List<UserServersError>
|
||||
): Boolean {
|
||||
return userServers != currUserServers && serverErrors.isEmpty()
|
||||
}
|
||||
|
||||
fun globalServersError(serverErrors: List<UserServersError>): String? {
|
||||
for (err in serverErrors) {
|
||||
if (err.globalError != null) {
|
||||
return err.globalError
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun globalSMPServersError(serverErrors: List<UserServersError>): String? {
|
||||
for (err in serverErrors) {
|
||||
if (err.globalSMPError != null) {
|
||||
return err.globalSMPError
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun globalXFTPServersError(serverErrors: List<UserServersError>): String? {
|
||||
for (err in serverErrors) {
|
||||
if (err.globalXFTPError != null) {
|
||||
return err.globalXFTPError
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun findDuplicateHosts(serverErrors: List<UserServersError>): Set<String> {
|
||||
val duplicateHostsList = serverErrors.mapNotNull { err ->
|
||||
if (err is UserServersError.DuplicateServer) {
|
||||
err.duplicateHost
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
return duplicateHostsList.toSet()
|
||||
}
|
||||
|
||||
private suspend fun saveServers(
|
||||
rhId: Long?,
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>
|
||||
) {
|
||||
val userServersToSave = userServers.value
|
||||
try {
|
||||
val set = setUserServers(rhId, userServersToSave)
|
||||
|
||||
if (set) {
|
||||
// Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers)
|
||||
val updatedServers = getUserServers(rhId)
|
||||
// Get updated operators to update model
|
||||
val updatedOperators = getServerOperators(rhId)
|
||||
|
||||
if (updatedOperators != null) {
|
||||
chatModel.conditions.value = updatedOperators
|
||||
}
|
||||
|
||||
if (updatedServers != null ) {
|
||||
currUserServers.value = updatedServers
|
||||
userServers.value = updatedServers
|
||||
} else {
|
||||
currUserServers.value = userServersToSave
|
||||
}
|
||||
} else {
|
||||
currUserServers.value = userServersToSave
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, ex.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewNetworkAndServersLayout() {
|
||||
@@ -530,6 +917,9 @@ fun PreviewNetworkAndServersLayout() {
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
toggleSocksProxy = {},
|
||||
currUserServers = remember { mutableStateOf(emptyList()) },
|
||||
userServers = remember { mutableStateOf(emptyList()) },
|
||||
serverErrors = remember { mutableStateOf(emptyList()) }
|
||||
)
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun ModalData.NewServerView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val testing = remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val newServer = remember { mutableStateOf(UserServer.empty) }
|
||||
|
||||
ModalView(close = {
|
||||
addServer(
|
||||
scope,
|
||||
newServer.value,
|
||||
userServers,
|
||||
serverErrors,
|
||||
rhId,
|
||||
close = close
|
||||
)
|
||||
}) {
|
||||
Box {
|
||||
NewServerLayout(
|
||||
newServer,
|
||||
testing.value,
|
||||
testServer = {
|
||||
testing.value = true
|
||||
withLongRunningApi {
|
||||
val res = testServerConnection(newServer.value, chatModel)
|
||||
if (isActive) {
|
||||
newServer.value = res.first
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (testing.value) {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewServerLayout(
|
||||
server: MutableState<UserServer>,
|
||||
testing: Boolean,
|
||||
testServer: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.smp_servers_new_server))
|
||||
CustomServer(server, testing, testServer, onDelete = null)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
fun serverProtocolAndOperator(
|
||||
server: UserServer,
|
||||
userServers: List<UserOperatorServers>
|
||||
): Pair<ServerProtocol, ServerOperator?>? {
|
||||
val serverAddress = parseServerAddress(server.server)
|
||||
return if (serverAddress != null) {
|
||||
val serverProtocol = serverAddress.serverProtocol
|
||||
val hostnames = serverAddress.hostnames
|
||||
val matchingOperator = userServers.mapNotNull { it.operator }.firstOrNull { op ->
|
||||
op.serverDomains.any { domain ->
|
||||
hostnames.any { hostname ->
|
||||
hostname.endsWith(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
Pair(serverProtocol, matchingOperator)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun addServer(
|
||||
scope: CoroutineScope,
|
||||
server: UserServer,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val result = serverProtocolAndOperator(server, userServers.value)
|
||||
if (result != null) {
|
||||
val (serverProtocol, matchingOperator) = result
|
||||
val operatorIndex = userServers.value.indexOfFirst { it.operator?.operatorId == matchingOperator?.operatorId }
|
||||
if (operatorIndex != -1) {
|
||||
// Create a mutable copy of the userServers list
|
||||
val updatedUserServers = userServers.value.toMutableList()
|
||||
val operatorServers = updatedUserServers[operatorIndex]
|
||||
// Create a mutable copy of the smpServers or xftpServers and add the server
|
||||
when (serverProtocol) {
|
||||
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)
|
||||
updatedUserServers[operatorIndex] = operatorServers.copy(xftpServers = updatedXFTPServers)
|
||||
}
|
||||
}
|
||||
|
||||
userServers.value = updatedUserServers
|
||||
close()
|
||||
matchingOperator?.let { op ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.operator_server_alert_title),
|
||||
text = String.format(generalGetString(MR.strings.server_added_to_operator__name), op.tradeName)
|
||||
)
|
||||
}
|
||||
} else { // Shouldn't happen
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error_adding_server))
|
||||
}
|
||||
} else {
|
||||
close()
|
||||
if (server.server.trim().isNotEmpty()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_invalid_address),
|
||||
text = generalGetString(MR.strings.smp_servers_check_address)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+701
@@ -0,0 +1,701 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.getUsageConditions
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun ModalData.OperatorView(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
val testing = remember { mutableStateOf(false) }
|
||||
val operator = remember { userServers.value[operatorIndex].operator_ }
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
|
||||
LaunchedEffect(userServers) {
|
||||
snapshotFlow { userServers.value }
|
||||
.collect { updatedServers ->
|
||||
validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors)
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
ColumnWithScrollBar(Modifier.alpha(if (testing.value) 0.6f else 1f)) {
|
||||
AppBarTitle(String.format(stringResource(MR.strings.operator_servers_title), operator.tradeName))
|
||||
OperatorViewLayout(
|
||||
currUserServers,
|
||||
userServers,
|
||||
serverErrors,
|
||||
operatorIndex,
|
||||
navigateToProtocolView = { serverIndex, server, protocol ->
|
||||
navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol)
|
||||
},
|
||||
currentUser,
|
||||
rhId,
|
||||
testing
|
||||
)
|
||||
}
|
||||
|
||||
if (testing.value) {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToProtocolView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
serverIndex: Int,
|
||||
server: UserServer,
|
||||
protocol: ServerProtocol
|
||||
) {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ProtocolServerView(
|
||||
m = chatModel,
|
||||
server = server,
|
||||
serverProtocol = protocol,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
onDelete = {
|
||||
if (protocol == ServerProtocol.SMP) {
|
||||
deleteSMPServer(userServers, operatorIndex, serverIndex)
|
||||
} else {
|
||||
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
|
||||
)
|
||||
}
|
||||
},
|
||||
close = close,
|
||||
rhId = rhId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OperatorViewLayout(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit,
|
||||
currentUser: User?,
|
||||
rhId: Long?,
|
||||
testing: MutableState<Boolean>
|
||||
) {
|
||||
val operator by remember { derivedStateOf { userServers.value[operatorIndex].operator_ } }
|
||||
val scope = rememberCoroutineScope()
|
||||
val duplicateHosts = findDuplicateHosts(serverErrors.value)
|
||||
|
||||
Column {
|
||||
SectionView(generalGetString(MR.strings.operator).uppercase()) {
|
||||
SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(operator) } }) {
|
||||
Image(painterResource(operator.largeLogo), null, Modifier.height(48.dp))
|
||||
}
|
||||
UseOperatorToggle(
|
||||
scope = scope,
|
||||
currUserServers = currUserServers,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
operatorIndex = operatorIndex,
|
||||
rhId = rhId
|
||||
)
|
||||
}
|
||||
val serversErr = globalServersError(serverErrors.value)
|
||||
if (serversErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(serversErr)
|
||||
}
|
||||
} else {
|
||||
val footerText = when (val c = operator.conditionsAcceptance) {
|
||||
is ConditionsAcceptance.Accepted -> if (c.acceptedAt != null) {
|
||||
String.format(generalGetString(MR.strings.operator_conditions_accepted_on), localDate(c.acceptedAt))
|
||||
} else null
|
||||
is ConditionsAcceptance.Required -> if (operator.enabled && c.deadline != null) {
|
||||
String.format(generalGetString(MR.strings.operator_conditions_will_be_accepted_on), localDate(c.deadline))
|
||||
} else null
|
||||
}
|
||||
if (footerText != null) {
|
||||
SectionTextFooter(footerText)
|
||||
}
|
||||
}
|
||||
|
||||
if (operator.enabled) {
|
||||
if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.operator_use_for_messages).uppercase()) {
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(
|
||||
stringResource(MR.strings.operator_use_for_messages_receiving),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
DefaultSwitch(
|
||||
checked = userServers.value[operatorIndex].operator_.smpRoles.storage,
|
||||
onCheckedChange = { enabled ->
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorIndex] = this[operatorIndex].copy(
|
||||
operator = this[operatorIndex].operator?.copy(
|
||||
smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(
|
||||
stringResource(MR.strings.operator_use_for_messages_private_routing),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
DefaultSwitch(
|
||||
checked = userServers.value[operatorIndex].operator_.smpRoles.proxy,
|
||||
onCheckedChange = { enabled ->
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorIndex] = this[operatorIndex].copy(
|
||||
operator = this[operatorIndex].operator?.copy(
|
||||
smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
val smpErr = globalSMPServersError(serverErrors.value)
|
||||
if (smpErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(smpErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preset servers can't be deleted
|
||||
if (userServers.value[operatorIndex].smpServers.any { it.preset }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.message_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].smpServers.forEachIndexed { i, server ->
|
||||
if (!server.preset) return@forEachIndexed
|
||||
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) {
|
||||
ProtocolServerViewLink(
|
||||
srv = server,
|
||||
serverProtocol = ServerProtocol.SMP,
|
||||
duplicateHosts = duplicateHosts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val smpErr = globalSMPServersError(serverErrors.value)
|
||||
if (smpErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(smpErr)
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.smp_servers_per_user) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (userServers.value[operatorIndex].smpServers.any { !it.preset && !it.deleted }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.operator_added_message_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].smpServers.forEachIndexed { i, server ->
|
||||
if (server.deleted || server.preset) return@forEachIndexed
|
||||
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) {
|
||||
ProtocolServerViewLink(
|
||||
srv = server,
|
||||
serverProtocol = ServerProtocol.SMP,
|
||||
duplicateHosts = duplicateHosts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.operator_use_for_files).uppercase()) {
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(
|
||||
stringResource(MR.strings.operator_use_for_sending),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
DefaultSwitch(
|
||||
checked = userServers.value[operatorIndex].operator_.xftpRoles.storage,
|
||||
onCheckedChange = { enabled ->
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorIndex] = this[operatorIndex].copy(
|
||||
operator = this[operatorIndex].operator?.copy(
|
||||
xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
val xftpErr = globalXFTPServersError(serverErrors.value)
|
||||
if (xftpErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(xftpErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preset servers can't be deleted
|
||||
if (userServers.value[operatorIndex].xftpServers.any { it.preset }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server ->
|
||||
if (!server.preset) return@forEachIndexed
|
||||
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) {
|
||||
ProtocolServerViewLink(
|
||||
srv = server,
|
||||
serverProtocol = ServerProtocol.XFTP,
|
||||
duplicateHosts = duplicateHosts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val xftpErr = globalXFTPServersError(serverErrors.value)
|
||||
if (xftpErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(xftpErr)
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.xftp_servers_per_user) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (userServers.value[operatorIndex].xftpServers.any { !it.preset && !it.deleted}) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.operator_added_xftp_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server ->
|
||||
if (server.deleted || server.preset) return@forEachIndexed
|
||||
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) {
|
||||
ProtocolServerViewLink(
|
||||
srv = server,
|
||||
serverProtocol = ServerProtocol.XFTP,
|
||||
duplicateHosts = duplicateHosts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
SectionView {
|
||||
TestServersButton(
|
||||
testing = testing,
|
||||
smpServers = userServers.value[operatorIndex].smpServers,
|
||||
xftpServers = userServers.value[operatorIndex].xftpServers,
|
||||
) { p, l ->
|
||||
when (p) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OperatorInfoView(serverOperator: ServerOperator) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.operator_info_title))
|
||||
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Image(painterResource(serverOperator.largeLogo), null, Modifier.height(48.dp))
|
||||
if (serverOperator.legalName != null) {
|
||||
Text(serverOperator.legalName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
serverOperator.info.description.forEach { d ->
|
||||
Text(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(generalGetString(MR.strings.operator_website).uppercase()) {
|
||||
SectionItemView {
|
||||
val website = serverOperator.info.website
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseOperatorToggle(
|
||||
scope: CoroutineScope,
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(
|
||||
stringResource(MR.strings.operator_use_operator_toggle_description),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
DefaultSwitch(
|
||||
checked = userServers.value[operatorIndex].operator?.enabled ?: false,
|
||||
onCheckedChange = { enabled ->
|
||||
val operator = userServers.value[operatorIndex].operator
|
||||
if (enabled) {
|
||||
when (val conditionsAcceptance = operator?.conditionsAcceptance) {
|
||||
is ConditionsAcceptance.Accepted -> {
|
||||
changeOperatorEnabled(userServers, operatorIndex, true)
|
||||
}
|
||||
|
||||
is ConditionsAcceptance.Required -> {
|
||||
if (conditionsAcceptance.deadline == null) {
|
||||
ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close ->
|
||||
SingleOperatorUsageConditionsView(
|
||||
currUserServers = currUserServers,
|
||||
userServers = userServers,
|
||||
serverErrors = serverErrors,
|
||||
operatorIndex = operatorIndex,
|
||||
rhId = rhId,
|
||||
close = close
|
||||
)
|
||||
}
|
||||
} else {
|
||||
changeOperatorEnabled(userServers, operatorIndex, true)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
} else {
|
||||
changeOperatorEnabled(userServers, operatorIndex, false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SingleOperatorUsageConditionsView(
|
||||
currUserServers: MutableState<List<UserOperatorServers>>,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } }
|
||||
val operator = remember { userServers.value[operatorIndex].operator_ }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
suspend fun acceptForOperators(rhId: Long?, operatorIds: List<Long>, operatorIndexToEnable: Int, close: () -> Unit) {
|
||||
try {
|
||||
val conditionsId = chatModel.conditions.value.currentConditions.conditionsId
|
||||
val r = chatController.acceptConditions(rhId, conditionsId, operatorIds) ?: return
|
||||
|
||||
chatModel.conditions.value = r
|
||||
updateOperatorsConditionsAcceptance(currUserServers, r.serverOperators)
|
||||
updateOperatorsConditionsAcceptance(userServers, r.serverOperators)
|
||||
changeOperatorEnabled(userServers, operatorIndex, true)
|
||||
close()
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, ex.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AcceptConditionsButton(close: () -> Unit) {
|
||||
// Opened operator or Other enabled operators with conditions not accepted
|
||||
val operatorIds = chatModel.conditions.value.serverOperators
|
||||
.filter { it.operatorId == operator.id || (it.enabled && !it.conditionsAcceptance.conditionsAccepted) }
|
||||
.map { it.operatorId }
|
||||
|
||||
Column(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
OnboardingActionButton(
|
||||
labelId = MR.strings.accept_conditions,
|
||||
onboarding = null,
|
||||
enabled = operatorIds.isNotEmpty(),
|
||||
onclick = {
|
||||
scope.launch {
|
||||
acceptForOperators(rhId, operatorIds, operatorIndex, close)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UsageConditionsDestinationView(close: () -> Unit) {
|
||||
ColumnWithScrollBar(modifier = Modifier.fillMaxSize()) {
|
||||
AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false)
|
||||
Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) {
|
||||
ConditionsTextView(rhId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UsageConditionsNavLinkButton() {
|
||||
Text(
|
||||
stringResource(MR.strings.view_conditions),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).clickable {
|
||||
ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close ->
|
||||
UsageConditionsDestinationView(close)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) {
|
||||
AppBarTitle(String.format(stringResource(MR.strings.use_servers_of_operator_x), operator.tradeName), enableAlphaChanges = false, withPadding = false)
|
||||
if (operator.conditionsAcceptance is ConditionsAcceptance.Accepted) {
|
||||
// In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted
|
||||
Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) {
|
||||
ConditionsTextView(rhId)
|
||||
}
|
||||
} else if (operatorsWithConditionsAccepted.isNotEmpty()) {
|
||||
ReadableText(
|
||||
MR.strings.operator_conditions_accepted_for_some,
|
||||
args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ }
|
||||
)
|
||||
ReadableText(
|
||||
MR.strings.operator_same_conditions_will_be_applied,
|
||||
args = operator.legalName_
|
||||
)
|
||||
ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex)
|
||||
|
||||
UsageConditionsNavLinkButton()
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
AcceptConditionsButton(close)
|
||||
} else {
|
||||
ReadableText(
|
||||
MR.strings.operator_in_order_to_use_accept_conditions,
|
||||
args = operator.legalName_
|
||||
)
|
||||
ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex)
|
||||
Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) {
|
||||
ConditionsTextView(rhId)
|
||||
}
|
||||
AcceptConditionsButton(close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConditionsTextView(
|
||||
rhId: Long?
|
||||
) {
|
||||
val conditionsData = remember { mutableStateOf<Triple<UsageConditionsDetail, String?, UsageConditionsDetail?>?>(null) }
|
||||
val failedToLoad = remember { mutableStateOf(false) }
|
||||
val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
val conditions = getUsageConditions(rh = rhId)
|
||||
|
||||
if (conditions != null) {
|
||||
conditionsData.value = conditions
|
||||
} else {
|
||||
failedToLoad.value = true
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
failedToLoad.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
val conditions = conditionsData.value
|
||||
|
||||
if (conditions != null) {
|
||||
val (usageConditions, conditionsText, _) = conditions
|
||||
|
||||
if (conditionsText != null) {
|
||||
val scrollState = rememberScrollState()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.border(border = BorderStroke(1.dp, CurrentColors.value.colors.secondary.copy(alpha = 0.6f)), shape = RoundedCornerShape(12.dp))
|
||||
.verticalScroll(scrollState)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = conditionsText.trimIndent(),
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md"
|
||||
ConditionsLinkView(conditionsLink)
|
||||
}
|
||||
} else if (failedToLoad.value) {
|
||||
ConditionsLinkView(defaultConditionsLink)
|
||||
} else {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConditionsLinkView(conditionsLink: String) {
|
||||
SectionItemView {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Text(stringResource(MR.strings.operator_conditions_failed_to_load), color = MaterialTheme.colors.onBackground)
|
||||
Text(conditionsLink, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(conditionsLink) })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConditionsAppliedToOtherOperatorsText(userServers: List<UserOperatorServers>, operatorIndex: Int) {
|
||||
val otherOperatorsToApply = remember {
|
||||
derivedStateOf {
|
||||
chatModel.conditions.value.serverOperators.filter {
|
||||
it.enabled &&
|
||||
!it.conditionsAcceptance.conditionsAccepted &&
|
||||
it.operatorId != userServers[operatorIndex].operator_.operatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (otherOperatorsToApply.value.isNotEmpty()) {
|
||||
ReadableText(MR.strings.operator_conditions_will_be_applied)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConditionsLinkButton() {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
Column {
|
||||
DefaultDropdownMenu(showMenu, offset = if (oneHandUI.value) DpOffset(0.dp, -AppBarHeight * fontSizeSqrtMultiplier * 3) else DpOffset.Zero) {
|
||||
val commit = chatModel.conditions.value.currentConditions.conditionsCommit
|
||||
ItemAction(stringResource(MR.strings.operator_open_conditions), painterResource(MR.images.ic_draft), onClick = {
|
||||
val mdUrl = "https://github.com/simplex-chat/simplex-chat/blob/$commit/PRIVACY.md"
|
||||
uriHandler.openUriCatching(mdUrl)
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(MR.strings.operator_open_changes), painterResource(MR.images.ic_more_horiz), onClick = {
|
||||
val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit"
|
||||
uriHandler.openUriCatching(commitUrl)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
IconButton({ showMenu.value = true }) {
|
||||
Icon(painterResource(MR.images.ic_outbound), null, tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeOperatorEnabled(userServers: MutableState<List<UserOperatorServers>>, operatorIndex: Int, enabled: Boolean) {
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorIndex] = this[operatorIndex].copy(
|
||||
operator = this[operatorIndex].operator?.copy(enabled = enabled)
|
||||
)
|
||||
}
|
||||
}
|
||||
+103
-66
@@ -1,16 +1,14 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -26,62 +24,103 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCode
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Composable
|
||||
fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
|
||||
var testing by remember { mutableStateOf(false) }
|
||||
ProtocolServerLayout(
|
||||
testing,
|
||||
server,
|
||||
serverProtocol,
|
||||
testServer = {
|
||||
testing = true
|
||||
withLongRunningApi {
|
||||
val res = testServerConnection(server, m)
|
||||
if (isActive) {
|
||||
onUpdate(res.first)
|
||||
testing = false
|
||||
fun ProtocolServerView(
|
||||
m: ChatModel,
|
||||
server: UserServer,
|
||||
serverProtocol: ServerProtocol,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
onDelete: () -> Unit,
|
||||
onUpdate: (UserServer) -> Unit,
|
||||
close: () -> Unit,
|
||||
rhId: Long?
|
||||
) {
|
||||
val testing = remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val draftServer = remember { mutableStateOf(server) }
|
||||
|
||||
ModalView(
|
||||
close = {
|
||||
scope.launch {
|
||||
val draftResult = serverProtocolAndOperator(draftServer.value, userServers.value)
|
||||
val savedResult = serverProtocolAndOperator(server, userServers.value)
|
||||
|
||||
if (draftResult != null && savedResult != null) {
|
||||
val (serverToEditProtocol, serverToEditOperator) = draftResult
|
||||
val (svProtocol, serverOperator) = savedResult
|
||||
|
||||
if (serverToEditProtocol != svProtocol) {
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.error_updating_server_title),
|
||||
text = generalGetString(MR.strings.error_server_protocol_changed)
|
||||
)
|
||||
} else if (serverToEditOperator != serverOperator) {
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.error_updating_server_title),
|
||||
text = generalGetString(MR.strings.error_server_operator_changed)
|
||||
)
|
||||
} else {
|
||||
onUpdate(draftServer.value)
|
||||
close()
|
||||
}
|
||||
} else {
|
||||
close()
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_invalid_address),
|
||||
text = generalGetString(MR.strings.smp_servers_check_address)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onUpdate,
|
||||
onDelete
|
||||
)
|
||||
if (testing) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.5.dp
|
||||
}
|
||||
) {
|
||||
Box {
|
||||
ProtocolServerLayout(
|
||||
draftServer,
|
||||
serverProtocol,
|
||||
testing.value,
|
||||
testServer = {
|
||||
testing.value = true
|
||||
withLongRunningApi {
|
||||
val res = testServerConnection(draftServer.value, m)
|
||||
if (isActive) {
|
||||
draftServer.value = res.first
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onDelete
|
||||
)
|
||||
|
||||
if (testing.value) {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProtocolServerLayout(
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
server: MutableState<UserServer>,
|
||||
serverProtocol: ServerProtocol,
|
||||
testing: Boolean,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(if (server.preset) MR.strings.smp_servers_preset_server else MR.strings.smp_servers_your_server))
|
||||
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.XFTP) MR.strings.xftp_server else MR.strings.smp_server))
|
||||
|
||||
if (server.preset) {
|
||||
PresetServer(testing, server, testServer, onUpdate, onDelete)
|
||||
if (server.value.preset) {
|
||||
PresetServer(server, testing, testServer)
|
||||
} else {
|
||||
CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete)
|
||||
CustomServer(server, testing, testServer, onDelete)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
@@ -89,16 +128,14 @@ private fun ProtocolServerLayout(
|
||||
|
||||
@Composable
|
||||
private fun PresetServer(
|
||||
server: MutableState<UserServer>,
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
testServer: () -> Unit
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.smp_servers_preset_address).uppercase()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
server.server,
|
||||
server.value.server,
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily.Monospace, fontSize = 16.sp,
|
||||
@@ -108,23 +145,21 @@ private fun PresetServer(
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
UseServerSection(true, testing, server, testServer, onUpdate, onDelete)
|
||||
UseServerSection(server, true, testing, testServer)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomServer(
|
||||
fun CustomServer(
|
||||
server: MutableState<UserServer>,
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
serverProtocol: ServerProtocol,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onDelete: (() -> Unit)?,
|
||||
) {
|
||||
val serverAddress = remember { mutableStateOf(server.server) }
|
||||
val serverAddress = remember { mutableStateOf(server.value.server) }
|
||||
val valid = remember {
|
||||
derivedStateOf {
|
||||
with(parseServerAddress(serverAddress.value)) {
|
||||
this?.valid == true && this.serverProtocol == serverProtocol
|
||||
this?.valid == true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,13 +177,14 @@ private fun CustomServer(
|
||||
snapshotFlow { serverAddress.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
testedPreviously[server.server] = server.tested
|
||||
onUpdate(server.copy(server = it, tested = testedPreviously[serverAddress.value]))
|
||||
testedPreviously[server.value.server] = server.value.tested
|
||||
server.value = server.value.copy(server = it, tested = testedPreviously[serverAddress.value])
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete)
|
||||
|
||||
UseServerSection(server, valid.value, testing, testServer, onDelete)
|
||||
|
||||
if (valid.value) {
|
||||
SectionDividerSpaced()
|
||||
@@ -160,43 +196,44 @@ private fun CustomServer(
|
||||
|
||||
@Composable
|
||||
private fun UseServerSection(
|
||||
server: MutableState<UserServer>,
|
||||
valid: Boolean,
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onDelete: (() -> Unit)? = null,
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) {
|
||||
SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) {
|
||||
Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
ShowTestStatus(server)
|
||||
ShowTestStatus(server.value)
|
||||
}
|
||||
|
||||
val enabled = rememberUpdatedState(server.enabled)
|
||||
val enabled = rememberUpdatedState(server.value.enabled)
|
||||
PreferenceToggle(
|
||||
stringResource(MR.strings.smp_servers_use_server_for_new_conn),
|
||||
disabled = server.tested != true && !server.preset,
|
||||
disabled = testing,
|
||||
checked = enabled.value
|
||||
) {
|
||||
onUpdate(server.copy(enabled = it))
|
||||
server.value = server.value.copy(enabled = it)
|
||||
}
|
||||
|
||||
SectionItemView(onDelete, disabled = testing) {
|
||||
Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error)
|
||||
|
||||
if (onDelete != null) {
|
||||
SectionItemView(onDelete, disabled = testing) {
|
||||
Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
|
||||
fun ShowTestStatus(server: UserServer, modifier: Modifier = Modifier) =
|
||||
when (server.tested) {
|
||||
true -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = SimplexGreen)
|
||||
false -> Icon(painterResource(MR.images.ic_close), null, modifier, tint = MaterialTheme.colors.error)
|
||||
else -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = Color.Transparent)
|
||||
}
|
||||
|
||||
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> =
|
||||
suspend fun testServerConnection(server: UserServer, m: ChatModel): Pair<UserServer, ProtocolTestFailure?> =
|
||||
try {
|
||||
val r = m.controller.testProtoServer(server.remoteHostId, server.server)
|
||||
server.copy(tested = r == null) to r
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ModalData.YourServersView(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
rhId: Long?
|
||||
) {
|
||||
val testing = remember { mutableStateOf(false) }
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(userServers) {
|
||||
snapshotFlow { userServers.value }
|
||||
.collect { updatedServers ->
|
||||
validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors)
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_servers))
|
||||
YourServersViewLayout(
|
||||
scope,
|
||||
userServers,
|
||||
serverErrors,
|
||||
operatorIndex,
|
||||
navigateToProtocolView = { serverIndex, server, protocol ->
|
||||
navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol)
|
||||
},
|
||||
currentUser,
|
||||
rhId,
|
||||
testing
|
||||
)
|
||||
}
|
||||
|
||||
if (testing.value) {
|
||||
DefaultProgressView(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun YourServersViewLayout(
|
||||
scope: CoroutineScope,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
operatorIndex: Int,
|
||||
navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit,
|
||||
currentUser: User?,
|
||||
rhId: Long?,
|
||||
testing: MutableState<Boolean>
|
||||
) {
|
||||
val duplicateHosts = findDuplicateHosts(serverErrors.value)
|
||||
|
||||
Column {
|
||||
if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) {
|
||||
SectionView(generalGetString(MR.strings.message_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].smpServers.forEachIndexed { i, server ->
|
||||
if (server.deleted) return@forEachIndexed
|
||||
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) {
|
||||
ProtocolServerViewLink(
|
||||
srv = server,
|
||||
serverProtocol = ServerProtocol.SMP,
|
||||
duplicateHosts = duplicateHosts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val smpErr = globalSMPServersError(serverErrors.value)
|
||||
if (smpErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(smpErr)
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.smp_servers_per_user) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) {
|
||||
userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server ->
|
||||
if (server.deleted) return@forEachIndexed
|
||||
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) {
|
||||
ProtocolServerViewLink(
|
||||
srv = server,
|
||||
serverProtocol = ServerProtocol.XFTP,
|
||||
duplicateHosts = duplicateHosts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val xftpErr = globalXFTPServersError(serverErrors.value)
|
||||
if (xftpErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(xftpErr)
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.xftp_servers_per_user) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
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),
|
||||
stringResource(MR.strings.smp_servers_add),
|
||||
click = { showAddServerDialog(scope, userServers, serverErrors, rhId) },
|
||||
disabled = testing.value,
|
||||
textColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
iconColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
val serversErr = globalServersError(serverErrors.value)
|
||||
if (serversErr != null) {
|
||||
SectionCustomFooter {
|
||||
ServersErrorFooter(serversErr)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
TestServersButton(
|
||||
testing = testing,
|
||||
smpServers = userServers.value[operatorIndex].smpServers,
|
||||
xftpServers = userServers.value[operatorIndex].xftpServers,
|
||||
) { p, l ->
|
||||
when (p) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HowToButton()
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestServersButton(
|
||||
smpServers: List<UserServer>,
|
||||
xftpServers: 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 }
|
||||
|
||||
SectionItemView(
|
||||
{
|
||||
scope.launch {
|
||||
testServers(testing, smpServers, xftpServers, chatModel, onUpdate)
|
||||
}
|
||||
},
|
||||
disabled = disabled.value
|
||||
) {
|
||||
Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!disabled.value) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAddServerDialog(
|
||||
scope: CoroutineScope,
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
serverErrors: MutableState<List<UserServersError>>,
|
||||
rhId: Long?
|
||||
) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.smp_servers_add),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
NewServerView(userServers, serverErrors, rhId, close)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ScanProtocolServer(rhId) { server ->
|
||||
addServer(
|
||||
scope,
|
||||
server,
|
||||
userServers,
|
||||
serverErrors,
|
||||
rhId,
|
||||
close = close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProtocolServerViewLink(serverProtocol: ServerProtocol, srv: UserServer, duplicateHosts: Set<String>) {
|
||||
val address = parseServerAddress(srv.server)
|
||||
when {
|
||||
address == null || !address.valid || address.serverProtocol != serverProtocol || address.hostnames.any { it in duplicateHosts } -> InvalidServer()
|
||||
!srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
|
||||
else -> ShowTestStatus(srv)
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
val text = address?.hostnames?.firstOrNull() ?: srv.server
|
||||
if (srv.enabled) {
|
||||
Text(text, color = MaterialTheme.colors.onBackground, maxLines = 1)
|
||||
} else {
|
||||
Text(text, maxLines = 1, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowToButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_open_in_new),
|
||||
stringResource(MR.strings.how_to_use_your_servers),
|
||||
{ uriHandler.openUriCatching("https://simplex.chat/docs/server.html") },
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InvalidServer() {
|
||||
Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error)
|
||||
}
|
||||
|
||||
private suspend fun testServers(
|
||||
testing: MutableState<Boolean>,
|
||||
smpServers: List<UserServer>,
|
||||
xftpServers: List<UserServer>,
|
||||
m: ChatModel,
|
||||
onUpdate: (ServerProtocol, List<UserServer>) -> Unit
|
||||
) {
|
||||
val smpResetStatus = resetTestStatus(smpServers)
|
||||
onUpdate(ServerProtocol.SMP, smpResetStatus)
|
||||
val xftpResetStatus = resetTestStatus(xftpServers)
|
||||
onUpdate(ServerProtocol.XFTP, xftpResetStatus)
|
||||
testing.value = true
|
||||
val smpFailures = runServersTest(smpResetStatus, m) { onUpdate(ServerProtocol.SMP, it) }
|
||||
val xftpFailures = runServersTest(xftpResetStatus, m) { onUpdate(ServerProtocol.XFTP, it) }
|
||||
testing.value = false
|
||||
val fs = smpFailures + xftpFailures
|
||||
if (fs.isNotEmpty()) {
|
||||
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_test_failed),
|
||||
text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetTestStatus(servers: List<UserServer>): List<UserServer> {
|
||||
val copy = ArrayList(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
copy.removeAt(index)
|
||||
copy.add(index, server.copy(tested = null))
|
||||
}
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
private suspend fun runServersTest(servers: List<UserServer>, m: ChatModel, onUpdated: (List<UserServer>) -> Unit): Map<String, ProtocolTestFailure> {
|
||||
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
|
||||
val updatedServers = ArrayList<UserServer>(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
interruptIfCancelled()
|
||||
val (updatedServer, f) = testServerConnection(server, m)
|
||||
updatedServers.removeAt(index)
|
||||
updatedServers.add(index, updatedServer)
|
||||
// toList() is important. Otherwise, Compose will not redraw the screen after first update
|
||||
onUpdated(updatedServers.toList())
|
||||
if (f != null) {
|
||||
fs[serverHostname(updatedServer.server)] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
fun deleteXFTPServer(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
operatorServersIndex: Int,
|
||||
serverIndex: Int
|
||||
) {
|
||||
val serverIsSaved = userServers.value[operatorServersIndex].xftpServers[serverIndex].serverId != null
|
||||
|
||||
if (serverIsSaved) {
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorServersIndex] = this[operatorServersIndex].copy(
|
||||
xftpServers = this[operatorServersIndex].xftpServers.toMutableList().apply {
|
||||
this[serverIndex] = this[serverIndex].copy(deleted = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorServersIndex] = this[operatorServersIndex].copy(
|
||||
xftpServers = this[operatorServersIndex].xftpServers.toMutableList().apply {
|
||||
this.removeAt(serverIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSMPServer(
|
||||
userServers: MutableState<List<UserOperatorServers>>,
|
||||
operatorServersIndex: Int,
|
||||
serverIndex: Int
|
||||
) {
|
||||
val serverIsSaved = userServers.value[operatorServersIndex].smpServers[serverIndex].serverId != null
|
||||
|
||||
if (serverIsSaved) {
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorServersIndex] = this[operatorServersIndex].copy(
|
||||
smpServers = this[operatorServersIndex].smpServers.toMutableList().apply {
|
||||
this[serverIndex] = this[serverIndex].copy(deleted = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
userServers.value = userServers.value.toMutableList().apply {
|
||||
this[operatorServersIndex] = this[operatorServersIndex].copy(
|
||||
smpServers = this[operatorServersIndex].smpServers.toMutableList().apply {
|
||||
this.removeAt(serverIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-9
@@ -1,29 +1,25 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.model.ServerCfg
|
||||
import chat.simplex.common.model.UserServer
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit)
|
||||
expect fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit)
|
||||
|
||||
@Composable
|
||||
fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
|
||||
fun ScanProtocolServerLayout(rhId: Long?, onNext: (UserServer) -> Unit) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr))
|
||||
QRCodeScanner { text ->
|
||||
val res = parseServerAddress(text)
|
||||
if (res != null) {
|
||||
onNext(ServerCfg(remoteHostId = rhId, text, false, null, false))
|
||||
onNext(UserServer(remoteHostId = rhId, null, text, false, null, false, false))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_invalid_address),
|
||||
@@ -109,6 +109,16 @@
|
||||
<string name="failed_to_create_user_invalid_title">Invalid display name!</string>
|
||||
<string name="failed_to_create_user_invalid_desc">This display name is invalid. Please choose another name.</string>
|
||||
<string name="failed_to_active_user_title">Error switching profile!</string>
|
||||
<string name="failed_to_save_servers">Error saving servers</string>
|
||||
<string name="no_message_servers_configured">No message servers.</string>
|
||||
<string name="no_message_servers_configured_for_receiving">No servers to receive messages.</string>
|
||||
<string name="no_message_servers_configured_for_private_routing">No servers for private message routing.</string>
|
||||
<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="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>
|
||||
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Connection timeout</string>
|
||||
@@ -750,6 +760,7 @@
|
||||
<string name="smp_servers_test_some_failed">Some servers failed the test:</string>
|
||||
<string name="smp_servers_scan_qr">Scan server QR code</string>
|
||||
<string name="smp_servers_enter_manually">Enter server manually</string>
|
||||
<string name="smp_servers_new_server">New server</string>
|
||||
<string name="smp_servers_preset_server">Preset server</string>
|
||||
<string name="smp_servers_your_server">Your server</string>
|
||||
<string name="smp_servers_your_server_address">Your server address</string>
|
||||
@@ -1038,6 +1049,19 @@
|
||||
<string name="you_can_change_it_later">Random passphrase is stored in settings as plaintext.\nYou can change it later.</string>
|
||||
<string name="use_random_passphrase">Use random passphrase</string>
|
||||
|
||||
<!-- ChooseServerOperators.kt -->
|
||||
<string name="onboarding_choose_server_operators">Choose operators</string>
|
||||
<string name="onboarding_network_operators">Network operators</string>
|
||||
<string name="onboarding_network_operators_app_will_use_different_operators">When more than one network operator is enabled, the app will use the servers of different operators for each conversation.</string>
|
||||
<string name="onboarding_network_operators_app_will_use_for_routing">For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing.</string>
|
||||
<string name="onboarding_select_network_operators_to_use">Select network operators to use.</string>
|
||||
<string name="onboarding_network_operators_configure_via_settings">You can configure servers via settings.</string>
|
||||
<string name="onboarding_network_operators_conditions_will_be_accepted">Conditions will be accepted for enabled operators after 30 days.</string>
|
||||
<string name="onboarding_network_operators_conditions_you_can_configure">You can configure operators in Network & servers settings.</string>
|
||||
<string name="onboarding_network_operators_review_later">Review later</string>
|
||||
<string name="onboarding_network_operators_update">Update</string>
|
||||
<string name="onboarding_network_operators_continue">Continue</string>
|
||||
|
||||
<!-- Call -->
|
||||
<string name="incoming_video_call">Incoming video call</string>
|
||||
<string name="incoming_audio_call">Incoming audio call</string>
|
||||
@@ -1667,6 +1691,59 @@
|
||||
<string name="save_group_profile">Save group profile</string>
|
||||
<string name="error_saving_group_profile">Error saving group profile</string>
|
||||
|
||||
<!-- NetworkAndServers.kt -->
|
||||
<string name="network_preset_servers_title">Preset servers</string>
|
||||
<string name="operator_review_conditions">Review conditions</string>
|
||||
<string name="operator_conditions_accepted">Accepted conditions</string>
|
||||
<string name="operator_conditions_accepted_for_enabled_operators_on">Conditions will be automatically accepted for enabled operators on: %s.</string>
|
||||
<string name="your_servers">Your servers</string>
|
||||
<string name="operators_conditions_accepted_for"><![CDATA[Conditions are accepted for the operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operators_conditions_will_be_accepted_for"><![CDATA[Conditions will be accepted for the operator(s): <b>%s</b>.]]></string>
|
||||
|
||||
<!-- OperatorView.kt -->
|
||||
<string name="operator">Operator</string>
|
||||
<string name="operator_servers_title">%s servers</string>
|
||||
<string name="operator_info_title">Network operator</string>
|
||||
<string name="operator_website">Website</string>
|
||||
<string name="operator_conditions_accepted_on">Conditions accepted on: %s.</string>
|
||||
<string name="operator_conditions_will_be_accepted_on">Conditions will be accepted on: %s.</string>
|
||||
<string name="operator">Operator</string>
|
||||
<string name="operator_use_operator_toggle_description">Use servers</string>
|
||||
<string name="use_servers_of_operator_x">Use %s</string>
|
||||
<string name="operator_conditions_failed_to_load">Current conditions text couldn\'t be loaded, you can review conditions via this link:</string>
|
||||
<string name="operator_conditions_accepted_for_some"><![CDATA[Conditions are already accepted for following operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_be_applied"><![CDATA[Same conditions will apply to operator <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Same conditions will apply to operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_accepted_for_some"><![CDATA[Conditions are already accepted for following operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_will_be_applied"><![CDATA[These conditions will also apply for: <b>%s</b>.]]></string>
|
||||
<string name="operator_same_conditions_will_be_applied"><![CDATA[Same conditions will apply to operator: <b>%s</b>.]]></string>
|
||||
<string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Conditions will be accepted for operator(s): <b>%s</b>.]]></string>
|
||||
<string name="operators_conditions_will_also_apply"><![CDATA[These conditions will also apply for: <b>%s</b>.]]></string>
|
||||
<string name="view_conditions">View conditions</string>
|
||||
<string name="accept_conditions">Accept conditions</string>
|
||||
<string name="operator_conditions_of_use">Conditions of use</string>
|
||||
<string name="operator_in_order_to_use_accept_conditions"><![CDATA[To use the servers of <b>%s</b>, accept conditions of use.]]></string>
|
||||
<string name="operator_use_for_messages">Use for messages</string>
|
||||
<string name="operator_use_for_messages_receiving">To receive</string>
|
||||
<string name="operator_use_for_messages_private_routing">For private routing</string>
|
||||
<string name="operator_added_message_servers">Added message servers</string>
|
||||
<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="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>
|
||||
|
||||
<!-- ProtocolServerView.kt -->
|
||||
<string name="error_updating_server_title">Error updating server</string>
|
||||
<string name="error_server_protocol_changed">Server protocol changed.</string>
|
||||
<string name="error_server_operator_changed">Server operator changed.</string>
|
||||
|
||||
<!-- NewServerView.kt -->
|
||||
<string name="operator_server_alert_title">Operator server</string>
|
||||
<string name="server_added_to_operator__name">Server added to operator %s.</string>
|
||||
<string name="error_adding_server">Error adding server</string>
|
||||
|
||||
<!-- AdvancedNetworkSettings.kt -->
|
||||
<string name="network_option_tcp_connection">TCP connection</string>
|
||||
<string name="network_options_reset_to_defaults">Reset to defaults</string>
|
||||
@@ -2059,6 +2136,13 @@
|
||||
<string name="v6_1_message_dates_descr">Better message dates.</string>
|
||||
<string name="v6_1_forward_many_messages_descr">Forward up to 20 messages at once.</string>
|
||||
<string name="v6_1_delete_many_messages_descr">Delete or moderate up to 200 messages.</string>
|
||||
<string name="v6_2_network_decentralization">Network decentralization</string>
|
||||
<string name="v6_2_network_decentralization_descr">The second preset operator in the app!</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux">Enable flux</string>
|
||||
<string name="v6_2_network_decentralization_enable_flux_reason">for better metadata privacy</string>
|
||||
<string name="v6_2_improved_chat_navigation">Improved chat navigation</string>
|
||||
<string name="v6_2_improved_chat_navigation_descr">- Open chat on the first unread message.\n- Jump to quoted messages.</string>
|
||||
<string name="view_updated_conditions">View updated conditions</string>
|
||||
|
||||
<!-- CustomTimePicker -->
|
||||
<string name="custom_time_unit_seconds">seconds</string>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m346-306 235.5-235v125.5H639V-639H415.5v57.5H540L306-347l40 41ZM480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 724 B |
-9
@@ -1,9 +0,0 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.common.model.ServerCfg
|
||||
|
||||
@Composable
|
||||
actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) {
|
||||
ScanProtocolServerLayout(rhId, onNext)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.common.model.UserServer
|
||||
|
||||
@Composable
|
||||
actual fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) {
|
||||
ScanProtocolServerLayout(rhId, onNext)
|
||||
}
|
||||
Reference in New Issue
Block a user