mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-31 03:16:05 +00:00
android: receiving messages in background; ios: background task completion (#382)
* android: receiving messages in background; ios: background task completion * complete receiving and sending messages in background
This commit is contained in:
committed by
GitHub
parent
310f56a9b3
commit
0b00c2ad76
@@ -71,6 +71,7 @@ dependencies {
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
|
||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
|
||||
@@ -76,7 +76,7 @@ fun processIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
when (intent?.action) {
|
||||
NtfManager.OpenChatAction -> {
|
||||
val chatId = intent.getStringExtra("chatId")
|
||||
Log.d("SIMPLEX", "processIntent: OpenChatAction $chatId")
|
||||
Log.d(TAG, "processIntent: OpenChatAction $chatId")
|
||||
if (chatId != null) {
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
if (cInfo != null) withApi { openChat(chatModel, cInfo) }
|
||||
@@ -90,7 +90,7 @@ fun processIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
Log.d("SIMPLEX", "connectIfOpenedViaUri: opened via link")
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
// TODO open from chat list view
|
||||
chatModel.appOpenUrl.value = uri
|
||||
@@ -102,7 +102,7 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
confirmText = "Connect",
|
||||
onConfirm = {
|
||||
withApi {
|
||||
Log.d("SIMPLEX", "connectIfOpenedViaUri: connecting")
|
||||
Log.d(TAG, "connectIfOpenedViaUri: connecting")
|
||||
connectViaUri(chatModel, action, uri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.app.Application
|
||||
import android.net.LocalServerSocket
|
||||
import android.content.Context
|
||||
import android.net.*
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.*
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
@@ -13,6 +15,8 @@ import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
const val TAG = "SIMPLEX"
|
||||
|
||||
// ghc's rts
|
||||
external fun initHS()
|
||||
// android-support
|
||||
@@ -24,7 +28,7 @@ external fun chatInit(path: String): ChatCtrl
|
||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String
|
||||
external fun chatRecvMsg(ctrl: ChatCtrl) : String
|
||||
|
||||
class SimplexApp: Application() {
|
||||
class SimplexApp: Application(), LifecycleEventObserver {
|
||||
private lateinit var controller: ChatController
|
||||
lateinit var chatModel: ChatModel
|
||||
private lateinit var ntfManager: NtfManager
|
||||
@@ -43,6 +47,8 @@ class SimplexApp: Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
registerNetworkCallback()
|
||||
ntfManager = NtfManager(applicationContext)
|
||||
val ctrl = chatInit(applicationContext.filesDir.toString())
|
||||
controller = ChatController(ctrl, ntfManager, applicationContext)
|
||||
@@ -53,18 +59,43 @@ class SimplexApp: Application() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
Log.d(TAG, "onStateChanged: $event")
|
||||
}
|
||||
|
||||
private fun registerNetworkCallback() {
|
||||
val connectivityManager = getSystemService(ConnectivityManager::class.java)
|
||||
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Log.e(TAG, "The default network is now: " + network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.e(TAG, "The application no longer has a default network. The last default network was " + network)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
Log.e(TAG, "The default network changed capabilities: " + networkCapabilities)
|
||||
}
|
||||
|
||||
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
|
||||
Log.e(TAG, "The default network changed link properties: " + linkProperties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
val socketName = "local.socket.address.listen.native.cmd2"
|
||||
|
||||
val s = Semaphore(0)
|
||||
thread(name="stdout/stderr pipe") {
|
||||
Log.d("SIMPLEX", "starting server")
|
||||
Log.d(TAG, "starting server")
|
||||
val server = LocalServerSocket(socketName)
|
||||
Log.d("SIMPLEX", "started server")
|
||||
Log.d(TAG, "started server")
|
||||
s.release()
|
||||
val receiver = server.accept()
|
||||
Log.d("SIMPLEX", "started receiver")
|
||||
Log.d(TAG, "started receiver")
|
||||
val logbuffer = FifoQueue<String>(500)
|
||||
if (receiver != null) {
|
||||
val inStream = receiver.inputStream
|
||||
@@ -73,7 +104,7 @@ class SimplexApp: Application() {
|
||||
|
||||
while(true) {
|
||||
val line = input.readLine() ?: break
|
||||
Log.d("SIMPLEX (stdout/stderr)", line)
|
||||
Log.w("$TAG (stdout/stderr)", line)
|
||||
logbuffer.add(line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.model
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.chatRecvMsg
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -24,7 +25,7 @@ class BackgroundAPIWorker(appContext: Context, workerParams: WorkerParameters, c
|
||||
private fun getNewItems() {
|
||||
val json = chatRecvMsg(controller)
|
||||
val r = APIResponse.decodeStr(json).resp
|
||||
Log.d("SIMPLEX", "chatRecvMsg: ${r.responseType}")
|
||||
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
|
||||
}
|
||||
|
||||
private fun buildRequest(): OneTimeWorkRequest {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import chat.simplex.app.ui.theme.SecretColor
|
||||
import chat.simplex.app.ui.theme.SimplexBlue
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -650,7 +651,7 @@ enum class FormatColor(val color: String) {
|
||||
val uiColor: Color @Composable get() = when (this) {
|
||||
red -> Color.Red
|
||||
green -> Color.Green
|
||||
blue -> Color.Blue
|
||||
blue -> SimplexBlue
|
||||
yellow -> Color.Yellow
|
||||
cyan -> Color.Cyan
|
||||
magenta -> Color.Magenta
|
||||
|
||||
@@ -6,8 +6,7 @@ import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import chat.simplex.app.MainActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.*
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
class NtfManager(val context: Context) {
|
||||
@@ -30,7 +29,7 @@ class NtfManager(val context: Context) {
|
||||
}
|
||||
|
||||
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
Log.d("SIMPLEX", "notifyMessageReceived ${cInfo.id}")
|
||||
Log.d(TAG, "notifyMessageReceived ${cInfo.id}")
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
val recentNotification = (now - prevNtfTime.getOrDefault(cInfo.id, 0) < msgNtfTimeoutMs)
|
||||
prevNtfTime[cInfo.id] = now
|
||||
@@ -64,7 +63,7 @@ class NtfManager(val context: Context) {
|
||||
}
|
||||
|
||||
private fun getMsgPendingIntent(cInfo: ChatInfo) : PendingIntent{
|
||||
Log.d("SIMPLEX", "getMsgPendingIntent ${cInfo.id}")
|
||||
Log.d(TAG, "getMsgPendingIntent ${cInfo.id}")
|
||||
val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt()
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
|
||||
@@ -5,8 +5,7 @@ import android.app.ActivityManager.RunningAppProcessInfo
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import chat.simplex.app.chatRecvMsg
|
||||
import chat.simplex.app.chatSendCmd
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -24,7 +23,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
var chatModel = ChatModel(this)
|
||||
|
||||
suspend fun startChat(u: User) {
|
||||
Log.d("SIMPLEX (user)", u.toString())
|
||||
Log.d(TAG, "user: $u")
|
||||
try {
|
||||
apiStartChat()
|
||||
chatModel.userAddress.value = apiGetUserAddress()
|
||||
@@ -32,9 +31,9 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
chatModel.currentUser = mutableStateOf(u)
|
||||
chatModel.userCreated.value = true
|
||||
startReceiver()
|
||||
Log.d("SIMPLEX", "started chat")
|
||||
Log.d(TAG, "started chat")
|
||||
} catch(e: Error) {
|
||||
Log.d("SIMPLEX", "failed starting chat $e")
|
||||
Log.e(TAG, "failed starting chat $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -62,11 +61,11 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
val c = cmd.cmdString
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(cmd))
|
||||
val json = chatSendCmd(ctrl, c)
|
||||
Log.d("SIMPLEX", "sendCmd: ${cmd.cmdType}")
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
val r = APIResponse.decodeStr(json)
|
||||
Log.d("SIMPLEX", "sendCmd response type ${r.resp.responseType}")
|
||||
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
|
||||
if (r.resp is CR.Response || r.resp is CR.Invalid) {
|
||||
Log.d("SIMPLEX", "sendCmd response json $json")
|
||||
Log.d(TAG, "sendCmd response json $json")
|
||||
}
|
||||
chatModel.terminalItems.add(TerminalItem.resp(r.resp))
|
||||
r.resp
|
||||
@@ -77,8 +76,8 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
return withContext(Dispatchers.IO) {
|
||||
val json = chatRecvMsg(ctrl)
|
||||
val r = APIResponse.decodeStr(json).resp
|
||||
Log.d("SIMPLEX", "chatRecvMsg: ${r.responseType}")
|
||||
if (r is CR.Response || r is CR.Invalid) Log.d("SIMPLEX", "chatRecvMsg json: $json")
|
||||
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
|
||||
if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json")
|
||||
r
|
||||
}
|
||||
}
|
||||
@@ -91,7 +90,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
suspend fun apiGetActiveUser(): User? {
|
||||
val r = sendCmd(CC.ShowActiveUser())
|
||||
if (r is CR.ActiveUser) return r.user
|
||||
Log.d("SIMPLEX", "apiGetActiveUser: ${r.responseType} ${r.details}")
|
||||
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
|
||||
chatModel.userCreated.value = false
|
||||
return null
|
||||
}
|
||||
@@ -99,7 +98,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
suspend fun apiCreateActiveUser(p: Profile): User {
|
||||
val r = sendCmd(CC.CreateActiveUser(p))
|
||||
if (r is CR.ActiveUser) return r.user
|
||||
Log.d("SIMPLEX", "apiCreateActiveUser: ${r.responseType} ${r.details}")
|
||||
Log.d(TAG, "apiCreateActiveUser: ${r.responseType} ${r.details}")
|
||||
throw Error("user not created ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
@@ -118,21 +117,21 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
suspend fun apiGetChat(type: ChatType, id: Long): Chat? {
|
||||
val r = sendCmd(CC.ApiGetChat(type, id))
|
||||
if (r is CR.ApiChat ) return r.chat
|
||||
Log.d("SIMPLEX", "apiGetChat bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? {
|
||||
val r = sendCmd(CC.ApiSendMessage(type, id, mc))
|
||||
if (r is CR.NewChatItem ) return r.chatItem
|
||||
Log.d("SIMPLEX", "apiSendMessage bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiSendMessage bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiAddContact(): String? {
|
||||
val r = sendCmd(CC.AddContact())
|
||||
if (r is CR.Invitation) return r.connReqInvitation
|
||||
Log.d("SIMPLEX", "apiAddContact bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiAddContact bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -182,21 +181,21 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
val r = sendCmd(CC.UpdateProfile(profile))
|
||||
if (r is CR.UserProfileNoChange) return profile
|
||||
if (r is CR.UserProfileUpdated) return r.toProfile
|
||||
Log.d("SIMPLEX", "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateUserAddress(): String? {
|
||||
val r = sendCmd(CC.CreateMyAddress())
|
||||
if (r is CR.UserContactLinkCreated) return r.connReqContact
|
||||
Log.d("SIMPLEX", "apiCreateUserAddress bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiCreateUserAddress bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteUserAddress(): Boolean {
|
||||
val r = sendCmd(CC.DeleteMyAddress())
|
||||
if (r is CR.UserContactLinkDeleted) return true
|
||||
Log.d("SIMPLEX", "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -207,34 +206,34 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
|
||||
return null
|
||||
}
|
||||
Log.d("SIMPLEX", "apiGetUserAddress bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? {
|
||||
val r = sendCmd(CC.ApiAcceptContact(contactReqId))
|
||||
if (r is CR.AcceptingContactRequest) return r.contact
|
||||
Log.d("SIMPLEX", "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiRejectContactRequest(contactReqId: Long): Boolean {
|
||||
val r = sendCmd(CC.ApiRejectContact(contactReqId))
|
||||
if (r is CR.ContactRequestRejected) return true
|
||||
Log.d("SIMPLEX", "apiRejectContactRequest bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean {
|
||||
val r = sendCmd(CC.ApiChatRead(type, id, range))
|
||||
if (r is CR.CmdOk) return true
|
||||
Log.d("SIMPLEX", "apiChatRead bad response: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
fun apiErrorAlert(method: String, title: String, r: CR) {
|
||||
val errMsg = "${r.responseType}: ${r.details}"
|
||||
Log.e("SIMPLEX", "$method bad response: $errMsg")
|
||||
Log.e(TAG, "$method bad response: $errMsg")
|
||||
AlertManager.shared.showAlertMsg(title, errMsg)
|
||||
}
|
||||
|
||||
@@ -286,7 +285,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
// }
|
||||
else ->
|
||||
Log.d("SIMPLEX" , "unsupported event: ${r.responseType}")
|
||||
Log.d(TAG , "unsupported event: ${r.responseType}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.item.ChatItemView
|
||||
@@ -39,7 +40,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
BackHandler { chatModel.chatId.value = null }
|
||||
// TODO a more advanced version would mark as read only if in view
|
||||
LaunchedEffect(chat.chatItems) {
|
||||
Log.d("SIMPLEX", "ChatView ${chatModel.chatId.value}: LaunchedEffect")
|
||||
Log.d(TAG, "ChatView ${chatModel.chatId.value}: LaunchedEffect")
|
||||
delay(1000L)
|
||||
if (chat.chatItems.count() > 0) {
|
||||
chatModel.markChatItemsRead(chat.chatInfo)
|
||||
@@ -79,18 +80,16 @@ fun ChatLayout(
|
||||
info: () -> Unit,
|
||||
sendMessage: (String) -> Unit
|
||||
) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chat, back, info) },
|
||||
bottomBar = { SendMsgView(sendMessage) },
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ChatItemsList(chatItems)
|
||||
Surface(Modifier.fillMaxWidth().background(MaterialTheme.colors.background)) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chat, back, info) },
|
||||
bottomBar = { SendMsgView(sendMessage) },
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Box(Modifier.padding(contentPadding)) {
|
||||
ChatItemsList(chatItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ import android.util.Log
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import chat.simplex.app.TAG
|
||||
|
||||
class AlertManager {
|
||||
var alertView = mutableStateOf<(@Composable () -> Unit)?>(null)
|
||||
var presentAlert = mutableStateOf<Boolean>(false)
|
||||
|
||||
fun showAlert(alert: @Composable () -> Unit) {
|
||||
Log.d("SIMPLEX", "AlertManager.showAlert")
|
||||
Log.d(TAG, "AlertManager.showAlert")
|
||||
alertView.value = alert
|
||||
presentAlert.value = true
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.views.helpers.CloseSheetBar
|
||||
|
||||
@Composable
|
||||
@@ -36,7 +37,7 @@ class ModalManager {
|
||||
}
|
||||
|
||||
fun showCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
Log.d("SIMPLEX", "ModalManager.showModal")
|
||||
Log.d(TAG, "ModalManager.showModal")
|
||||
modalViews.add(modal)
|
||||
modalCount.value = modalViews.count()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.simplex.app.TAG
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
@@ -62,7 +63,7 @@ fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
||||
} catch (e: Exception) {
|
||||
Log.d("SIMPLEX", "CameraPreview: ${e.localizedMessage}")
|
||||
Log.d(TAG, "CameraPreview: ${e.localizedMessage}")
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
@@ -90,11 +91,11 @@ class BarCodeAnalyser(
|
||||
if (barcodes.isNotEmpty()) {
|
||||
onBarcodeDetected(barcodes)
|
||||
} else {
|
||||
Log.d("SIMPLEX", "BarcodeAnalyser: No barcode Scanned")
|
||||
Log.d(TAG, "BarcodeAnalyser: No barcode Scanned")
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { exception ->
|
||||
Log.d("SIMPLEX", "BarcodeAnalyser: Something went wrong $exception")
|
||||
Log.e(TAG, "BarcodeAnalyser: Something went wrong $exception")
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
image.close()
|
||||
|
||||
@@ -25,14 +25,14 @@ fun MarkdownHelpView() {
|
||||
"You can use markdown to format messages:",
|
||||
Modifier.padding(vertical = 16.dp)
|
||||
)
|
||||
MdFormat("*bold*", "bold text", Format.Bold())
|
||||
MdFormat("_italic_", "italic text", Format.Italic())
|
||||
MdFormat("~strike~", "strikethrough text", Format.StrikeThrough())
|
||||
MdFormat("`code`", "a = b + c", Format.Snippet())
|
||||
MdFormat("*bold*", "bold", Format.Bold())
|
||||
MdFormat("_italic_", "italic", Format.Italic())
|
||||
MdFormat("~strike~", "strike", Format.StrikeThrough())
|
||||
MdFormat("`a + b`", "a + b", Format.Snippet())
|
||||
Row {
|
||||
MdSyntax("!1 colored!")
|
||||
Text(buildAnnotatedString {
|
||||
withStyle(Format.Colored(FormatColor.red).style) { append("red text") }
|
||||
withStyle(Format.Colored(FormatColor.red).style) { append("colored") }
|
||||
append(" (")
|
||||
appendColor(this, "1", FormatColor.red, ", ")
|
||||
appendColor(this, "2", FormatColor.green, ", ")
|
||||
@@ -46,7 +46,7 @@ fun MarkdownHelpView() {
|
||||
MdSyntax("#secret#")
|
||||
SelectionContainer {
|
||||
Text(buildAnnotatedString {
|
||||
withStyle(Format.Secret().style) { append("secret text") }
|
||||
withStyle(Format.Secret().style) { append("secret") }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,7 @@ fun SettingsView(chatModel: ChatModel) {
|
||||
if (user != null) {
|
||||
SettingsLayout(
|
||||
profile = user.profile,
|
||||
// showModal = { modal -> ModalManager.shared.showModal { modal(chatModel) } },
|
||||
showProfile = { ModalManager.shared.showModal { UserProfileView(chatModel) } },
|
||||
showAddress = { ModalManager.shared.showModal { UserAddressView(chatModel) } },
|
||||
showHelp = { ModalManager.shared.showModal { HelpView(chatModel) } },
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } }
|
||||
)
|
||||
}
|
||||
@@ -45,10 +42,7 @@ val simplexTeamUri =
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
profile: Profile,
|
||||
// showModal: (modal: @Composable (ChatModel) -> Unit) -> Unit,
|
||||
showProfile: () -> Unit,
|
||||
showAddress: () -> Unit,
|
||||
showHelp: () -> Unit,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showTerminal: () -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -67,10 +61,11 @@ fun SettingsLayout(
|
||||
Text(
|
||||
"Your Settings",
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
Spacer(Modifier.height(30.dp))
|
||||
|
||||
SettingsSectionView(showProfile, 60.dp) {
|
||||
SettingsSectionView(showModal { UserProfileView(it) }, 60.dp) {
|
||||
Icon(
|
||||
Icons.Outlined.AccountCircle,
|
||||
contentDescription = "Avatar Placeholder",
|
||||
@@ -86,7 +81,7 @@ fun SettingsLayout(
|
||||
}
|
||||
}
|
||||
Divider(Modifier.padding(horizontal = 8.dp))
|
||||
SettingsSectionView(showAddress) {
|
||||
SettingsSectionView(showModal { UserAddressView(it) }) {
|
||||
Icon(
|
||||
Icons.Outlined.QrCode,
|
||||
contentDescription = "Address",
|
||||
@@ -96,7 +91,7 @@ fun SettingsLayout(
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
SettingsSectionView(showHelp) {
|
||||
SettingsSectionView(showModal { HelpView(it) }) {
|
||||
Icon(
|
||||
Icons.Outlined.HelpOutline,
|
||||
contentDescription = "Chat help",
|
||||
@@ -104,7 +99,7 @@ fun SettingsLayout(
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text("How to use SimpleX Chat")
|
||||
}
|
||||
SettingsSectionView({ ModalManager.shared.showModal { MarkdownHelpView() } }) {
|
||||
SettingsSectionView(showModal { MarkdownHelpView() }) {
|
||||
Icon(
|
||||
Icons.Outlined.TextFormat,
|
||||
contentDescription = "Markdown help",
|
||||
@@ -167,12 +162,12 @@ fun SettingsLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsSectionView(func: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) {
|
||||
fun SettingsSectionView(click: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = func)
|
||||
.clickable(onClick = click)
|
||||
.height(height),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -191,9 +186,7 @@ fun PreviewSettingsLayout() {
|
||||
SimpleXTheme {
|
||||
SettingsLayout(
|
||||
profile = Profile.sampleData,
|
||||
showProfile = {},
|
||||
showAddress = {},
|
||||
showHelp = {},
|
||||
showModal = {{}},
|
||||
showTerminal = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ final class ChatModel: ObservableObject {
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@Published var userAddress: String?
|
||||
@Published var appOpenUrl: URL?
|
||||
|
||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||
|
||||
static let shared = ChatModel()
|
||||
|
||||
func hasChat(_ id: String) -> Bool {
|
||||
|
||||
@@ -178,6 +178,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
if let s = body { content.body = s }
|
||||
content.targetContentIdentifier = targetContentIdentifier
|
||||
content.userInfo = userInfo
|
||||
// TODO move logic of adding sound here, so it applies to background notifications too
|
||||
content.sound = .default
|
||||
// content.interruptionLevel = .active
|
||||
// content.relevanceScore = 0.5 // 0-1
|
||||
|
||||
@@ -241,10 +241,53 @@ enum TerminalItem: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
func chatSendCmdSync(_ cmd: ChatCommand) -> ChatResponse {
|
||||
private func _sendCmd(_ cmd: ChatCommand) -> ChatResponse {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
|
||||
}
|
||||
|
||||
private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
|
||||
var id: UIBackgroundTaskIdentifier!
|
||||
var running = true
|
||||
let endTask = {
|
||||
// logger.debug("beginBGTask: endTask \(id.rawValue)")
|
||||
if running {
|
||||
running = false
|
||||
if let h = handler {
|
||||
// logger.debug("beginBGTask: user handler")
|
||||
h()
|
||||
}
|
||||
if id != .invalid {
|
||||
UIApplication.shared.endBackgroundTask(id)
|
||||
id = .invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
id = UIApplication.shared.beginBackgroundTask(expirationHandler: endTask)
|
||||
// logger.debug("beginBGTask: \(id.rawValue)")
|
||||
return endTask
|
||||
}
|
||||
|
||||
let msgDelay: Double = 7.5
|
||||
let maxTaskDuration: Double = 15
|
||||
|
||||
private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse) -> ChatResponse {
|
||||
let endTask = beginBGTask()
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
|
||||
let r = f()
|
||||
if let d = bgDelay {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + d, execute: endTask)
|
||||
} else {
|
||||
endTask()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||
let resp = chatResponse(chat_send_cmd(getChatCtrl(), &c))
|
||||
let resp = bgTask
|
||||
? withBGTask(bgDelay: bgDelay) { _sendCmd(cmd) }
|
||||
: _sendCmd(cmd)
|
||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
@@ -256,16 +299,19 @@ func chatSendCmdSync(_ cmd: ChatCommand) -> ChatResponse {
|
||||
return resp
|
||||
}
|
||||
|
||||
func chatSendCmd(_ cmd: ChatCommand) async -> ChatResponse {
|
||||
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse {
|
||||
await withCheckedContinuation { cont in
|
||||
cont.resume(returning: chatSendCmdSync(cmd))
|
||||
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay))
|
||||
}
|
||||
}
|
||||
|
||||
func chatRecvMsg() async -> ChatResponse {
|
||||
await withCheckedContinuation { cont in
|
||||
let resp = chatResponse(chat_recv_msg(getChatCtrl())!)
|
||||
cont.resume(returning: resp)
|
||||
_ = withBGTask(bgDelay: msgDelay) {
|
||||
let resp = chatResponse(chat_recv_msg(getChatCtrl())!)
|
||||
cont.resume(returning: resp)
|
||||
return resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,13 +350,30 @@ func apiGetChat(type: ChatType, id: Int64) async throws -> Chat {
|
||||
}
|
||||
|
||||
func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiSendMessage(type: type, id: id, msg: msg))
|
||||
if case let .newChatItem(aChatItem) = r { return aChatItem.chatItem }
|
||||
let chatModel = ChatModel.shared
|
||||
let cmd = ChatCommand.apiSendMessage(type: type, id: id, msg: msg)
|
||||
let r: ChatResponse
|
||||
if type == .direct {
|
||||
var cItem: ChatItem!
|
||||
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
|
||||
r = await chatSendCmd(cmd, bgTask: false)
|
||||
if case let .newChatItem(aChatItem) = r {
|
||||
cItem = aChatItem.chatItem
|
||||
chatModel.messageDelivery[cItem.id] = endTask
|
||||
return cItem
|
||||
}
|
||||
endTask()
|
||||
} else {
|
||||
r = await chatSendCmd(cmd, bgDelay: msgDelay)
|
||||
if case let .newChatItem(aChatItem) = r {
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
}
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiAddContact() throws -> String {
|
||||
let r = chatSendCmdSync(.addContact)
|
||||
let r = chatSendCmdSync(.addContact, bgTask: false)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
throw r
|
||||
}
|
||||
@@ -325,7 +388,7 @@ func apiConnect(connReq: String) async throws {
|
||||
}
|
||||
|
||||
func apiDeleteChat(type: ChatType, id: Int64) async throws {
|
||||
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id))
|
||||
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false)
|
||||
if case .contactDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -450,6 +513,8 @@ class ChatReceiver {
|
||||
self._lastMsgTime = .now
|
||||
processReceivedMsg(msg)
|
||||
if self.receiveMessages {
|
||||
do { try await Task.sleep(nanoseconds: 7_500_000) }
|
||||
catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") }
|
||||
await receiveMsgLoop()
|
||||
}
|
||||
}
|
||||
@@ -508,6 +573,13 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||
let cItem = aChatItem.chatItem
|
||||
if chatModel.upsertChatItem(cInfo, cItem) {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
} else if let endTask = chatModel.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndSent: endTask()
|
||||
case .sndErrorAuth: endTask()
|
||||
case .sndError: endTask()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
|
||||
@@ -13,13 +13,13 @@ struct MarkdownHelp: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("You can use markdown to format messages:")
|
||||
.padding(.bottom)
|
||||
mdFormat("*bold*", Text("bold text").bold())
|
||||
mdFormat("_italic_", Text("italic text").italic())
|
||||
mdFormat("~strike~", Text("strikethrough text").strikethrough())
|
||||
mdFormat("`code`", Text("`a = b + c`").font(.body.monospaced()))
|
||||
mdFormat("!1 colored!", Text("red text").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")"))
|
||||
mdFormat("*bold*", Text("bold").bold())
|
||||
mdFormat("_italic_", Text("italic").italic())
|
||||
mdFormat("~strike~", Text("strike").strikethrough())
|
||||
mdFormat("`a + b`", Text("`a + b`").font(.body.monospaced()))
|
||||
mdFormat("!1 colored!", Text("colored").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")"))
|
||||
(
|
||||
mdFormat("#secret#", Text("secret text")
|
||||
mdFormat("#secret#", Text("secret")
|
||||
.foregroundColor(.clear)
|
||||
.underline(color: .primary) + Text(" (can be copied)"))
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user