mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 00:44:27 +00:00
wip
This commit is contained in:
+18
-4
@@ -45,15 +45,22 @@ import kotlin.collections.ArrayList
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.*
|
||||
|
||||
// A directory search and a connection can overlap - tapping a result starts a connection while
|
||||
// the search is still running - so the single progress slot records its owner: a late search
|
||||
// result must not clear the spinner that now belongs to the connection.
|
||||
enum class ConnectProgressOwner { Connect, DirectorySearch }
|
||||
|
||||
object ConnectProgressManager {
|
||||
private val connectInProgress = mutableStateOf<String?>(null)
|
||||
private val connectProgressByTimeout = mutableStateOf(false)
|
||||
private var onCancel: (() -> Unit)? = null
|
||||
private var owner: ConnectProgressOwner? = null
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
fun startConnectProgress(text: String, onCancel: (() -> Unit)? = null) {
|
||||
fun startConnectProgress(text: String, owner: ConnectProgressOwner = ConnectProgressOwner.Connect, onCancel: (() -> Unit)? = null) {
|
||||
connectInProgress.value = text
|
||||
this.owner = owner
|
||||
this.onCancel = onCancel
|
||||
coroutineScope.launch {
|
||||
delay(1000)
|
||||
@@ -61,15 +68,22 @@ object ConnectProgressManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun stopConnectProgress() {
|
||||
fun stopConnectProgress(owner: ConnectProgressOwner = ConnectProgressOwner.Connect) {
|
||||
if (this.owner != null && this.owner != owner) return
|
||||
connectInProgress.value = null
|
||||
this.owner = null
|
||||
onCancel = null
|
||||
connectProgressByTimeout.value = false
|
||||
}
|
||||
|
||||
// a user-initiated cancel, and the takeover in planAndConnect, cancel whatever is running
|
||||
fun cancelConnectProgress() {
|
||||
onCancel?.invoke()
|
||||
stopConnectProgress()
|
||||
val cancel = onCancel
|
||||
owner = null
|
||||
onCancel = null
|
||||
connectInProgress.value = null
|
||||
connectProgressByTimeout.value = false
|
||||
cancel?.invoke()
|
||||
}
|
||||
|
||||
val showConnectProgress: String? get() =
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package chat.simplex.common.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
// The directory's contact address, as published in docs/DIRECTORY.md. It must be the short
|
||||
// link: only that form carries the address DR keys that service requests require, so the full
|
||||
// links on the What's New cards cannot be substituted here.
|
||||
const val DIRECTORY_SERVICE_LINK = "https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok"
|
||||
|
||||
// A service request is a full DR handshake, so it is slower than a local API call; the user
|
||||
// gets a cancellable spinner while it runs and a retry row if it times out.
|
||||
const val DIRECTORY_SEARCH_TIMEOUT_SEC = 10.0
|
||||
|
||||
@Serializable
|
||||
data class DirectoryPublicLink(
|
||||
val connFullLink: String? = null,
|
||||
val connShortLink: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DirectoryEntryType(
|
||||
val groupType: GroupType? = null,
|
||||
val summary: GroupSummary,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DirectorySearchEntry(
|
||||
val entryType: DirectoryEntryType,
|
||||
val displayName: String,
|
||||
val simplexName: String? = null,
|
||||
val groupLink: DirectoryPublicLink,
|
||||
val shortDescr: String? = null,
|
||||
val image: String? = null,
|
||||
val activeAt: Instant? = null,
|
||||
val createdAt: Instant? = null,
|
||||
) {
|
||||
// the directory drops entries with no link, but the response is untrusted input
|
||||
val connectLink: String? get() = groupLink.connShortLink ?: groupLink.connFullLink
|
||||
}
|
||||
|
||||
data class DirectorySearchResults(
|
||||
val entries: List<DirectorySearchEntry>,
|
||||
// opaque: stored and echoed back on the next request, never inspected
|
||||
val cursor: JsonObject?,
|
||||
)
|
||||
|
||||
fun directorySearchRequest(text: String, cursor: JsonObject?): JsonObject = buildJsonObject {
|
||||
put("type", JsonPrimitive("search"))
|
||||
put("searchText", JsonPrimitive(text))
|
||||
if (cursor != null) put("searchCursor", cursor)
|
||||
}
|
||||
|
||||
// The response is a tagged object: searchResults or error. Anything else is treated as a failure
|
||||
// rather than parsed leniently - it comes from outside the app.
|
||||
fun parseDirectorySearchResponse(resp: JsonObject): DirectorySearchResults? =
|
||||
when ((resp["type"] as? JsonPrimitive)?.contentOrNull) {
|
||||
"searchResults" -> {
|
||||
val entries = (resp["entries"] as? JsonArray)?.mapNotNull {
|
||||
runCatching { json.decodeFromJsonElement<DirectorySearchEntry>(it) }.getOrNull()
|
||||
} ?: emptyList()
|
||||
DirectorySearchResults(entries, resp["searchCursor"] as? JsonObject)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
+18
@@ -1531,6 +1531,17 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
// Blocks until the directory replies or the timeout elapses, so callers must use
|
||||
// withLongRunningApi, not the single-threaded withBGApi.
|
||||
suspend fun apiSearchDirectory(rh: Long?, text: String, cursor: JsonObject?): DirectorySearchResults? {
|
||||
val userId = kotlin.runCatching { currentUserId("apiSearchDirectory") }.getOrElse { return null }
|
||||
val req = directorySearchRequest(text, cursor)
|
||||
val r = sendCmdWithRetry(rh, CC.APISendServiceRequest(userId, DIRECTORY_SERVICE_LINK, DIRECTORY_SEARCH_TIMEOUT_SEC, req))
|
||||
if (r is API.Result && r.res is CR.CRServiceResponse) return parseDirectorySearchResponse(r.res.responseData)
|
||||
Log.e(TAG, "apiSearchDirectory error: $r")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiConnectPlan(rh: Long?, connLink: String, resolveMode: PlanResolveMode = PlanResolveMode.PRMUnknown, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState<Boolean>): ConnectionPlanResult? {
|
||||
val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null }
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, resolveMode, linkOwnerSig), inProgress = inProgress)
|
||||
@@ -3879,6 +3890,7 @@ sealed class 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 connLink: String, val resolveMode: PlanResolveMode = PlanResolveMode.PRMUnknown, val linkOwnerSig: LinkOwnerSig? = null): CC()
|
||||
class APISendServiceRequest(val userId: Long, val target: String, val timeoutSec: Double?, val request: JsonObject): CC()
|
||||
class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData, val verifiedDomain: SimplexDomain? = null): CC()
|
||||
class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData, val verifiedDomain: SimplexDomain? = null): CC()
|
||||
class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC()
|
||||
@@ -4092,6 +4104,10 @@ sealed class CC {
|
||||
val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else ""
|
||||
"/_connect plan $userId $connLink$resolveStr$sigStr"
|
||||
}
|
||||
is APISendServiceRequest -> {
|
||||
val timeoutStr = if (timeoutSec != null) " timeout=$timeoutSec" else ""
|
||||
"/_service_request $userId $target$timeoutStr ${json.encodeToString(request)}"
|
||||
}
|
||||
is APIPrepareContact -> "/_prepare contact $userId ${connLink.cmdString}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(contactShortLinkData)}"
|
||||
is APIPrepareGroup -> "/_prepare group $userId ${connLink.cmdString} direct=${onOff(directLink)}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(groupShortLinkData)}"
|
||||
is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId"
|
||||
@@ -4278,6 +4294,7 @@ sealed class CC {
|
||||
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
|
||||
is ApiChangeConnectionUser -> "apiChangeConnectionUser"
|
||||
is APIConnectPlan -> "apiConnectPlan"
|
||||
is APISendServiceRequest -> "apiSendServiceRequest"
|
||||
is APIPrepareContact -> "apiPrepareContact"
|
||||
is APIPrepareGroup -> "apiPrepareGroup"
|
||||
is APIChangePreparedContactUser -> "apiChangePreparedContactUser"
|
||||
@@ -6564,6 +6581,7 @@ sealed class 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 connLink: CreatedConnLink, val planSimplexName: SimplexNameInfo? = null, val otherSimplexName: SimplexNameInfo? = null, val connectionPlan: ConnectionPlan): CR()
|
||||
@Serializable @SerialName("serviceResponse") class CRServiceResponse(val user: UserRef, val responseData: JsonObject): CR()
|
||||
@Serializable @SerialName("newPreparedChat") class NewPreparedChat(val user: UserRef, val chat: Chat): CR()
|
||||
@Serializable @SerialName("contactUserChanged") class ContactUserChanged(val user: UserRef, val fromContact: Contact, val newUser: UserRef, val toContact: Contact): CR()
|
||||
@Serializable @SerialName("groupUserChanged") class GroupUserChanged(val user: UserRef, val fromGroup: GroupInfo, val newUser: UserRef, val toGroup: GroupInfo): CR()
|
||||
|
||||
Reference in New Issue
Block a user