mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-05 23:07:57 +00:00
Merge branch 'master' into nd/android-attach-sheet-keyboard
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
// Android blurs the bars with a RenderEffect on the layer itself, so this is never called there.
|
||||
actual fun DrawScope.drawBarsBlurred(radiusPx: Float, barWidth: Float, barHeight: Float, drawBar: DrawScope.() -> Unit) = drawBar()
|
||||
+3
-3
@@ -132,7 +132,7 @@ actual fun ActiveCallView() {
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
WebRTCView(chatModel.callCommand) { apiMsg ->
|
||||
Log.d(TAG, "received from WebRTCView: $apiMsg")
|
||||
Log.d(TAG, "received from WebRTCView: ${apiMsg.resp.javaClass.simpleName}")
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
val callState = call.androidCallState as ActiveCallState
|
||||
@@ -711,7 +711,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
|
||||
.collect {
|
||||
while (callCommand.isNotEmpty()) {
|
||||
val cmd = callCommand.removeFirstOrNull()
|
||||
Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd")
|
||||
Log.d(TAG, "WebRTCView LaunchedEffect executing ${cmd?.javaClass?.simpleName}")
|
||||
if (cmd != null) {
|
||||
processCommand(wv, cmd)
|
||||
}
|
||||
@@ -785,7 +785,7 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) {
|
||||
// onResponse(message)
|
||||
onResponse(json.decodeFromString(message))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "failed parsing WebView message: $message")
|
||||
Log.e(TAG, "failed parsing WebView message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ object AppLock {
|
||||
fun elapsedRealtime(): Long = System.nanoTime() / 1_000_000
|
||||
|
||||
fun recheckAuthState() {
|
||||
if (ChatModel.showCallView.value) return
|
||||
val enteredBackgroundVal = enteredBackground.value
|
||||
val delay = ChatController.appPrefs.laLockDelay.get()
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
|
||||
|
||||
+4
-1
@@ -1931,7 +1931,7 @@ data class Contact(
|
||||
}
|
||||
|
||||
val isContactCard: Boolean get() =
|
||||
(activeConn == null || activeConn.connStatus == ConnStatus.Prepared) && profile.contactLink != null && active && preparedContact == null && contactRequestId == null
|
||||
(activeConn == null || activeConn.connStatus == ConnStatus.Prepared) && profile.contactLink != null && active && preparedContact == null && contactRequestId == null && groupDirectInv == null
|
||||
|
||||
val isBot: Boolean get() = profile.peerType == ChatPeerType.Bot
|
||||
|
||||
@@ -3555,6 +3555,7 @@ data class ChatItem (
|
||||
is RcvGroupEvent.MemberCreatedContact -> false
|
||||
is RcvGroupEvent.MemberProfileUpdated -> false
|
||||
is RcvGroupEvent.NewMemberPendingReview -> true
|
||||
is RcvGroupEvent.MsgBadSignature -> false
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> false
|
||||
is CIContent.RcvConnEventContent -> false
|
||||
@@ -5461,6 +5462,7 @@ sealed class RcvGroupEvent() {
|
||||
@Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent()
|
||||
@Serializable @SerialName("newMemberPendingReview") class NewMemberPendingReview(): RcvGroupEvent()
|
||||
@Serializable @SerialName("msgBadSignature") class MsgBadSignature(): RcvGroupEvent()
|
||||
|
||||
val text: String get() = text(isChannel = false)
|
||||
|
||||
@@ -5485,6 +5487,7 @@ sealed class RcvGroupEvent() {
|
||||
is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact)
|
||||
is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile)
|
||||
is NewMemberPendingReview -> generalGetString(MR.strings.rcv_group_event_new_member_pending_review)
|
||||
is MsgBadSignature -> generalGetString(MR.strings.rcv_group_event_msg_bad_signature)
|
||||
}
|
||||
|
||||
private fun profileUpdatedText(from: Profile, to: Profile): String =
|
||||
|
||||
+6
-7
@@ -941,7 +941,7 @@ object ChatController {
|
||||
val r = json.decodeFromString<API>(rStr)
|
||||
if (log) {
|
||||
Log.d(TAG, "sendCmd response type ${r.responseType}")
|
||||
if (r is API.Result && (r.res is CR.Response || r.res is CR.Invalid)) {
|
||||
if (r is API.Result && ((r.res is CR.Response && !r.res.type.startsWith("call")) || r.res is CR.Invalid)) {
|
||||
Log.d(TAG, "sendCmd response json $rStr")
|
||||
}
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||
@@ -958,7 +958,7 @@ object ChatController {
|
||||
} else {
|
||||
val r = json.decodeFromString<API>(rStr)
|
||||
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
|
||||
if (r is API.Result && (r.res is CR.Response || r.res is CR.Invalid)) Log.d(TAG, "chatRecvMsg json: $rStr")
|
||||
if (r is API.Result && ((r.res is CR.Response && !r.res.type.startsWith("call")) || r.res is CR.Invalid)) Log.d(TAG, "chatRecvMsg json: $rStr")
|
||||
r
|
||||
}
|
||||
}
|
||||
@@ -3367,10 +3367,9 @@ object ChatController {
|
||||
// TODO askConfirmation?
|
||||
// TODO check encryption is compatible
|
||||
withCall(r, r.contact) { call ->
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, sharedKey = r.sharedKey)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, hasSharedKey = r.sharedKey != null)
|
||||
val useRelay = appPrefs.webrtcPolicyRelay.get()
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, ".callOffer iceServers $iceServers")
|
||||
chatModel.callCommand.add(WCallCommand.Offer(
|
||||
offer = r.offer.rtcSession,
|
||||
iceCandidates = r.offer.rtcIceCandidates,
|
||||
@@ -7215,9 +7214,9 @@ sealed class CR {
|
||||
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
|
||||
is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
|
||||
is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${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 CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations.map { it.copy(sharedKey = null) })}"
|
||||
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: ${callInvitation.callType}"
|
||||
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
|
||||
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
|
||||
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
|
||||
is CallEnded -> withUser(user, "contact: ${contact.id}")
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
// Draws the bars' background blurred. The drawing to blur is given in bar coordinates, whatever size the blur runs at.
|
||||
expect fun DrawScope.drawBarsBlurred(radiusPx: Float, barWidth: Float, barHeight: Float, drawBar: DrawScope.() -> Unit)
|
||||
+35
-10
@@ -1,18 +1,18 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.views.helpers.KeyChangeEffect
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
expect fun Modifier.desktopOnExternalDrag(
|
||||
@@ -58,20 +58,23 @@ fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState<B
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun blurHidesMedia(enabled: Boolean, blurred: State<Boolean>): Boolean =
|
||||
enabled && blurred.value && remember { appPrefs.privacyMediaBlurRadius.state }.value > 0
|
||||
|
||||
@Composable
|
||||
fun Modifier.privacyBlur(
|
||||
enabled: Boolean,
|
||||
preview: ImageBitmap,
|
||||
blurred: MutableState<Boolean> = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) },
|
||||
scrollState: State<Boolean>,
|
||||
onLongClick: () -> Unit = {}
|
||||
): Modifier {
|
||||
val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state }
|
||||
return if (enabled && blurred.value) {
|
||||
this then Modifier.blur(
|
||||
radiusX = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
|
||||
radiusY = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
|
||||
edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp))
|
||||
)
|
||||
return if (blurHidesMedia(enabled, blurred)) {
|
||||
val blurredPreview = remember(preview, blurRadius.value) { preview.blurredBy(blurRadius.value) }
|
||||
this then Modifier
|
||||
.drawWithContent { drawImage(blurredPreview, dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt())) }
|
||||
.combinedClickable(
|
||||
onLongClick = onLongClick,
|
||||
onClick = {
|
||||
@@ -90,3 +93,25 @@ fun Modifier.privacyBlur(
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
// Calibrated so the resample blurs as much as Modifier.blur did at each radius; 360 read 5-75% stronger.
|
||||
private const val BLURRED_MEDIA_WIDTH_DP = 400
|
||||
// Bounds the first step: nothing bounds a decoded video frame, and reading every pixel of a 4K one would stall.
|
||||
private const val RESAMPLE_MEDIA_FROM_SIDE = 512
|
||||
|
||||
private fun ImageBitmap.blurredBy(radius: Int): ImageBitmap {
|
||||
if (width <= 0 || height <= 0) return this
|
||||
val w = (BLURRED_MEDIA_WIDTH_DP / radius).coerceIn(1, width)
|
||||
val h = (w * height / width).coerceIn(1, BLURRED_MEDIA_WIDTH_DP)
|
||||
val longest = maxOf(width, height)
|
||||
var image = if (longest > RESAMPLE_MEDIA_FROM_SIDE) {
|
||||
val step = RESAMPLE_MEDIA_FROM_SIDE.toFloat() / longest
|
||||
scale((width * step).roundToInt().coerceAtLeast(1), (height * step).roundToInt().coerceAtLeast(1))
|
||||
} else this
|
||||
// One bilinear step from a large image undersamples it.
|
||||
while (image.width / 2 > w) image = image.scale(image.width / 2, (image.height / 2).coerceAtLeast(1))
|
||||
image = image.scale(w, h)
|
||||
// Stretching w px straight to the screen shows their grid; each doubling smooths it, so the last stretch is small.
|
||||
while (maxOf(image.width, image.height) < BLURRED_MEDIA_WIDTH_DP / 2) image = image.scale(image.width * 2, image.height * 2)
|
||||
return image
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.common.views.call
|
||||
|
||||
import chat.simplex.common.AppLock
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.withBGApi
|
||||
@@ -51,13 +52,12 @@ class CallManager(val chatModel: ChatModel) {
|
||||
callUUID = invitation.callUUID,
|
||||
callState = CallState.InvitationAccepted,
|
||||
initialCallType = invitation.callType.media,
|
||||
sharedKey = invitation.sharedKey,
|
||||
hasSharedKey = invitation.sharedKey != null,
|
||||
androidCallState = platform.androidCreateActiveCallState()
|
||||
)
|
||||
showCallView.value = true
|
||||
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, "answerIncomingCall iceServers: $iceServers")
|
||||
callCommand.add(WCallCommand.Start(
|
||||
media = invitation.callType.media,
|
||||
aesKey = invitation.sharedKey,
|
||||
@@ -79,6 +79,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
|
||||
// Don't destroy WebView if you plan to accept next call right after this one
|
||||
if (!switchingCall.value) {
|
||||
AppLock.appWasHidden()
|
||||
showCallView.value = false
|
||||
activeCall.value?.androidCallState?.close()
|
||||
activeCall.value = null
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@ data class Call(
|
||||
val localMediaSources: CallMediaSources = CallMediaSources(mic = true, camera = initialCallType == CallMediaType.Video),
|
||||
val localCapabilities: CallCapabilities? = null,
|
||||
val peerMediaSources: CallMediaSources = CallMediaSources(),
|
||||
val sharedKey: String? = null,
|
||||
val hasSharedKey: Boolean = false,
|
||||
var localCamera: VideoCamera = VideoCamera.User,
|
||||
val connectionInfo: ConnectionInfo? = null,
|
||||
var connectedAt: Instant? = null,
|
||||
@@ -32,14 +32,14 @@ data class Call(
|
||||
|
||||
val androidCallState: Closeable
|
||||
) {
|
||||
val encrypted: Boolean get() = localEncrypted && sharedKey != null
|
||||
val encrypted: Boolean get() = localEncrypted && hasSharedKey
|
||||
private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
||||
|
||||
val encryptionStatus: String get() = when(callState) {
|
||||
CallState.WaitCapabilities -> ""
|
||||
CallState.InvitationSent -> generalGetString(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption)
|
||||
CallState.InvitationAccepted -> generalGetString(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
|
||||
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
|
||||
CallState.InvitationAccepted -> generalGetString(if (!hasSharedKey) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
|
||||
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (!hasSharedKey) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
|
||||
}
|
||||
|
||||
val hasVideo: Boolean
|
||||
|
||||
+2
-1
@@ -1205,12 +1205,13 @@ fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit, disabled: Boolean = false) {
|
||||
SettingsActionItem(
|
||||
if (contactVerified) painterResource(MR.images.ic_verified_user) else painterResource(MR.images.ic_shield),
|
||||
stringResource(if (contactVerified) MR.strings.view_security_code else MR.strings.verify_security_code),
|
||||
click = onClick,
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
disabled = disabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -499,6 +499,15 @@ fun ChatView(
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
groupMembersJob.cancel()
|
||||
val connStats = mutableStateOf<ConnectionStats?>(null)
|
||||
val connectionCode = mutableStateOf<String?>(null)
|
||||
val connectionLoaded = mutableStateOf(false)
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, member, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = false, close = close, closeAll = close)
|
||||
}
|
||||
groupMembersJob = scope.launch(Dispatchers.Default) {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
@@ -513,15 +522,9 @@ fun ChatView(
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertGroupMember(chatRh, groupInfo, updatedMember)
|
||||
}
|
||||
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close = close, closeAll = close)
|
||||
}
|
||||
}
|
||||
connStats.value = stats
|
||||
connectionCode.value = code
|
||||
connectionLoaded.value = true
|
||||
}
|
||||
},
|
||||
loadMessages = { chatId, pagination, visibleItemIndexes ->
|
||||
|
||||
+12
-8
@@ -123,6 +123,15 @@ fun ModalData.GroupChatInfoView(
|
||||
}
|
||||
},
|
||||
showMemberInfo = { member, groupRelay ->
|
||||
val connStats = mutableStateOf<ConnectionStats?>(null)
|
||||
val connectionCode = mutableStateOf<String?>(null)
|
||||
val connectionLoaded = mutableStateOf(false)
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
GroupMemberInfoView(rhId, groupInfo, member, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
@@ -132,14 +141,9 @@ fun ModalData.GroupChatInfoView(
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
connStats.value = stats
|
||||
connectionCode.value = code
|
||||
connectionLoaded.value = true
|
||||
}
|
||||
},
|
||||
editGroupProfile = {
|
||||
|
||||
+33
-10
@@ -45,10 +45,11 @@ import kotlinx.coroutines.*
|
||||
fun GroupMemberInfoView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
groupMember: GroupMember,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
connectionStats: ConnectionStats?,
|
||||
connectionCode: String?,
|
||||
connStats: MutableState<ConnectionStats?>,
|
||||
connectionCode: State<String?>,
|
||||
connectionLoaded: State<Boolean>,
|
||||
chatModel: ChatModel,
|
||||
openedFromSupportChat: Boolean,
|
||||
groupRelay: GroupRelay? = null,
|
||||
@@ -60,7 +61,10 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
// the passed member is shown until the loaded one is added to the model, so that the profile opens without waiting for the core
|
||||
val member = remember(groupMember.groupMemberId) {
|
||||
derivedStateOf { chatModel.getGroupMember(groupMember.groupMemberId) ?: groupMember }
|
||||
}.value
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -91,7 +95,8 @@ fun GroupMemberInfoView(
|
||||
connStats,
|
||||
newRole,
|
||||
developerTools,
|
||||
connectionCode,
|
||||
connectionCode.value,
|
||||
connectionLoaded.value,
|
||||
groupRelay = groupRelay,
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
openDirectChat = { contactId ->
|
||||
@@ -101,6 +106,7 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
},
|
||||
createMemberContact = {
|
||||
val connectionStats = connStats.value
|
||||
if (member.sendMsgEnabled) {
|
||||
withBGApi {
|
||||
progressIndicator = true
|
||||
@@ -209,7 +215,7 @@ fun GroupMemberInfoView(
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
VerifyCodeView(
|
||||
mem.displayName,
|
||||
connectionCode,
|
||||
connectionCode.value,
|
||||
mem.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
@@ -368,6 +374,7 @@ fun GroupMemberInfoLayout(
|
||||
newRole: MutableState<GroupMemberRole>,
|
||||
developerTools: Boolean,
|
||||
connectionCode: String?,
|
||||
connectionLoaded: Boolean,
|
||||
groupRelay: GroupRelay? = null,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
@@ -538,22 +545,23 @@ fun GroupMemberInfoLayout(
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
val memberConnected = member.memberActive || (groupInfo.useRelays && member.memberCurrent)
|
||||
val showMemberSupportChat = !openedFromSupportChat &&
|
||||
groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
|
||||
member.memberRole != GroupMemberRole.Relay &&
|
||||
((groupInfo.fullGroupPreferences.support.on && member.memberRole < GroupMemberRole.Moderator)
|
||||
|| member.supportChat != null)
|
||||
val canVerifyCode = connectionCode != null && member.memberRole != GroupMemberRole.Relay
|
||||
// the same condition decides whether the code is requested, so the row is shown disabled until it arrives
|
||||
val canVerifyCode = member.memberRole != GroupMemberRole.Relay && (connectionCode != null || !connectionLoaded)
|
||||
val canSyncConn = cStats != null && cStats.ratchetSyncAllowed
|
||||
|
||||
if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent))
|
||||
&& (showMemberSupportChat || canVerifyCode || canSyncConn)) {
|
||||
if (memberConnected && (showMemberSupportChat || canVerifyCode || canSyncConn)) {
|
||||
SectionView {
|
||||
if (showMemberSupportChat) {
|
||||
SupportChatButton()
|
||||
}
|
||||
if (canVerifyCode) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
VerifyCodeButton(member.verified, verifyClicked, disabled = connectionCode == null)
|
||||
}
|
||||
if (canSyncConn) {
|
||||
SynchronizeConnectionButton(syncMemberConnection)
|
||||
@@ -655,6 +663,20 @@ fun GroupMemberInfoLayout(
|
||||
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
} else if (!connectionLoaded && memberConnected) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||
// the status is set from the same receiving queues as the servers below, so it is reserved with them
|
||||
SectionItemView {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(stringResource(MR.strings.network_status))
|
||||
Icon(painterResource(MR.images.ic_info), stringResource(MR.strings.network_status), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
SwitchAddressButton(disabled = true, switchAddress = {})
|
||||
SimplexServers(stringResource(MR.strings.receiving_via), emptyList())
|
||||
SimplexServers(stringResource(MR.strings.sending_via), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
val connFailedErr = member.activeConn?.connFailedErr
|
||||
@@ -1117,6 +1139,7 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
|
||||
developerTools = false,
|
||||
connectionCode = "123",
|
||||
connectionLoaded = true,
|
||||
getContactChat = { Chat.sampleData },
|
||||
openDirectChat = {},
|
||||
createMemberContact = {},
|
||||
|
||||
+12
-8
@@ -76,6 +76,15 @@ fun MemberSupportChatAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
title = { MemberSupportChatToolbarTitle(scopeMember_) },
|
||||
onTitleClick = {
|
||||
val connStats = mutableStateOf<ConnectionStats?>(null)
|
||||
val connectionCode = mutableStateOf<String?>(null)
|
||||
val connectionLoaded = mutableStateOf(false)
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
GroupMemberInfoView(rhId, groupInfo, scopeMember_, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = true, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId)
|
||||
val stats = r?.second
|
||||
@@ -85,14 +94,9 @@ fun MemberSupportChatAppBar(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(scopeMember_.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = true, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
connStats.value = stats
|
||||
connectionCode.value = code
|
||||
connectionLoaded.value = true
|
||||
}
|
||||
},
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
|
||||
+18
-14
@@ -114,7 +114,7 @@ fun CIImageView(
|
||||
onClick = onClick
|
||||
)
|
||||
.onRightClick { showMenu.value = true }
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
.privacyBlur(!smallView, imageBitmap, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ fun CIImageView(
|
||||
onClick = onClick
|
||||
)
|
||||
.onRightClick { showMenu.value = true }
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
.privacyBlur(!smallView, previewBitmap, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
|
||||
)
|
||||
} else {
|
||||
@@ -149,7 +149,7 @@ fun CIImageView(
|
||||
onClick = {}
|
||||
)
|
||||
.onRightClick { showMenu.value = true }
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
.privacyBlur(!smallView, previewBitmap, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
imageView(previewBitmap, onClick = {
|
||||
@@ -195,22 +195,26 @@ fun CIImageView(
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember { mutableStateOf(null) }
|
||||
if (chatModel.connectedToRemote()) {
|
||||
LaunchedEffect(file, CIFile.cachedRemoteFileRequests.toList()) {
|
||||
withBGApi {
|
||||
// Hidden media is not worth reading, decoding at full size and caching.
|
||||
val revealed = !blurHidesMedia(!smallView, blurred)
|
||||
if (revealed) {
|
||||
if (chatModel.connectedToRemote()) {
|
||||
LaunchedEffect(file, CIFile.cachedRemoteFileRequests.toList()) {
|
||||
withBGApi {
|
||||
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
|
||||
res.value = imageAndFilePath(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(file) {
|
||||
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
|
||||
res.value = imageAndFilePath(file)
|
||||
res.value = withContext(Dispatchers.IO) { imageAndFilePath(file) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(file) {
|
||||
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
|
||||
res.value = withContext(Dispatchers.IO) { imageAndFilePath(file) }
|
||||
}
|
||||
}
|
||||
}
|
||||
val loaded = res.value
|
||||
val loaded = if (revealed) res.value else null
|
||||
if (loaded != null && file != null) {
|
||||
val (imageBitmap, data, _) = loaded
|
||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, blurred, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
|
||||
|
||||
+1
-1
@@ -421,7 +421,7 @@ fun VideoPreviewImageView(
|
||||
onClick = onClick
|
||||
)
|
||||
.onRightClick(onLongClick)
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick),
|
||||
.privacyBlur(!smallView, preview, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick),
|
||||
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -779,7 +779,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
|
||||
} else {
|
||||
withBGApi {
|
||||
chatModel.appOpenUrlConnecting.value = true
|
||||
planAndConnect(rhId, uri, close = null, cleanup = { chatModel.appOpenUrlConnecting.value = false })
|
||||
planAndConnect(rhId, uri, close = { ModalManager.closeAllModalsEverywhere() }, cleanup = { chatModel.appOpenUrlConnecting.value = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -556,12 +556,16 @@ suspend fun deleteChatAsync(m: ChatModel) {
|
||||
fun deleteChatDatabaseFilesAndState() {
|
||||
val chat = File(dataDir, chatDatabaseFileName)
|
||||
val chatBak = File(dataDir, "$chatDatabaseFileName.bak")
|
||||
val chatExported = File(dataDir, "$chatDatabaseFileName.exported")
|
||||
val agent = File(dataDir, agentDatabaseFileName)
|
||||
val agentBak = File(dataDir, "$agentDatabaseFileName.bak")
|
||||
val agentExported = File(dataDir, "$agentDatabaseFileName.exported")
|
||||
chat.delete()
|
||||
chatBak.delete()
|
||||
chatExported.delete()
|
||||
agent.delete()
|
||||
agentBak.delete()
|
||||
agentExported.delete()
|
||||
filesDir.deleteRecursively()
|
||||
filesDir.mkdir()
|
||||
remoteHostsDir.deleteRecursively()
|
||||
|
||||
+32
-27
@@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.graphics.layer.drawLayer
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.drawBarsBlurred
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
|
||||
fun Modifier.blurredBackgroundModifier(
|
||||
@@ -32,7 +33,7 @@ fun Modifier.blurredBackgroundModifier(
|
||||
return if (appPlatform.isAndroid) {
|
||||
this.androidBlurredModifier(keyboardInset, blurRadius.value, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
|
||||
} else {
|
||||
this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
|
||||
this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, handler, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,40 +101,44 @@ private fun Modifier.desktopBlurredModifier(
|
||||
blurRadius: State<Int>,
|
||||
keyboardCoversBar: Boolean,
|
||||
onTop: Boolean,
|
||||
handler: AppBarHandler,
|
||||
graphicsLayer: GraphicsLayer,
|
||||
backgroundGraphicsLayer: GraphicsLayer,
|
||||
backgroundGraphicsLayerSize: State<IntSize>,
|
||||
density: Density
|
||||
): Modifier = this
|
||||
.graphicsLayer {
|
||||
renderEffect = if (blurRadius.value > 0) BlurEffect(blurRadius.value.dp.toPx(), blurRadius.value.dp.toPx()) else null
|
||||
clip = blurRadius.value > 0
|
||||
}
|
||||
.drawBehind {
|
||||
drawRect(CurrentColors.value.colors.background)
|
||||
if (onTop) {
|
||||
clipRect {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
val barWidth = size.width
|
||||
val barHeight = size.height
|
||||
// The blur is taken from a copy of the scrolled content rather than drawn from that layer, so unlike a layer's own
|
||||
// filter it does not follow the content on its own: the bar is redrawn when the container reports the copy moved.
|
||||
handler.contentVersion.value
|
||||
drawBarsBlurred(blurRadius.value.dp.toPx(), barWidth, barHeight) {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(barWidth, barHeight))
|
||||
if (onTop) {
|
||||
clipRect(0f, 0f, barWidth, barHeight) {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
} else {
|
||||
val bgSize = when {
|
||||
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||
else -> backgroundGraphicsLayerSize.value.height
|
||||
}
|
||||
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||
translate(top = -bgSize + size.height + keyboardHeightCovered) {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
} else {
|
||||
val bgSize = when {
|
||||
backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height
|
||||
backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height
|
||||
else -> backgroundGraphicsLayerSize.value.height
|
||||
}
|
||||
val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0
|
||||
translate(top = -bgSize + barHeight + keyboardHeightCovered) {
|
||||
if (backgroundGraphicsLayer.size != IntSize.Zero) {
|
||||
drawLayer(backgroundGraphicsLayer)
|
||||
} else {
|
||||
drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat()))
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
drawLayer(graphicsLayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -78,6 +78,10 @@ class AppBarHandler(
|
||||
|
||||
val backgroundGraphicsLayerSize: MutableState<IntSize> = mutableStateOf(IntSize.Zero)
|
||||
|
||||
// Bars that blur a copy of the scrolled content depend on this to know the copy moved. It is bumped from a collector
|
||||
// rather than while drawing: a write made during the draw phase invalidates the bars every frame and never settles.
|
||||
val contentVersion: MutableState<Int> = mutableStateOf(0)
|
||||
|
||||
companion object {
|
||||
var appBarMaxHeightPx: Int = 0
|
||||
}
|
||||
|
||||
+2
-2
@@ -194,7 +194,7 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel
|
||||
|
||||
@Composable
|
||||
fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State<Boolean>, onLongClick: () -> Unit) {
|
||||
val image = base64ToBitmap(linkPreview.image)
|
||||
val image = remember(linkPreview.image) { base64ToBitmap(linkPreview.image) }
|
||||
Column(
|
||||
Modifier
|
||||
.layoutId(CHAT_IMAGE_LAYOUT_ID)
|
||||
@@ -207,7 +207,7 @@ fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State<Boolean>, onLongC
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.desktopModifyBlurredState(true, blurred, showMenu)
|
||||
.privacyBlur(true, blurred, chatViewScrollState.collectAsState(), onLongClick = onLongClick),
|
||||
.privacyBlur(true, image, blurred, chatViewScrollState.collectAsState(), onLongClick = onLongClick),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
Column(Modifier.padding(top = 6.dp).padding(horizontal = 12.dp)) {
|
||||
|
||||
+2
-2
@@ -1033,7 +1033,7 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean {
|
||||
return v != lastVersion
|
||||
}
|
||||
|
||||
private const val WEFUNDER_URL = "https://wefunder.com/simplex.chat"
|
||||
private const val WEFUNDER_URL = "https://wefunder.com/simplex.chat?utm_source=app"
|
||||
|
||||
private const val CROWDFUNDING_CONTACT_URI = "simplex:/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo?h=smp11.simplex.im"
|
||||
|
||||
@@ -1197,7 +1197,7 @@ fun GetStakeView(showFirstImage: Boolean, inCenterOfWindow: Boolean = false, clo
|
||||
|
||||
ColumnWithScrollBar(Modifier.pinchZoom().padding(horizontal = DEFAULT_PADDING)) {
|
||||
// in the center of the window the page is wide enough for the title to fit on one line
|
||||
val title = "Get a stake in\nSimpleX Chat"
|
||||
val title = "Invest in\nSimpleX Chat"
|
||||
AppBarTitle(if (inCenterOfWindow) title.replace("\n", " ") else title, withPadding = false)
|
||||
// What's new already shows the image of the first slide, above the link that opens this page
|
||||
if (showFirstImage) {
|
||||
|
||||
+4
@@ -74,6 +74,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: (
|
||||
val simplexTeamUri =
|
||||
"simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im"
|
||||
|
||||
val simplexNewsUri =
|
||||
"simplex:/c#grcfG3ulVI4Sh6ow33qBsmSk7uEy3gRSl2KkJ5ER6tA?h=smp18.simplex.im"
|
||||
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
stopped: Boolean,
|
||||
@@ -161,6 +164,7 @@ fun HelpAndSupportView(
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_contact)) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_bigtop_updates), stringResource(MR.strings.follow_simplex_network_news), { uriHandler.openVerifiedSimplexUri(simplexNewsUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
|
||||
}
|
||||
SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary)
|
||||
|
||||
@@ -1015,6 +1015,7 @@
|
||||
<string name="markdown_in_messages">Markdown in messages</string>
|
||||
<string name="chat_with_the_founder">Send questions and ideas</string>
|
||||
<string name="send_us_an_email">Send us email</string>
|
||||
<string name="follow_simplex_network_news">Follow SimpleX Network News</string>
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="message_servers">Message servers</string>
|
||||
@@ -1783,6 +1784,7 @@
|
||||
<string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string>
|
||||
<string name="rcv_group_event_member_created_contact">requested connection</string>
|
||||
<string name="rcv_group_event_new_member_pending_review">New member wants to join the group.</string>
|
||||
<string name="rcv_group_event_msg_bad_signature">message rejected: bad signature</string>
|
||||
<string name="snd_group_event_changed_member_role">you changed role of %s to %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string>
|
||||
<string name="snd_group_event_member_blocked">you blocked %s</string>
|
||||
@@ -2678,8 +2680,8 @@
|
||||
<string name="v7_0_invest_descr" translatable="false">Crowdfunding on Wefunder.</string>
|
||||
<string name="v7_0_crowdfunding" translatable="false">Crowdfunding on Wefunder</string>
|
||||
<string name="v7_0_invest_learn_more" translatable="false">Learn more on Wefunder</string>
|
||||
<string name="invest_banner_title" translatable="false">Get a stake in SimpleX Chat!</string>
|
||||
<string name="invest_banner_subtitle" translatable="false">Invest on Wefunder from $100</string>
|
||||
<string name="invest_banner_title" translatable="false">Invest in SimpleX Chat!</string>
|
||||
<string name="invest_banner_subtitle" translatable="false">Crowdfunding on Wefunder</string>
|
||||
<string name="v7_0_simplex_names">SimpleX public names (BETA)</string>
|
||||
<string name="v7_0_simplex_names_descr">Public names for your channel or business.</string>
|
||||
<string name="v7_0_channels">Better channels 📢</string>
|
||||
|
||||
@@ -182,9 +182,10 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
|
||||
}
|
||||
}
|
||||
var windowFocused by remember { simplexWindowState.windowFocused }
|
||||
LaunchedEffect(windowFocused) {
|
||||
val showCallView = ChatModel.showCallView.value
|
||||
LaunchedEffect(windowFocused, showCallView) {
|
||||
val delay = ChatController.appPrefs.laLockDelay.get()
|
||||
if (!windowFocused && ChatModel.showAuthScreen.value && delay > 0) {
|
||||
if (!windowFocused && !showCallView && ChatModel.showAuthScreen.value && delay > 0) {
|
||||
delay(delay * 1000L)
|
||||
// Trigger auth state check when delay ends (and if it ends)
|
||||
AppLock.recheckAuthState()
|
||||
|
||||
+5
-6
@@ -76,12 +76,11 @@ internal class SkiaBitmapVideoSurface : VideoSurface(VideoSurfaceAdapters.getVid
|
||||
override fun getBufferFormat(sourceWidth: Int, sourceHeight: Int): BufferFormat {
|
||||
// libvlc passes the size the decoder padded the picture to, not the size of the picture (dav1d
|
||||
// pads to a multiple of 128, so 1920x1080 arrives as 1920x1152), and vlc stretches the picture to
|
||||
// fill whatever size is returned. Ask for the size of the track being played instead. The format
|
||||
// is negotiated more than once, and vlc has not selected the track yet on the first calls
|
||||
val player = mediaPlayer
|
||||
val tracks = player?.media()?.info()?.videoTracks()
|
||||
val playingTrack = player?.video()?.track()
|
||||
val track = tracks?.firstOrNull { it.id() == playingTrack } ?: tracks?.singleOrNull()
|
||||
// fill whatever size is returned. Ask for the size of the video track instead. Not the selected
|
||||
// track: video().track() takes the input lock, which stop() holds while it waits for this thread
|
||||
// to finish, so asking here deadlocks both, and every caller of the player after them
|
||||
val tracks = mediaPlayer?.media()?.info()?.videoTracks()
|
||||
val track = tracks?.singleOrNull()
|
||||
// Both track sides or neither: one side from the track and the other from the padded size libvlc
|
||||
// passed never described the same picture, and transposing such a pair compounds the mismatch
|
||||
val trackW = track?.width() ?: 0
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlurEffect
|
||||
import androidx.compose.ui.graphics.asComposeCanvas
|
||||
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.unit.Density
|
||||
import org.jetbrains.skia.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
// Skia's raster blur costs width x (bar + 6 sigma) pixels every frame, which is what makes the bars lag. The blur cannot be
|
||||
// made cheaper by scaling the layer it runs on, because a layer's filter is evaluated in device space: any scale applied to
|
||||
// the layer scales the sigma straight back up, which weakens the blur without saving anything. So the bar is drawn into a
|
||||
// narrower offscreen surface and blurred there, at its own resolution, and only the finished blur is scaled back out.
|
||||
// Only the width is reduced: a bar is just AppBarHeight tall, and taking rows away from it visibly weakens the blur.
|
||||
private const val MAX_BLUR_NARROWING = 8
|
||||
private const val MIN_BLUR_WIDTH = 128
|
||||
|
||||
// How small the blur's own sigma is allowed to get. Below about this the narrowed copy is barely blurred, and stretching it
|
||||
// back out shows the steps of the narrowing instead of a blur.
|
||||
private const val MIN_NARROWED_SIGMA = 1.5f
|
||||
|
||||
// The rows are halved as well, with the vertical sigma halved to match, which is only sound because the copy is drawn at
|
||||
// that size rather than scaled afterwards. A bar has few rows to begin with, so it is not reduced past this.
|
||||
private const val MIN_BLUR_HEIGHT = 24
|
||||
|
||||
// The panes of the desktop window have different widths, so bars of several sizes are drawn in the same frame and a single
|
||||
// pair of surfaces would be reallocated for each of them. Keeping one pair per size, with the least recently drawn size
|
||||
// released once there are more than the window can show at once, allocates each pair once instead.
|
||||
private const val MAX_BAR_SURFACES = 4
|
||||
|
||||
private class BarBlurSurface(width: Int, height: Int) {
|
||||
val surface: Surface = Surface.makeRaster(ImageInfo.makeN32Premul(width, height))
|
||||
|
||||
fun close() = surface.close()
|
||||
}
|
||||
|
||||
private val barBlurSurfaces = ThreadLocal.withInitial { LinkedHashMap<Long, BarBlurSurface>() }
|
||||
|
||||
private fun surfaceFor(width: Int, height: Int): BarBlurSurface {
|
||||
val cached = barBlurSurfaces.get()
|
||||
val key = (width.toLong() shl 32) or height.toLong()
|
||||
val surface = cached.remove(key) ?: BarBlurSurface(width, height)
|
||||
cached[key] = surface
|
||||
while (cached.size > MAX_BAR_SURFACES) {
|
||||
cached.remove(cached.keys.first())?.close()
|
||||
}
|
||||
return surface
|
||||
}
|
||||
|
||||
private fun narrowing(sigma: Float, barWidth: Float): Int {
|
||||
var scale = 1
|
||||
while (sigma / (scale * 2) >= MIN_NARROWED_SIGMA && scale < MAX_BLUR_NARROWING && barWidth / (scale * 2) >= MIN_BLUR_WIDTH) scale *= 2
|
||||
return scale
|
||||
}
|
||||
|
||||
private fun shortening(sigma: Float, barHeight: Float): Int =
|
||||
if (sigma / 2 >= MIN_NARROWED_SIGMA && barHeight / 2 >= MIN_BLUR_HEIGHT) 2 else 1
|
||||
|
||||
actual fun DrawScope.drawBarsBlurred(radiusPx: Float, barWidth: Float, barHeight: Float, drawBar: DrawScope.() -> Unit) {
|
||||
val sigma = BlurEffect.convertRadiusToSigma(radiusPx)
|
||||
val scale = narrowing(sigma, barWidth)
|
||||
val rows = shortening(sigma, barHeight)
|
||||
val width = ceil(barWidth / scale).toInt()
|
||||
val height = ceil(barHeight / rows).toInt()
|
||||
if (width <= 0 || height <= 0) return
|
||||
val surface = surfaceFor(width, height).surface
|
||||
|
||||
surface.canvas.clear(Color.TRANSPARENT)
|
||||
// the blur is the layer's own paint, so it runs at this surface's resolution rather than at the canvas the bar draws to
|
||||
surface.canvas.saveLayer(Rect.makeWH(width.toFloat(), height.toFloat()), Paint().apply {
|
||||
imageFilter = ImageFilter.makeBlur(sigma / scale, sigma / rows, FilterTileMode.CLAMP)
|
||||
})
|
||||
CanvasDrawScope().draw(Density(density, fontScale), layoutDirection, surface.canvas.asComposeCanvas(), Size(width.toFloat(), height.toFloat())) {
|
||||
scale(1f / scale, 1f / rows, Offset.Zero) { drawBar() }
|
||||
}
|
||||
surface.canvas.restore()
|
||||
|
||||
// The surface is whole pixels while the bar is not, so only the bar's own area is taken from it.
|
||||
drawIntoCanvas {
|
||||
it.nativeCanvas.drawImageRect(
|
||||
surface.makeImageSnapshot(),
|
||||
Rect.makeWH(barWidth / scale, barHeight / rows),
|
||||
Rect.makeWH(barWidth, barHeight),
|
||||
SamplingMode.LINEAR,
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
+6
@@ -64,6 +64,9 @@ actual fun LazyColumnWithScrollBar(
|
||||
}
|
||||
val state = state ?: handler.listState
|
||||
val connection = handler.connection
|
||||
LaunchedEffect(state, handler) {
|
||||
snapshotFlow { state.firstVisibleItemIndex to state.firstVisibleItemScrollOffset }.collect { handler.contentVersion.value++ }
|
||||
}
|
||||
// When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state
|
||||
// (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row)
|
||||
val scrollBarDraggingState = remember { mutableStateOf(false) }
|
||||
@@ -202,6 +205,9 @@ actual fun ColumnWithScrollBar(
|
||||
}
|
||||
}
|
||||
val state = state ?: handler.scrollState
|
||||
LaunchedEffect(state, handler) {
|
||||
snapshotFlow { state.value }.collect { handler.contentVersion.value++ }
|
||||
}
|
||||
val connection = handler.connection
|
||||
// When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state
|
||||
// (exact scroll position is available but in Int, not Float)
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@ val connections = ArrayList<WebSocket>()
|
||||
actual fun ActiveCallView() {
|
||||
val scope = rememberCoroutineScope()
|
||||
WebRTCController(chatModel.callCommand) { apiMsg ->
|
||||
Log.d(TAG, "received from WebRTCController: $apiMsg")
|
||||
Log.d(TAG, "received from WebRTCController: ${apiMsg.resp.javaClass.simpleName}")
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
Log.d(TAG, "has active call $call")
|
||||
@@ -202,7 +202,7 @@ fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, onResponse: (
|
||||
}
|
||||
while (callCommand.isNotEmpty()) {
|
||||
val cmd = callCommand.removeFirstOrNull()
|
||||
Log.d(TAG, "WebRTCController LaunchedEffect executing $cmd")
|
||||
Log.d(TAG, "WebRTCController LaunchedEffect executing ${cmd?.javaClass?.simpleName}")
|
||||
if (cmd != null) {
|
||||
processCommand(cmd)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ class MyWebSocket(val onResponse: (WVAPIMessage) -> Unit, handshakeRequest: IHTT
|
||||
// onResponse(message.textPayload)
|
||||
onResponse(json.decodeFromString(message.textPayload))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "failed parsing browser message: $message")
|
||||
Log.e(TAG, "failed parsing browser message")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user