Merge branch 'master' into master-ghc8107

This commit is contained in:
Evgeny Poberezkin
2024-01-10 12:06:48 +00:00
33 changed files with 296 additions and 90 deletions
@@ -506,6 +506,10 @@ object ChatController {
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.UserExists
) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc))
} else if (
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.InvalidDisplayName
) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_invalid_title), generalGetString(MR.strings.failed_to_create_user_invalid_desc))
} else {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_title), r.details)
}
@@ -1124,6 +1128,13 @@ object ChatController {
return false
}
suspend fun apiGetCallInvitations(rh: Long?): List<RcvCallInvitation> {
val r = sendCmd(rh, CC.ApiGetCallInvitations())
if (r is CR.CallInvitations) return r.callInvitations
Log.e(TAG, "apiGetCallInvitations bad response: ${r.responseType} ${r.details}")
return emptyList()
}
suspend fun apiSendCallInvitation(rh: Long?, contact: Contact, callType: CallType): Boolean {
val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType))
return r is CR.CmdOk
@@ -1880,9 +1891,34 @@ object ChatController {
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
chatModel.remoteHostPairing.value = null
if (disconnectedHost != null) {
showToast(
generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() })
)
val deviceName = disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() }
when (r.rhStopReason) {
is RemoteHostStopReason.ConnectionFailed -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_host_was_disconnected_title),
if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) {
r.rhStopReason.chatError.remoteHostError.localizedString(deviceName)
} else {
generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string)
}
)
}
is RemoteHostStopReason.Crashed -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_host_was_disconnected_title),
if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) {
r.rhStopReason.chatError.remoteHostError.localizedString(deviceName)
} else {
generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string)
}
)
}
is RemoteHostStopReason.Disconnected -> {
if (r.rhsState is RemoteHostSessionState.Connected || r.rhsState is RemoteHostSessionState.Confirmed) {
showToast(generalGetString(MR.strings.remote_host_was_disconnected_toast).format(deviceName))
}
}
}
}
if (chatModel.remoteHostId() == r.remoteHostId_) {
chatModel.currentRemoteHost.value = null
@@ -1913,6 +1949,27 @@ object ChatController {
val sess = chatModel.remoteCtrlSession.value
if (sess != null) {
chatModel.remoteCtrlSession.value = null
fun showAlert(chatError: ChatError) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
if (chatError is ChatError.ChatErrorRemoteCtrl) {
chatError.remoteCtrlError.localizedString
} else {
generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string)
}
)
}
when (r.rcStopReason) {
is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError)
is RemoteCtrlStopReason.ConnectionFailed -> showAlert(r.rcStopReason.chatError)
is RemoteCtrlStopReason.SetupFailed -> showAlert(r.rcStopReason.chatError)
is RemoteCtrlStopReason.Disconnected -> {
/*AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
)*/
}
}
if (sess.sessionState is UIRemoteCtrlSessionState.Connected) {
switchToLocalSession()
}
@@ -2246,6 +2303,7 @@ sealed class CC {
class ApiShowMyAddress(val userId: Long): CC()
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC()
class ApiGetCallInvitations: CC()
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
class ApiRejectCall(val contact: Contact): CC()
class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC()
@@ -2382,6 +2440,7 @@ sealed class CC {
is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}"
is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId"
is ApiRejectContact -> "/_reject $contactReqId"
is ApiGetCallInvitations -> "/_call get"
is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}"
is ApiRejectCall -> "/_call reject @${contact.apiId}"
is ApiSendCallOffer -> "/_call offer @${contact.apiId} ${json.encodeToString(callOffer)}"
@@ -2505,6 +2564,7 @@ sealed class CC {
is ApiAddressAutoAccept -> "apiAddressAutoAccept"
is ApiAcceptContact -> "apiAcceptContact"
is ApiRejectContact -> "apiRejectContact"
is ApiGetCallInvitations -> "apiGetCallInvitations"
is ApiSendCallInvitation -> "apiSendCallInvitation"
is ApiRejectCall -> "apiRejectCall"
is ApiSendCallOffer -> "apiSendCallOffer"
@@ -3880,6 +3940,7 @@ sealed class CR {
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR()
// call events
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
@Serializable @SerialName("callOffer") class CallOffer(val user: UserRef, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
@@ -4027,6 +4088,7 @@ sealed class CR {
is SndFileProgressXFTP -> "sndFileProgressXFTP"
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
is SndFileError -> "sndFileError"
is CallInvitations -> "callInvitations"
is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer"
is CallAnswer -> "callAnswer"
@@ -4173,6 +4235,7 @@ sealed class CR {
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
is SndFileError -> withUser(user, json.encodeToString(chatItem))
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
@@ -4447,6 +4510,7 @@ sealed class ChatErrorType {
is EmptyUserPassword -> "emptyUserPassword"
is UserAlreadyHidden -> "userAlreadyHidden"
is UserNotHidden -> "userNotHidden"
is InvalidDisplayName -> "invalidDisplayName"
is ChatNotStarted -> "chatNotStarted"
is ChatNotStopped -> "chatNotStopped"
is ChatStoreChanged -> "chatStoreChanged"
@@ -4524,6 +4588,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType()
@Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(val userId: Long): ChatErrorType()
@Serializable @SerialName("userNotHidden") class UserNotHidden(val userId: Long): ChatErrorType()
@Serializable @SerialName("invalidDisplayName") object InvalidDisplayName: ChatErrorType()
@Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType()
@Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType()
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
@@ -4973,6 +5038,15 @@ sealed class RemoteHostError {
is BadVersion -> "badVersion"
is Disconnected -> "disconnected"
}
fun localizedString(name: String): String = when (this) {
is Missing -> generalGetString(MR.strings.remote_host_error_missing)
is Inactive -> generalGetString(MR.strings.remote_host_error_inactive)
is Busy -> generalGetString(MR.strings.remote_host_error_busy)
is Timeout -> generalGetString(MR.strings.remote_host_error_timeout)
is BadState -> generalGetString(MR.strings.remote_host_error_bad_state)
is BadVersion -> generalGetString(MR.strings.remote_host_error_bad_version)
is Disconnected -> generalGetString(MR.strings.remote_host_error_disconnected)
}.format(name)
@Serializable @SerialName("missing") object Missing: RemoteHostError()
@Serializable @SerialName("inactive") object Inactive: RemoteHostError()
@Serializable @SerialName("busy") object Busy: RemoteHostError()
@@ -4993,6 +5067,16 @@ sealed class RemoteCtrlError {
is BadInvitation -> "badInvitation"
is BadVersion -> "badVersion"
}
val localizedString: String get() = when (this) {
is Inactive -> generalGetString(MR.strings.remote_ctrl_error_inactive)
is BadState -> generalGetString(MR.strings.remote_ctrl_error_bad_state)
is Busy -> generalGetString(MR.strings.remote_ctrl_error_busy)
is Timeout -> generalGetString(MR.strings.remote_ctrl_error_timeout)
is Disconnected -> generalGetString(MR.strings.remote_ctrl_error_disconnected)
is BadInvitation -> generalGetString(MR.strings.remote_ctrl_error_bad_invitation)
is BadVersion -> generalGetString(MR.strings.remote_ctrl_error_bad_version)
}
@Serializable @SerialName("inactive") object Inactive: RemoteCtrlError()
@Serializable @SerialName("badState") object BadState: RemoteCtrlError()
@Serializable @SerialName("busy") object Busy: RemoteCtrlError()
@@ -93,7 +93,7 @@ abstract class NtfManager {
}
}
abstract fun notifyCallInvitation(invitation: RcvCallInvitation)
abstract fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean
abstract fun hasNotificationsForChat(chatId: String): Boolean
abstract fun cancelNotificationsForChat(chatId: String)
abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> Unit>> = emptyList())
@@ -13,8 +13,8 @@ class CallManager(val chatModel: ChatModel) {
callInvitations[invitation.contact.id] = invitation
if (invitation.user.showNotifications) {
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
invitation.sentNotification = ntfManager.notifyCallInvitation(invitation)
activeCallInvitation.value = invitation
ntfManager.notifyCallInvitation(invitation)
} else {
val contact = invitation.contact
ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
@@ -15,11 +15,10 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.common.views.usersettings.ProfilePreview
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.platform.SoundPlayer
import chat.simplex.res.MR
import kotlinx.datetime.Clock
@@ -27,7 +26,11 @@ import kotlinx.datetime.Clock
fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
val scope = rememberCoroutineScope()
LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) }
LaunchedEffect(Unit) {
if (chatModel.activeCallInvitation.value?.sentNotification == false || appPlatform.isDesktop) {
SoundPlayer.start(scope, sound = !chatModel.showCallView.value)
}
}
DisposableEffect(true) { onDispose { SoundPlayer.stop() } }
IncomingCallAlertLayout(
invitation,
@@ -112,6 +112,9 @@ sealed class WCallResponse {
CallMediaType.Video -> MR.strings.incoming_video_call
CallMediaType.Audio -> MR.strings.incoming_audio_call
})
// Shows whether notification was shown or not to prevent playing sound twice in both notification and in-app
var sentNotification: Boolean = false
}
@Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
@@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.group.*
@@ -317,11 +318,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
acceptCall = { contact ->
hideKeyboard(view)
val invitation = chatModel.callInvitations.remove(contact.id)
if (invitation == null) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
} else {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
withApi {
val invitation = chatModel.callInvitations.remove(contact.id)
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
if (invitation == null) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
} else {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
}
},
acceptFeature = { contact, feature, param ->
@@ -90,6 +90,8 @@
<string name="failed_to_create_user_title">Error creating profile!</string>
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
<string name="failed_to_create_user_invalid_title">Invalid display name!</string>
<string name="failed_to_create_user_invalid_desc">This display name is invalid. Please choose another name.</string>
<string name="failed_to_active_user_title">Error switching profile!</string>
<!-- API Error Responses - SimpleXAPI.kt -->
@@ -1694,6 +1696,10 @@
<string name="disconnect_remote_host">Disconnect</string>
<string name="disconnect_remote_hosts">Disconnect mobiles</string>
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
<string name="remote_host_was_disconnected_title">Connection stopped</string>
<string name="remote_ctrl_was_disconnected_title">Connection stopped</string>
<string name="remote_host_disconnected_from"><![CDATA[Disconnected from mobile <b>%s</b> with the reason: %s]]></string>
<string name="remote_ctrl_disconnected_with_reason">Disconnected with the reason: %s</string>
<string name="disconnect_desktop_question">Disconnect desktop?</string>
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
@@ -1728,6 +1734,20 @@
<string name="random_port">Random</string>
<string name="open_port_in_firewall_title">Open port in firewall</string>
<string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string>
<string name="remote_host_error_missing"><![CDATA[Mobile <b>%s</b> is missing]]></string>
<string name="remote_host_error_inactive"><![CDATA[Mobile <b>%s</b> is inactive]]></string>
<string name="remote_host_error_busy"><![CDATA[Mobile <b>%s</b> is busy]]></string>
<string name="remote_host_error_timeout"><![CDATA[Timeout reached while connecting to the mobile <b>%s</b>]]></string>
<string name="remote_host_error_bad_state"><![CDATA[Connection to the mobile <b>%s</b> is in a bad state]]></string>
<string name="remote_host_error_bad_version"><![CDATA[Mobile <b>%s</b> has an unsupported version. Please, make sure you use the same version on both devices]]></string>
<string name="remote_host_error_disconnected"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
<string name="remote_ctrl_error_inactive">Desktop is inactive</string>
<string name="remote_ctrl_error_bad_state">Connection to the desktop is in a bad state</string>
<string name="remote_ctrl_error_busy">Desktop is busy</string>
<string name="remote_ctrl_error_timeout">Timeout reached while connecting to the desktop</string>
<string name="remote_ctrl_error_disconnected">Desktop was disconnected</string>
<string name="remote_ctrl_error_bad_invitation">Desktop has wrong invitation code</string>
<string name="remote_ctrl_error_bad_version">Desktop has an unsupported version. Please, make sure you use the same version on both devices</string>
<!-- Under development -->
<string name="in_developing_title">Coming soon!</string>