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:
Evgeny Poberezkin
2022-02-28 10:44:48 +00:00
committed by GitHub
parent 310f56a9b3
commit 0b00c2ad76
17 changed files with 199 additions and 96 deletions

View File

@@ -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'

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -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}")
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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") }
})
}
}

View File

@@ -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 = {}
)
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)")

View File

@@ -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)"))
)