mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -424,6 +424,7 @@ fun PreviewIncomingCallLockScreenAlert() {
|
||||
) {
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
callUUID = "",
|
||||
remoteHostId = null,
|
||||
user = User.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
|
||||
+2
@@ -770,6 +770,7 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Video,
|
||||
peerMedia = CallMediaType.Video,
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
||||
@@ -799,6 +800,7 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Audio,
|
||||
peerMedia = CallMediaType.Audio,
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ actual fun authenticate(
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean,
|
||||
usingLAMode: LAMode,
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
val activity = mainActivity.get() ?: return completed(LAResult.Error(""))
|
||||
@@ -27,7 +28,7 @@ actual fun authenticate(
|
||||
else -> completed(LAResult.Unavailable())
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
|
||||
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -14,13 +14,11 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -33,7 +31,6 @@ import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.helpers.APPLICATION_ID
|
||||
import chat.simplex.common.helpers.saveAppLocale
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -82,7 +79,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
|
||||
val context = LocalContext.current
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// SectionItemWithValue(
|
||||
@@ -123,7 +120,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
LazyRow {
|
||||
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
|
||||
val item = AppIcon.values()[index]
|
||||
|
||||
@@ -74,6 +74,7 @@ fun MainScreen() {
|
||||
LaunchedEffect(showAdvertiseLAAlert) {
|
||||
if (
|
||||
!chatModel.controller.appPrefs.laNoticeShown.get()
|
||||
&& !appPrefs.performLA.get()
|
||||
&& showAdvertiseLAAlert
|
||||
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
|
||||
&& chatModel.chats.size > 3
|
||||
@@ -211,10 +212,8 @@ fun MainScreen() {
|
||||
} else {
|
||||
ActiveCallView()
|
||||
}
|
||||
} else {
|
||||
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
|
||||
ModalManager.fullscreen.showPasscodeInView()
|
||||
}
|
||||
ModalManager.fullscreen.showOneTimePasscodeInView()
|
||||
AlertManager.privacySensitive.showInView()
|
||||
if (onboarding == OnboardingStage.OnboardingComplete) {
|
||||
LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.localauth.SetAppPasscodeView
|
||||
@@ -31,7 +32,7 @@ object AppLock {
|
||||
|
||||
fun showLANotice(laNoticeShown: SharedPreference<Boolean>) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (!laNoticeShown.get()) {
|
||||
if (!laNoticeShown.get() && !appPrefs.performLA.get()) {
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.la_notice_title_simplex_lock),
|
||||
@@ -57,6 +58,8 @@ object AppLock {
|
||||
|
||||
private fun showChooseLAMode() {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (appPrefs.performLA.get()) return
|
||||
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.la_lock_mode),
|
||||
text = null,
|
||||
@@ -80,21 +83,23 @@ object AppLock {
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_enable_simplex_lock),
|
||||
generalGetString(MR.strings.auth_confirm_credential),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = true
|
||||
m.showAuthScreen.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
m.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// appPrefs.performLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
m.showAdvertiseLAUnavailableAlert.value = true
|
||||
}
|
||||
@@ -104,19 +109,22 @@ object AppLock {
|
||||
}
|
||||
|
||||
private fun setPasscode() {
|
||||
if (appPrefs.performLA.get()) return
|
||||
|
||||
val appPrefs = ChatController.appPrefs
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
ChatModel.performLA.value = true
|
||||
ChatModel.showAuthScreen.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
appPrefs.laMode.set(LAMode.PASSCODE)
|
||||
laTurnedOnAlert()
|
||||
},
|
||||
cancel = {
|
||||
ChatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
ChatModel.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// appPrefs.performLA.set(false)
|
||||
laPasscodeNotSetAlert()
|
||||
},
|
||||
close = close
|
||||
@@ -147,6 +155,7 @@ object AppLock {
|
||||
else
|
||||
generalGetString(MR.strings.auth_unlock),
|
||||
selfDestruct = true,
|
||||
oneTime = false,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success ->
|
||||
@@ -160,7 +169,7 @@ object AppLock {
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
userAuthorized.value = true
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
m.controller.appPrefs.performLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
@@ -192,22 +201,23 @@ object AppLock {
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
else
|
||||
"",
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = true
|
||||
m.showAuthScreen.value = true
|
||||
prefPerformLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
@@ -227,12 +237,13 @@ object AppLock {
|
||||
generalGetString(MR.strings.auth_confirm_credential)
|
||||
else
|
||||
generalGetString(MR.strings.auth_disable_simplex_lock),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
val selfDestructPref = m.controller.appPrefs.selfDestruct
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
DatabaseUtils.ksAppPassword.remove()
|
||||
selfDestructPref.set(false)
|
||||
@@ -240,12 +251,12 @@ object AppLock {
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = true
|
||||
m.showAuthScreen.value = true
|
||||
prefPerformLA.set(true)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
m.showAuthScreen.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
|
||||
+10
-3
@@ -20,7 +20,6 @@ import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.internal.ChannelFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.*
|
||||
@@ -98,7 +97,7 @@ object ChatModel {
|
||||
}
|
||||
)
|
||||
}
|
||||
val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
|
||||
val showAuthScreen by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
|
||||
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
|
||||
val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) }
|
||||
|
||||
@@ -784,7 +783,8 @@ object ChatModel {
|
||||
data class ShowingInvitation(
|
||||
val connId: String,
|
||||
val connReq: String,
|
||||
val connChatUsed: Boolean
|
||||
val connChatUsed: Boolean,
|
||||
val conn: PendingContactConnection
|
||||
)
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@@ -985,6 +985,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val fullName get() = contact.fullName
|
||||
override val image get() = contact.image
|
||||
override val localAlias: String get() = contact.localAlias
|
||||
override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase)
|
||||
|
||||
companion object {
|
||||
val sampleData = Direct(Contact.sampleData)
|
||||
@@ -1219,6 +1220,12 @@ data class Contact(
|
||||
override val localAlias get() = profile.localAlias
|
||||
val verified get() = activeConn?.connectionCode != null
|
||||
|
||||
override fun anyNameContains(searchAnyCase: String): Boolean {
|
||||
val s = searchAnyCase.trim().lowercase()
|
||||
return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s)
|
||||
}
|
||||
|
||||
|
||||
val directOrUsed: Boolean get() =
|
||||
if (activeConn != null) {
|
||||
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
|
||||
|
||||
+32
-3
@@ -1132,9 +1132,30 @@ object ChatController {
|
||||
|
||||
suspend fun apiSetConnectionIncognito(rh: Long?, connId: Long, incognito: Boolean): PendingContactConnection? {
|
||||
val r = sendCmd(rh, CC.ApiSetConnectionIncognito(connId, incognito))
|
||||
if (r is CR.ConnectionIncognitoUpdated) return r.toConnection
|
||||
Log.e(TAG, "apiSetConnectionIncognito bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
|
||||
return when (r) {
|
||||
is CR.ConnectionIncognitoUpdated -> r.toConnection
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? {
|
||||
val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId))
|
||||
|
||||
return when (r) {
|
||||
is CR.ConnectionUserChanged -> r.toConnection
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? {
|
||||
@@ -2916,6 +2937,7 @@ sealed class CC {
|
||||
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
|
||||
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
|
||||
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
|
||||
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
|
||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||
@@ -3071,6 +3093,7 @@ sealed class CC {
|
||||
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
|
||||
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
|
||||
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
|
||||
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
|
||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
@@ -3213,6 +3236,7 @@ sealed class CC {
|
||||
is APIVerifyGroupMember -> "apiVerifyGroupMember"
|
||||
is APIAddContact -> "apiAddContact"
|
||||
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
|
||||
is ApiChangeConnectionUser -> "apiChangeConnectionUser"
|
||||
is APIConnectPlan -> "apiConnectPlan"
|
||||
is APIConnect -> "apiConnect"
|
||||
is ApiConnectContactViaAddress -> "apiConnectContactViaAddress"
|
||||
@@ -4757,6 +4781,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
|
||||
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
|
||||
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
|
||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||
@@ -4935,6 +4960,7 @@ sealed class CR {
|
||||
is ConnectionVerified -> "connectionVerified"
|
||||
is Invitation -> "invitation"
|
||||
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
|
||||
is ConnectionUserChanged -> "ConnectionUserChanged"
|
||||
is CRConnectionPlan -> "connectionPlan"
|
||||
is SentConfirmation -> "sentConfirmation"
|
||||
is SentInvitation -> "sentInvitation"
|
||||
@@ -5103,6 +5129,7 @@ sealed class CR {
|
||||
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
||||
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
|
||||
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
||||
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
|
||||
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
|
||||
is SentConfirmation -> withUser(user, json.encodeToString(connection))
|
||||
is SentInvitation -> withUser(user, json.encodeToString(connection))
|
||||
@@ -5553,6 +5580,7 @@ sealed class ChatErrorType {
|
||||
is AgentCommandError -> "agentCommandError"
|
||||
is InvalidFileDescription -> "invalidFileDescription"
|
||||
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
|
||||
is ConnectionUserChangeProhibited -> "connectionUserChangeProhibited"
|
||||
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
|
||||
is InternalError -> "internalError"
|
||||
is CEException -> "exception $message"
|
||||
@@ -5630,6 +5658,7 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
|
||||
@Serializable @SerialName("connectionUserChangeProhibited") object ConnectionUserChangeProhibited: ChatErrorType()
|
||||
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
|
||||
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
remoteHostId = invitation.remoteHostId,
|
||||
userProfile = userProfile,
|
||||
contact = invitation.contact,
|
||||
callUUID = invitation.callUUID,
|
||||
callState = CallState.InvitationAccepted,
|
||||
localMedia = invitation.callType.media,
|
||||
sharedKey = invitation.sharedKey,
|
||||
|
||||
+1
@@ -115,6 +115,7 @@ fun PreviewIncomingCallAlertLayout() {
|
||||
contact = Contact.sampleData,
|
||||
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
|
||||
sharedKey = null,
|
||||
callUUID = "",
|
||||
callTs = Clock.System.now()
|
||||
),
|
||||
chatModel = ChatModel,
|
||||
|
||||
+2
@@ -13,6 +13,7 @@ data class Call(
|
||||
val remoteHostId: Long?,
|
||||
val userProfile: Profile,
|
||||
val contact: Contact,
|
||||
val callUUID: String?,
|
||||
val callState: CallState,
|
||||
val localMedia: CallMediaType,
|
||||
val localCapabilities: CallCapabilities? = null,
|
||||
@@ -105,6 +106,7 @@ sealed class WCallResponse {
|
||||
val contact: Contact,
|
||||
val callType: CallType,
|
||||
val sharedKey: String? = null,
|
||||
val callUUID: String,
|
||||
val callTs: Instant
|
||||
) {
|
||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||
|
||||
+5
-5
@@ -282,14 +282,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val versions = ciInfo.itemVersions
|
||||
if (versions.isNotEmpty()) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
versions.forEachIndexed { i, ciVersion ->
|
||||
ItemVersionView(ciVersion, current = i == 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_history), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
@@ -304,7 +304,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
QuotedMsgView(qi)
|
||||
}
|
||||
@@ -381,14 +381,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
|
||||
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
||||
if (mss.isNotEmpty()) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
mss.forEach { (member, status, sentViaProxy) ->
|
||||
MemberDeliveryStatusView(member, status, sentViaProxy)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
+2
-1
@@ -86,6 +86,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
.collect { chatId ->
|
||||
markUnreadChatAsRead(chatId)
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
}
|
||||
@@ -544,7 +545,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
|
||||
+2
-2
@@ -509,11 +509,11 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
|
||||
Modifier.weight(1f).padding(top = 8.dp, end = DEFAULT_PADDING, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
MemberProfileImage(size = DEFAULT_MIN_SECTION_ITEM_HEIGHT, member)
|
||||
MemberProfileImage(size = 42.dp, member)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
+5
-3
@@ -80,7 +80,7 @@ private fun ShareListNavLinkLayout(
|
||||
click: () -> Unit,
|
||||
stopped: Boolean,
|
||||
) {
|
||||
SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) {
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 8.dp), click = click, disabled = stopped) {
|
||||
chatLinkPreview()
|
||||
}
|
||||
Divider(Modifier.padding(horizontal = 8.dp))
|
||||
@@ -98,9 +98,11 @@ private fun SharePreviewView(chat: Chat, disabled: Boolean) {
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (chat.chatInfo is ChatInfo.Local) {
|
||||
ProfileImage(size = 46.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
|
||||
ProfileImage(size = 42.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
ProfileImage(size = 42.dp, chat.chatInfo.image, icon = MR.images.ic_supervised_user_circle_filled)
|
||||
} else {
|
||||
ProfileImage(size = 46.dp, chat.chatInfo.image)
|
||||
ProfileImage(size = 42.dp, chat.chatInfo.image)
|
||||
}
|
||||
Text(
|
||||
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
|
||||
+11
-4
@@ -94,10 +94,17 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
})
|
||||
UserPicker(
|
||||
chatModel,
|
||||
userPickerState,
|
||||
showSettings = false,
|
||||
showCancel = true,
|
||||
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart,
|
||||
cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -5,9 +5,7 @@ import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -66,7 +64,7 @@ fun DatabaseErrorView(
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
|
||||
SectionView(null, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content = content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+1
@@ -423,6 +423,7 @@ fun authStopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null,
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_stop_chat),
|
||||
generalGetString(MR.strings.auth_log_in_using_credential),
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, is LAResult.Unavailable -> {
|
||||
|
||||
+2
-1
@@ -85,7 +85,8 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
|
||||
Text(
|
||||
title.value,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
+3
-1
@@ -34,6 +34,7 @@ expect fun authenticate(
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean = false,
|
||||
usingLAMode: LAMode = ChatModel.controller.appPrefs.laMode.get(),
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
)
|
||||
|
||||
@@ -41,10 +42,11 @@ fun authenticateWithPasscode(
|
||||
promptTitle: String,
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean,
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password)))
|
||||
ModalManager.fullscreen.showPasscodeCustomModal { close ->
|
||||
ModalManager.fullscreen.showPasscodeCustomModal(oneTime) { close ->
|
||||
BackHandler {
|
||||
close()
|
||||
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
|
||||
|
||||
+13
-3
@@ -69,6 +69,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
// Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat():
|
||||
// java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
|
||||
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
val data = ModalData()
|
||||
@@ -105,9 +106,13 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
}
|
||||
}
|
||||
|
||||
fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showPasscodeCustomModal")
|
||||
passcodeView.value = modal
|
||||
fun showPasscodeCustomModal(oneTime: Boolean, modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showPasscodeCustomModal, oneTime: $oneTime")
|
||||
if (oneTime) {
|
||||
onTimePasscodeView.value = modal
|
||||
} else {
|
||||
passcodeView.value = modal
|
||||
}
|
||||
}
|
||||
|
||||
fun hasModalsOpen() = modalCount.value > 0
|
||||
@@ -179,6 +184,11 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
passcodeView.collectAsState().value?.invoke { passcodeView.value = null }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun showOneTimePasscodeInView() {
|
||||
onTimePasscodeView.collectAsState().value?.invoke { onTimePasscodeView.value = null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to modify a list without getting [ConcurrentModificationException]
|
||||
* */
|
||||
|
||||
+3
-3
@@ -21,15 +21,15 @@ import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit)) {
|
||||
fun SectionView(title: String? = null, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) {
|
||||
Column {
|
||||
if (title != null) {
|
||||
Text(
|
||||
title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING), fontSize = 12.sp
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = headerBottomPadding), fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
|
||||
Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ fun ContactConnectionInfoView(
|
||||
) {
|
||||
LaunchedEffect(connReqInvitation) {
|
||||
if (connReqInvitation != null) {
|
||||
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false, conn = contactConnection)
|
||||
}
|
||||
}
|
||||
/** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation].
|
||||
|
||||
+112
-113
@@ -1,7 +1,9 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionDivider
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
@@ -164,6 +166,10 @@ private fun NewChatSheetLayout(
|
||||
)
|
||||
|
||||
val sectionModifier = Modifier.fillMaxWidth()
|
||||
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
|
||||
}
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
@@ -224,7 +230,9 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(Modifier.padding(bottom = 27.dp))
|
||||
if (searchText.value.text.isEmpty()) {
|
||||
Spacer(Modifier.padding(bottom = 27.dp))
|
||||
}
|
||||
|
||||
val actionButtonsOriginal = listOf(
|
||||
Triple(
|
||||
@@ -262,37 +270,30 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
|
||||
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
|
||||
}
|
||||
if (deletedChats.isNotEmpty()) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
Row(modifier = sectionModifier) {
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
closeOnTop = !oneHandUI.value,
|
||||
) {
|
||||
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { closeDeletedChats ->
|
||||
ModalView(
|
||||
close = closeDeletedChats,
|
||||
closeOnTop = !oneHandUI.value,
|
||||
) {
|
||||
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
|
||||
ModalManager.start.closeModals()
|
||||
})
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
contentDescription = stringResource(MR.strings.deleted_chats),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,16 +301,28 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isNotEmpty() && !oneHandUI.value) {
|
||||
if (searchText.value.text.isNotEmpty()) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
} else {
|
||||
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
|
||||
if (!oneHandUI.value) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
} else {
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(MR.strings.contact_list_header_title).uppercase(), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
|
||||
modifier = sectionModifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF), fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,17 +335,6 @@ private fun NewChatSheetLayout(
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -555,78 +557,75 @@ private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Un
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
) { contentPadding ->
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value
|
||||
val allChats by remember(chatModel.chats.value) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) }
|
||||
}
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = oneHandUI.value,
|
||||
) {
|
||||
if (!oneHandUI.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value
|
||||
val allChats by remember(chatModel.chats.value) {
|
||||
derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) }
|
||||
}
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
reverseLayout = oneHandUI.value,
|
||||
) {
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
}
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Divider()
|
||||
|
||||
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
|
||||
}
|
||||
}
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.deleted_chats),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (!oneHandUI.value) {
|
||||
Divider()
|
||||
}
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
searchShowingSimplexLink = searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
|
||||
close = close,
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+326
-20
@@ -2,20 +2,25 @@ package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
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.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -32,6 +37,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@@ -43,7 +49,7 @@ enum class NewChatOption {
|
||||
fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) {
|
||||
val selection = remember { stateGetOrPut("selection") { selection } }
|
||||
val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } }
|
||||
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) }
|
||||
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) }
|
||||
val connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } }
|
||||
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
|
||||
val pastedLink = rememberSaveable { mutableStateOf("") }
|
||||
@@ -177,6 +183,15 @@ private fun CreatingLinkProgressView() {
|
||||
DefaultProgressView(stringResource(MR.strings.creating_link))
|
||||
}
|
||||
|
||||
private fun updateShownConnection(conn: PendingContactConnection) {
|
||||
chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(
|
||||
conn = conn,
|
||||
connId = conn.id,
|
||||
connReq = conn.connReqInv ?: "",
|
||||
connChatUsed = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RetryButton(onClick: () -> Unit) {
|
||||
Column(
|
||||
@@ -192,35 +207,326 @@ private fun RetryButton(onClick: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfilePickerOption(
|
||||
title: String,
|
||||
selected: Boolean,
|
||||
disabled: Boolean,
|
||||
onSelected: () -> Unit,
|
||||
image: @Composable () -> Unit,
|
||||
onInfo: (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
|
||||
.clickable(enabled = !disabled, onClick = onSelected)
|
||||
.padding(horizontal = DEFAULT_PADDING, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
image()
|
||||
TextIconSpaced(false)
|
||||
Text(title, modifier = Modifier.align(Alignment.CenterVertically))
|
||||
if (onInfo != null) {
|
||||
Spacer(Modifier.padding(6.dp))
|
||||
Column(Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
enabled = !disabled,
|
||||
onClick = { ModalManager.start.showModal { IncognitoView() } }
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_info),
|
||||
stringResource(MR.strings.incognito),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (selected) {
|
||||
Icon(
|
||||
painterResource(
|
||||
MR.images.ic_check
|
||||
),
|
||||
title,
|
||||
Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(
|
||||
Modifier.padding(
|
||||
start = DEFAULT_PADDING_HALF,
|
||||
end = DEFAULT_PADDING_HALF,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun filteredProfiles(users: List<User>, searchTextOrPassword: String): List<User> {
|
||||
val s = searchTextOrPassword.trim()
|
||||
val lower = s.lowercase()
|
||||
return users.filter { u ->
|
||||
if ((u.activeUser || !u.hidden) && (s == "" || u.anyNameContains(lower))) {
|
||||
true
|
||||
} else {
|
||||
correctPassword(u, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveProfilePicker(
|
||||
search: MutableState<String>,
|
||||
contactConnection: PendingContactConnection?,
|
||||
close: () -> Unit,
|
||||
rhId: Long?
|
||||
) {
|
||||
val switchingProfile = remember { mutableStateOf(false) }
|
||||
val incognito = remember {
|
||||
chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get()
|
||||
}
|
||||
val selectedProfile by remember { chatModel.currentUser }
|
||||
val searchTextOrPassword = rememberSaveable { search }
|
||||
val profiles = remember {
|
||||
chatModel.users.map { it.user }.sortedBy { !it.activeUser }
|
||||
}
|
||||
val filteredProfiles by remember {
|
||||
derivedStateOf { filteredProfiles(profiles, searchTextOrPassword.value) }
|
||||
}
|
||||
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(switchingProfile.value) {
|
||||
progressByTimeout = if (switchingProfile.value) {
|
||||
delay(500)
|
||||
switchingProfile.value
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfilePickerUserOption(user: User) {
|
||||
val selected = selectedProfile?.userId == user.userId && !incognito
|
||||
|
||||
ProfilePickerOption(
|
||||
title = user.chatViewName,
|
||||
disabled = switchingProfile.value || selected,
|
||||
selected = selected,
|
||||
onSelected = {
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
if (contactConnection != null) {
|
||||
val conn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId)
|
||||
if (conn != null) {
|
||||
withChats {
|
||||
updateContactConnection(rhId, conn)
|
||||
updateShownConnection(conn)
|
||||
}
|
||||
controller.changeActiveUser_(
|
||||
rhId = user.remoteHostId,
|
||||
toUserId = user.userId,
|
||||
viewPwd = if (user.hidden) searchTextOrPassword.value else null
|
||||
)
|
||||
|
||||
if (chatModel.currentUser.value?.userId != user.userId) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(
|
||||
MR.strings.switching_profile_error_title),
|
||||
String.format(generalGetString(MR.strings.switching_profile_error_message), user.chatViewName)
|
||||
)
|
||||
}
|
||||
|
||||
withChats {
|
||||
updateContactConnection(user.remoteHostId, conn)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
switchingProfile.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
image = { ProfileImage(size = 42.dp, image = user.image) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoUserOption() {
|
||||
ProfilePickerOption(
|
||||
disabled = switchingProfile.value,
|
||||
title = stringResource(MR.strings.incognito),
|
||||
selected = incognito,
|
||||
onSelected = {
|
||||
if (!incognito) {
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
if (contactConnection != null) {
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true)
|
||||
|
||||
if (conn != null) {
|
||||
withChats {
|
||||
updateContactConnection(rhId, conn)
|
||||
updateShownConnection(conn)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
switchingProfile.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
image = {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy_filled),
|
||||
contentDescription = stringResource(MR.strings.incognito),
|
||||
Modifier.size(32.dp),
|
||||
tint = Indigo,
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
},
|
||||
onInfo = { ModalManager.start.showModal { IncognitoView() } },
|
||||
)
|
||||
}
|
||||
|
||||
BoxWithConstraints {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(if (progressByTimeout) 0.6f else 1f)
|
||||
) {
|
||||
LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) {
|
||||
item {
|
||||
AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING)
|
||||
}
|
||||
val activeProfile = filteredProfiles.firstOrNull { it.activeUser }
|
||||
|
||||
if (activeProfile != null) {
|
||||
val otherProfiles = filteredProfiles.filter { it.userId != activeProfile.userId }
|
||||
|
||||
if (incognito) {
|
||||
item {
|
||||
IncognitoUserOption()
|
||||
}
|
||||
item {
|
||||
ProfilePickerUserOption(activeProfile)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
ProfilePickerUserOption(activeProfile)
|
||||
}
|
||||
item {
|
||||
IncognitoUserOption()
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(otherProfiles) { _, p ->
|
||||
ProfilePickerUserOption(p)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
IncognitoUserOption()
|
||||
}
|
||||
itemsIndexed(filteredProfiles) { _, p ->
|
||||
ProfilePickerUserOption(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (progressByTimeout) {
|
||||
DefaultProgressView("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState<PendingContactConnection?>) {
|
||||
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase()) {
|
||||
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
|
||||
LinkTextView(connReqInvitation, true)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase()) {
|
||||
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase(), headerBottomPadding = 5.dp) {
|
||||
SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() })
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) }
|
||||
IncognitoToggle(controller.appPrefs.incognito, incognito) {
|
||||
ModalManager.start.showModal { IncognitoView() }
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) {
|
||||
derivedStateOf {
|
||||
chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get()
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(incognito.value) {
|
||||
withBGApi {
|
||||
val contactConn = contactConnection.value ?: return@withBGApi
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi
|
||||
withChats {
|
||||
contactConnection.value = conn
|
||||
updateContactConnection(rhId, conn)
|
||||
val currentUser = remember { chatModel.currentUser }.value
|
||||
|
||||
if (currentUser != null) {
|
||||
SectionView(stringResource(MR.strings.new_chat_share_profile).uppercase(), headerBottomPadding = 5.dp) {
|
||||
SectionItemView(
|
||||
padding = PaddingValues(
|
||||
top = 0.dp,
|
||||
bottom = 0.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp
|
||||
),
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
content = {
|
||||
ActiveProfilePicker(
|
||||
search = search,
|
||||
close = close,
|
||||
rhId = rhId,
|
||||
contactConnection = contactConnection.value
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (incognito) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy_filled),
|
||||
contentDescription = stringResource(MR.strings.incognito),
|
||||
tint = Indigo,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
} else {
|
||||
ProfileImage(size = 42.dp, image = currentUser.image)
|
||||
}
|
||||
TextIconSpaced(false)
|
||||
Text(
|
||||
text = if (incognito) stringResource(MR.strings.incognito) else currentUser.chatViewName,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End) {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_arrow_forward_ios),
|
||||
contentDescription = stringResource(MR.strings.new_chat_share_profile),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
chatModel.markShowingInvitationUsed()
|
||||
if (incognito) {
|
||||
SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(sharedProfileInfo(chatModel, incognito.value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -242,14 +548,14 @@ fun AddContactLearnMoreButton() {
|
||||
|
||||
@Composable
|
||||
private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit) {
|
||||
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase()) {
|
||||
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) {
|
||||
PasteLinkView(rhId, pastedLink, showQRCodeScanner, close)
|
||||
}
|
||||
|
||||
if (appPlatform.isAndroid) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase()) {
|
||||
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) {
|
||||
QRCodeScanner(showQRCodeScanner) { text ->
|
||||
withBGApi {
|
||||
val res = verify(rhId, text, close)
|
||||
@@ -366,7 +672,7 @@ private fun createInvitation(
|
||||
if (r != null) {
|
||||
withChats {
|
||||
updateContactConnection(rhId, r.second)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false)
|
||||
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = r.second)
|
||||
contactConnection.value = r.second
|
||||
}
|
||||
} else {
|
||||
|
||||
+5
-7
@@ -8,9 +8,7 @@ import SectionSpacer
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -149,7 +147,7 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<Re
|
||||
@Composable
|
||||
private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
||||
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
@@ -199,7 +197,7 @@ private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList<
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.waiting_for_desktop), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
SectionSpacer()
|
||||
@@ -226,7 +224,7 @@ private fun FoundDesktop(
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
CtrlDeviceVersionText(session)
|
||||
if (!compatible) {
|
||||
@@ -258,7 +256,7 @@ private fun FoundDesktop(
|
||||
@Composable
|
||||
private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
AppBarTitle(stringResource(MR.strings.verify_connection))
|
||||
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
@@ -313,7 +311,7 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
|
||||
@Composable
|
||||
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) {
|
||||
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(rc.deviceViewName)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ expect fun AppearanceView(m: ChatModel)
|
||||
object AppearanceScope {
|
||||
@Composable
|
||||
fun ProfileImageSection() {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
val image = remember { chatModel.currentUser }.value?.image
|
||||
Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
val size = 60
|
||||
@@ -86,7 +86,7 @@ object AppearanceScope {
|
||||
@Composable
|
||||
fun FontScaleSection() {
|
||||
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
|
||||
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(60.dp)
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
|
||||
|
||||
+1
-4
@@ -1,15 +1,12 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionItemViewWithoutMinPadding
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -64,7 +61,7 @@ private fun HiddenProfileLayout(
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.hide_profile))
|
||||
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
UserProfileRow(user)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
+1
-2
@@ -22,7 +22,6 @@ import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
@@ -264,7 +263,7 @@ fun SocksProxySettings(
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(MR.strings.host_verb),
|
||||
|
||||
+16
-20
@@ -1,12 +1,10 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
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.*
|
||||
@@ -34,8 +32,6 @@ import chat.simplex.common.views.onboarding.ReadableText
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class LAMode {
|
||||
SYSTEM,
|
||||
@@ -374,7 +370,8 @@ fun SimplexLockView(
|
||||
currentLAMode: SharedPreference<LAMode>,
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
) {
|
||||
val performLA = remember { chatModel.performLA }
|
||||
val showAuthScreen = remember { chatModel.showAuthScreen }
|
||||
val performLA = remember { appPrefs.performLA.state }
|
||||
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
|
||||
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
|
||||
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
|
||||
@@ -382,13 +379,9 @@ fun SimplexLockView(
|
||||
val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") }
|
||||
val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName }
|
||||
|
||||
fun resetLAEnabled(onOff: Boolean) {
|
||||
chatModel.controller.appPrefs.performLA.set(onOff)
|
||||
chatModel.performLA.value = onOff
|
||||
}
|
||||
|
||||
fun disableUnavailableLA() {
|
||||
resetLAEnabled(false)
|
||||
chatModel.controller.appPrefs.performLA.set(false)
|
||||
chatModel.showAuthScreen.value = false
|
||||
currentLAMode.set(LAMode.default)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
@@ -405,7 +398,8 @@ fun SimplexLockView(
|
||||
} else {
|
||||
generalGetString(MR.strings.chat_lock)
|
||||
},
|
||||
generalGetString(MR.strings.change_lock_mode)
|
||||
generalGetString(MR.strings.change_lock_mode),
|
||||
oneTime = true,
|
||||
) { laResult ->
|
||||
when (laResult) {
|
||||
is LAResult.Error -> {
|
||||
@@ -415,7 +409,7 @@ fun SimplexLockView(
|
||||
LAResult.Success -> {
|
||||
when (toLAMode) {
|
||||
LAMode.SYSTEM -> {
|
||||
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode, oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
currentLAMode.set(toLAMode)
|
||||
@@ -451,7 +445,7 @@ fun SimplexLockView(
|
||||
}
|
||||
|
||||
fun toggleSelfDestruct(selfDestruct: SharedPreference<Boolean>) {
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode)) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode), oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
is LAResult.Error -> laFailedAlert()
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
@@ -470,7 +464,7 @@ fun SimplexLockView(
|
||||
}
|
||||
|
||||
fun changeLAPassword() {
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode), oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
@@ -494,7 +488,7 @@ fun SimplexLockView(
|
||||
}
|
||||
|
||||
fun changeSelfDestructPassword() {
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult ->
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode), oneTime = true) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
@@ -525,8 +519,8 @@ fun SimplexLockView(
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.chat_lock))
|
||||
SectionView {
|
||||
EnableLock(performLA) { performLAToggle ->
|
||||
performLA.value = performLAToggle
|
||||
EnableLock(remember { appPrefs.performLA.state }) { performLAToggle ->
|
||||
showAuthScreen.value = performLAToggle
|
||||
chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (performLAToggle) {
|
||||
when (currentLAMode.state.value) {
|
||||
@@ -543,7 +537,9 @@ fun SimplexLockView(
|
||||
passcodeAlert(generalGetString(MR.strings.passcode_set))
|
||||
},
|
||||
cancel = {
|
||||
resetLAEnabled(false)
|
||||
chatModel.showAuthScreen.value = false
|
||||
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
|
||||
// chatModel.controller.appPrefs.performLA.set(false)
|
||||
},
|
||||
close = close
|
||||
)
|
||||
@@ -660,7 +656,7 @@ private fun EnableSelfDestruct(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
|
||||
private fun EnableLock(performLA: State<Boolean>, onCheckedChange: (Boolean) -> Unit) {
|
||||
SectionItemView {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
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
|
||||
@@ -234,7 +235,7 @@ fun ChatLockItem(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
) {
|
||||
val performLA = remember { ChatModel.performLA }
|
||||
val performLA = remember { appPrefs.performLA.state }
|
||||
val currentLAMode = remember { ChatModel.controller.appPrefs.laMode }
|
||||
SettingsActionItemWithContent(
|
||||
click = showSettingsModal { SimplexLockView(ChatModel, currentLAMode, setPerformLA) },
|
||||
@@ -505,6 +506,7 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) ->
|
||||
authenticate(
|
||||
title,
|
||||
desc,
|
||||
oneTime = true,
|
||||
completed = { laResult ->
|
||||
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
|
||||
}
|
||||
|
||||
+3
-4
@@ -8,7 +8,6 @@ import SectionItemViewWithoutMinPadding
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -270,7 +269,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (
|
||||
|
||||
@Composable fun ActionHeader(title: StringResource) {
|
||||
AppBarTitle(stringResource(title))
|
||||
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
UserProfileRow(user)
|
||||
}
|
||||
SectionSpacer()
|
||||
@@ -304,7 +303,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
|
||||
fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
|
||||
val s = searchTextOrPassword.trim()
|
||||
val lower = s.lowercase()
|
||||
return m.users.filter { u ->
|
||||
@@ -318,7 +317,7 @@ private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User
|
||||
|
||||
private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size
|
||||
|
||||
private fun correctPassword(user: User, pwd: String): Boolean {
|
||||
fun correctPassword(user: User, pwd: String): Boolean {
|
||||
val ph = user.viewPwdHash
|
||||
return ph != null && pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
|
||||
}
|
||||
|
||||
@@ -665,6 +665,10 @@
|
||||
<string name="one_time_link_short">1-time link</string>
|
||||
<string name="simplex_address">SimpleX address</string>
|
||||
<string name="or_show_this_qr_code">Or show this code</string>
|
||||
<string name="new_chat_share_profile">Share profile</string>
|
||||
<string name="select_chat_profile">Select chat profile</string>
|
||||
<string name="switching_profile_error_title">Error switching profile</string>
|
||||
<string name="switching_profile_error_message">Your connection was moved to %s but an unexpected error occurred while redirecting you to the profile.</string>
|
||||
<string name="or_scan_qr_code">Or scan QR code</string>
|
||||
<string name="keep_unused_invitation_question">Keep unused invitation?</string>
|
||||
<string name="you_can_view_invitation_link_again">You can view invitation link again in connection details.</string>
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
@@ -27,7 +26,6 @@ import kotlinx.coroutines.*
|
||||
import java.awt.event.WindowEvent
|
||||
import java.awt.event.WindowFocusListener
|
||||
import java.io.File
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
val simplexWindowState = SimplexWindowState()
|
||||
@@ -172,7 +170,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
|
||||
var windowFocused by remember { simplexWindowState.windowFocused }
|
||||
LaunchedEffect(windowFocused) {
|
||||
val delay = ChatController.appPrefs.laLockDelay.get()
|
||||
if (!windowFocused && ChatModel.performLA.value && delay > 0) {
|
||||
if (!windowFocused && ChatModel.showAuthScreen.value && delay > 0) {
|
||||
delay(delay * 1000L)
|
||||
// Trigger auth state check when delay ends (and if it ends)
|
||||
AppLock.recheckAuthState()
|
||||
|
||||
+85
-13
@@ -19,12 +19,76 @@ import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import kotlin.math.min
|
||||
|
||||
data class SemVer(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val patch: Int,
|
||||
val preRelease: String? = null,
|
||||
val buildNumber: Int? = null,
|
||||
): Comparable<SemVer?> {
|
||||
|
||||
val isNotStable: Boolean = preRelease != null
|
||||
|
||||
override fun compareTo(other: SemVer?): Int {
|
||||
if (other == null) return 1
|
||||
return when {
|
||||
major != other.major -> major.compareTo(other.major)
|
||||
minor != other.minor -> minor.compareTo(other.minor)
|
||||
patch != other.patch -> patch.compareTo(other.patch)
|
||||
preRelease != null && other.preRelease != null -> {
|
||||
val pr = preRelease.compareTo(other.preRelease, ignoreCase = true)
|
||||
when {
|
||||
pr != 0 -> pr
|
||||
buildNumber != null && other.buildNumber != null -> buildNumber.compareTo(other.buildNumber)
|
||||
buildNumber != null -> -1
|
||||
other.buildNumber != null -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
preRelease != null -> -1
|
||||
other.preRelease != null -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([A-Za-z]+)\\.(\\d+))?\$")
|
||||
fun from(tagName: String): SemVer? {
|
||||
val trimmed = tagName.trimStart { it == 'v' }
|
||||
val redacted = when {
|
||||
trimmed.contains('-') && trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed.substringBefore('-')}.0-${trimmed.substringAfter('-')}"
|
||||
trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed}.0"
|
||||
else -> trimmed
|
||||
}
|
||||
val group = regex.matchEntire(redacted)?.groups
|
||||
return if (group != null) {
|
||||
SemVer(
|
||||
major = group[1]?.value?.toIntOrNull() ?: return null,
|
||||
minor = group[2]?.value?.toIntOrNull() ?: return null,
|
||||
patch = group[3]?.value?.toIntOrNull() ?: return null,
|
||||
preRelease = group[4]?.value,
|
||||
buildNumber = group[5]?.value?.toIntOrNull(),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun fromCurrentVersionName(): SemVer? {
|
||||
val currentVersionName = if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME
|
||||
return from(currentVersionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GitHubRelease(
|
||||
@@ -34,12 +98,18 @@ data class GitHubRelease(
|
||||
val htmlUrl: String,
|
||||
val name: String,
|
||||
val draft: Boolean,
|
||||
val prerelease: Boolean,
|
||||
@SerialName("prerelease")
|
||||
private val preRelease: Boolean,
|
||||
val body: String,
|
||||
@SerialName("published_at")
|
||||
val publishedAt: String,
|
||||
val assets: List<GitHubAsset>
|
||||
)
|
||||
) {
|
||||
@Transient
|
||||
val semVer: SemVer? = SemVer.from(tagName)
|
||||
|
||||
val isConsideredBeta: Boolean = preRelease || semVer == null || semVer.isNotStable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GitHubAsset(
|
||||
@@ -105,25 +175,25 @@ private fun createUpdateJob() {
|
||||
|
||||
fun checkForUpdate() {
|
||||
Log.d(TAG, "Checking for update")
|
||||
val currentSemVer = SemVer.fromCurrentVersionName()
|
||||
if (currentSemVer == null) {
|
||||
Log.e(TAG, "Current SemVer cannot be parsed")
|
||||
return
|
||||
}
|
||||
val client = setupHttpClient()
|
||||
try {
|
||||
val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
response.body?.use {
|
||||
val body = it.string()
|
||||
val releases = json.decodeFromString<List<GitHubRelease>>(body).filterNot { it.draft }
|
||||
val releases = json.decodeFromString<List<GitHubRelease>>(body)
|
||||
val release = when (appPrefs.appUpdateChannel.get()) {
|
||||
AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease }
|
||||
AppUpdatesChannel.BETA -> releases.firstOrNull()
|
||||
AppUpdatesChannel.STABLE -> releases.firstOrNull { r -> !r.draft && !r.isConsideredBeta && currentSemVer < r.semVer }
|
||||
AppUpdatesChannel.BETA -> releases.firstOrNull { r -> !r.draft && currentSemVer < r.semVer }
|
||||
AppUpdatesChannel.DISABLED -> return
|
||||
} ?: return
|
||||
val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME)
|
||||
val redactedCurrentVersionName = when {
|
||||
currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}"
|
||||
currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0"
|
||||
else -> currentVersionName
|
||||
}
|
||||
if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) {
|
||||
|
||||
if (release == null || release.tagName == appPrefs.appSkippedUpdate.get()) {
|
||||
Log.d(TAG, "Skipping update because of the same version or skipped version")
|
||||
return
|
||||
}
|
||||
@@ -298,13 +368,15 @@ private suspend fun downloadAsset(asset: GitHubAsset) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRunningFromAppImage(): Boolean = System.getenv("APPIMAGE") != null
|
||||
|
||||
private fun isRunningFromFlatpak(): Boolean = System.getenv("container") == "flatpak"
|
||||
|
||||
private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset> {
|
||||
val res = if (isRunningFromFlatpak()) {
|
||||
// No need to show download options for Flatpak users
|
||||
emptyList()
|
||||
} else if (Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
} else if (!isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
// Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives)
|
||||
release.assets.filter { it.name.lowercase().endsWith(".deb") }
|
||||
} else {
|
||||
|
||||
+2
-1
@@ -7,10 +7,11 @@ actual fun authenticate(
|
||||
promptSubtitle: String,
|
||||
selfDestruct: Boolean,
|
||||
usingLAMode: LAMode,
|
||||
oneTime: Boolean,
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
when (usingLAMode) {
|
||||
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
|
||||
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -25,7 +25,6 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
actual fun AppearanceView(m: ChatModel) {
|
||||
@@ -44,7 +43,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.appearance_settings))
|
||||
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_language), contentPadding = PaddingValues()) {
|
||||
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
|
||||
LangSelector(state) {
|
||||
state.value = it
|
||||
@@ -79,7 +78,7 @@ fun AppearanceScope.AppearanceLayout(
|
||||
@Composable
|
||||
fun DensityScaleSection() {
|
||||
val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) }
|
||||
SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(60.dp)
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.views.helpers.SemVer
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// use this command for testing:
|
||||
// ./gradlew desktopTest
|
||||
class SemVerTest {
|
||||
@Test
|
||||
fun testValidSemVer() {
|
||||
assertEquals(SemVer.from("1.0.0"), SemVer(1, 0, 0))
|
||||
assertEquals(SemVer.from("1.0"), SemVer(1, 0, 0))
|
||||
assertEquals(SemVer.from("v1.0"), SemVer(1, 0, 0))
|
||||
assertEquals(SemVer.from("v1.0-beta.1"), SemVer(1, 0, 0, "beta", 1))
|
||||
val r = listOf<Pair<String, SemVer>>(
|
||||
"0.0.4" to SemVer(0, 0, 4),
|
||||
"1.2.3" to SemVer(1, 2, 3),
|
||||
"10.20.30" to SemVer(10, 20, 30),
|
||||
"1.0.0-alpha.1" to SemVer(1, 0, 0, "alpha", buildNumber = 1),
|
||||
"1.0.0" to SemVer(1, 0, 0),
|
||||
"2.0.0" to SemVer(2, 0, 0),
|
||||
"1.1.7" to SemVer(1, 1, 7),
|
||||
"2.0.1-alpha.1227" to SemVer(2, 0, 1, "alpha", 1227),
|
||||
)
|
||||
r.forEach { (value, correct) ->
|
||||
assertEquals(SemVer.from(value), correct)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testComparisonSemVer() {
|
||||
assert(SemVer(0, 1, 0) == SemVer.from("0.1.0"))
|
||||
assert(SemVer(1, 1, 0) == SemVer.from("v1.1.0"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 0, 1))
|
||||
assert(SemVer(1, 0, 0) > SemVer(0, 100, 100))
|
||||
assert(SemVer(0, 200, 0) > SemVer(0, 100, 100))
|
||||
assert(SemVer(0, 1, 0, "beta") > SemVer(0, 1, 0, "alpha"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "alpha"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta"))
|
||||
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta.0"))
|
||||
assert(SemVer(0, 1, 0, "beta", 1) > SemVer(0, 1, 0, "beta", 0))
|
||||
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 10))
|
||||
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 9))
|
||||
assert(SemVer(0, 1, 0, "beta.1") > SemVer(0, 1, 0, "alpha.2"))
|
||||
assert(SemVer(1, 1, 0, "beta.1") > SemVer(0, 1, 0, "beta.1"))
|
||||
assert(SemVer(1, 0, 0) > SemVer(1, 0, 0, "beta.1"))
|
||||
assert(SemVer(1, 0, 0) > null)
|
||||
assert(SemVer.from("v6.0.0")!! > SemVer.from("v6.0.0-beta.3"))
|
||||
assert(SemVer.from("v6.0.0-beta.3")!! > SemVer.from("v6.0.0-beta.2"))
|
||||
assert(SemVer.from("0.1.0") == SemVer.from("0.1.0"))
|
||||
assert(SemVer.from("0.1.1")!! > SemVer.from("0.1.0"))
|
||||
assert(SemVer.from("0.2.1")!! > SemVer.from("0.1.1"))
|
||||
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
|
||||
assert(SemVer.from("0.1.1-beta.0")!! > SemVer.from("0.1.0-beta.0"))
|
||||
assert(SemVer.from("0.1.1-beta.0")!! == SemVer.from("0.1.1-beta.0"))
|
||||
assert(SemVer.from("0.1.1-beta.1")!! > SemVer.from("0.1.1-beta.0"))
|
||||
assert(SemVer.from("10.0.0-beta.12")!! > SemVer.from("1.1.1"))
|
||||
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.9"))
|
||||
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.120"))
|
||||
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.0.3
|
||||
android.version_code=235
|
||||
android.version_name=6.0.4
|
||||
android.version_code=237
|
||||
|
||||
desktop.version_name=6.0.3
|
||||
desktop.version_code=64
|
||||
desktop.version_name=6.0.4
|
||||
desktop.version_code=65
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
Reference in New Issue
Block a user