From eaf6f9846905095b33b5765171d25a2af216275a Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 23 Oct 2025 11:21:46 +1100 Subject: [PATCH 1/6] Updated to the latest BOM --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1cd175d618..49a4f171e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ assertjCoreVersion = "3.27.6" biometricVersion = "1.1.0" cameraCamera2Version = "1.5.0" cardviewVersion = "1.0.0" -composeBomVersion = "2025.09.01" +composeBomVersion = "2025.10.01" conscryptAndroidVersion = "2.5.3" conscryptJavaVersion = "2.5.2" constraintlayoutVersion = "2.2.1" From 7284f8c4af5f71e388172eadf1dba5ef1d303016 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 23 Oct 2025 11:21:59 +1100 Subject: [PATCH 2/6] SES-4767 - zoom handling in QR code scanning --- .../securesms/ui/components/QR.kt | 90 ++++++++++++++----- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index 8a2b3e26bd..a0454ff985 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -10,8 +10,11 @@ import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.CameraController +import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,6 +41,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -163,32 +167,42 @@ fun QRScannerScreen( @Composable fun ScanQrCode(errors: Flow, onScan: (String) -> Unit) { - val localContext = LocalContext.current - val cameraProvider = remember { ProcessCameraProvider.getInstance(localContext) } - - val preview = Preview.Builder().build() - val selector = CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) - .build() - - runCatching { - cameraProvider.get().unbindAll() - - cameraProvider.get().bindToLifecycle( - LocalLifecycleOwner.current, - selector, - preview, - buildAnalysisUseCase(QRCodeReader(), onScan) - ) - - }.onFailure { Log.e(TAG, "error binding camera", it) } + val context = LocalContext.current + + // Setting up camera objects + val lifecycleOwner = LocalLifecycleOwner.current + val controller = remember { + LifecycleCameraController(context).apply { + setEnabledUseCases(CameraController.IMAGE_ANALYSIS) + setTapToFocusEnabled(true) + setPinchToZoomEnabled(true) + } + } - DisposableEffect(cameraProvider) { + DisposableEffect(Unit) { + val executor = Executors.newSingleThreadExecutor() + controller.setImageAnalysisAnalyzer(executor, QRCodeAnalyzer(QRCodeReader(), onScan)) onDispose { - cameraProvider.get().unbindAll() + controller.clearImageAnalysisAnalyzer() + executor.shutdown() } } + LaunchedEffect(controller, lifecycleOwner) { + controller.bindToLifecycle(lifecycleOwner) + controller.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + } + + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + PreviewView(ctx).apply { + controller.let { this.controller = it } + } + } + ) + + val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() @@ -224,12 +238,24 @@ fun ScanQrCode(errors: Flow, onScan: (String) -> Unit) { } } ) { padding -> - Box { + var cachedZoom by remember { mutableStateOf(1f) } + + val zoomRange = controller.zoomState.value?.let { + it.minZoomRatio..it.maxZoomRatio + } ?: 1f..4f + + Box(Modifier.fillMaxSize() + .padding(padding)) { AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } } + modifier = Modifier.matchParentSize(), + factory = { ctx -> + PreviewView(ctx).apply { + this.controller = controller + } + } ) + // visual cue for middle part Box( Modifier .aspectRatio(1f) @@ -238,6 +264,22 @@ fun ScanQrCode(errors: Flow, onScan: (String) -> Unit) { .background(Color(0x33ffffff)) .align(Alignment.Center) ) + + // Fullscreen overlay that captures gestures and updates camera zoom + // Without this, the bottom sheet in start-conversation, or the viewpagers + // all fight for gesture handling and the zoom doesn't work + Box( + Modifier + .matchParentSize() + .pointerInput(controller) { + detectTransformGestures { _, _, zoom, _ -> + val new = (cachedZoom * zoom) + .coerceIn(zoomRange.start, zoomRange.endInclusive) + cachedZoom = new + controller.cameraControl?.setZoomRatio(new) + } + } + ) } } } From b318c9f19d79fc06db70af9b7dbf9d5c59ee8e33 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 23 Oct 2025 11:30:21 +1100 Subject: [PATCH 3/6] SES-4768 - moderators can "delete for everyone" in a community --- .../securesms/conversation/v2/ConversationViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 21113c8bc3..0d0ee140fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -225,6 +225,10 @@ class ConversationViewModel @AssistedInject constructor( it.currentUserRole in EnumSet.of(GroupMemberRole.ADMIN, GroupMemberRole.HIDDEN_ADMIN) } + val canModerate: StateFlow = recipientFlow.mapStateFlow(viewModelScope) { + it.currentUserRole.canModerate + } + private val _searchOpened = MutableStateFlow(false) val appBarData: StateFlow = combine( @@ -750,7 +754,7 @@ class ConversationViewModel @AssistedInject constructor( } // If the user is an admin or is interacting with their own message And are allowed to delete for everyone - (isAdmin.value || allSentByCurrentUser) && canDeleteForEveryone -> { + (canModerate.value || allSentByCurrentUser) && canDeleteForEveryone -> { _dialogsState.update { it.copy( deleteEveryone = DeleteForEveryoneDialogData( From 657928cc77f4f4d6005faee8a87990c1857e268c Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 23 Oct 2025 11:55:23 +1100 Subject: [PATCH 4/6] SES-2740 - Do not show indicators from others when the toggle is off --- .../messaging/sending_receiving/ReceivedMessageHandler.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index cd04dec911..b45e0b2a2b 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -53,6 +53,7 @@ import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID import org.session.libsession.utilities.SSKEnvironment +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.MessageType import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientData @@ -100,6 +101,7 @@ class ReceivedMessageHandler @Inject constructor( @param:ManagerScope private val scope: CoroutineScope, private val configFactory: ConfigFactoryProtocol, private val messageRequestResponseHandler: Provider, + private val prefs: TextSecurePreferences, ) { suspend fun handle( @@ -183,6 +185,9 @@ class ReceivedMessageHandler @Inject constructor( } private fun showTypingIndicatorIfNeeded(senderPublicKey: String) { + // We don't want to show other people's indicators if the toggle is off + if(!prefs.isTypingIndicatorsEnabled()) return + val address = Address.fromSerialized(senderPublicKey) val threadID = storage.getThreadId(address) ?: return typingIndicators.didReceiveTypingStartedMessage(threadID, address, 1) From 5df0d79f1db8f832ecc68953835da131cb96f5a3 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 23 Oct 2025 12:57:09 +1100 Subject: [PATCH 5/6] Added a typing indicatorin the preference setting --- .../TypingIndicatorPreferenceCompat.kt | 81 +++++++++++++++++++ .../TypingIndicatorViewContainer.kt | 7 +- .../layout/typing_indicator_preference.xml | 23 ++++++ app/src/main/res/xml/preferences_privacy.xml | 4 +- 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/TypingIndicatorPreferenceCompat.kt create mode 100644 app/src/main/res/layout/typing_indicator_preference.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TypingIndicatorPreferenceCompat.kt b/app/src/main/java/org/thoughtcrime/securesms/components/TypingIndicatorPreferenceCompat.kt new file mode 100644 index 0000000000..fc597a7894 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TypingIndicatorPreferenceCompat.kt @@ -0,0 +1,81 @@ +package org.thoughtcrime.securesms.components + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView +import androidx.preference.PreferenceViewHolder +import androidx.preference.TwoStatePreference +import kotlinx.coroutines.flow.MutableStateFlow +import network.loki.messenger.R +import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY +import org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer +import org.thoughtcrime.securesms.ui.components.SessionSwitch +import org.thoughtcrime.securesms.ui.getSubbedCharSequence +import org.thoughtcrime.securesms.ui.setThemedContent + +class TypingIndicatorPreferenceCompat : TwoStatePreference { + private var listener: OnPreferenceClickListener? = null + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, androidx.preference.R.attr.switchPreferenceCompatStyle) + constructor(context: Context) : this(context, null, androidx.preference.R.attr.switchPreferenceCompatStyle) + + private val checkState = MutableStateFlow(isChecked) + private val enableState = MutableStateFlow(isEnabled) + + init { + widgetLayoutResource = R.layout.typing_indicator_preference + } + + override fun setChecked(checked: Boolean) { + super.setChecked(checked) + + checkState.value = checked + } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + + enableState.value = enabled + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + val composeView = holder.findViewById(R.id.compose_preference) as ComposeView + composeView.setThemedContent { + SessionSwitch( + checked = checkState.collectAsState().value, + onCheckedChange = null, + enabled = isEnabled + ) + } + + val typingView = holder.findViewById(R.id.pref_typing_indicator_view) as TypingIndicatorViewContainer + typingView.apply { + startAnimation() + + // stop animation if the preference row is detached + holder.itemView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) = Unit + override fun onViewDetachedFromWindow(v: View) { + stopAnimation() + v.removeOnAttachStateChangeListener(this) + } + }) + } + } + + override fun setOnPreferenceClickListener(listener: OnPreferenceClickListener?) { + this.listener = listener + } + + override fun onClick() { + if (listener == null || !listener!!.onPreferenceClick(this)) { + super.onClick() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt index f9a10c602d..e362531fbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt @@ -19,7 +19,10 @@ class TypingIndicatorViewContainer : LinearLayout { } fun setTypists(typists: List
) { - if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return } - binding.typingIndicator.root.startAnimation() + if (typists.isEmpty()) { stopAnimation(); return } + startAnimation() } + + fun startAnimation() = binding.typingIndicator.root.startAnimation() + fun stopAnimation() = binding.typingIndicator.root.stopAnimation() } \ No newline at end of file diff --git a/app/src/main/res/layout/typing_indicator_preference.xml b/app/src/main/res/layout/typing_indicator_preference.xml new file mode 100644 index 0000000000..a7754d36b3 --- /dev/null +++ b/app/src/main/res/layout/typing_indicator_preference.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_privacy.xml b/app/src/main/res/xml/preferences_privacy.xml index ab5449e31d..eaed62b9b1 100644 --- a/app/src/main/res/xml/preferences_privacy.xml +++ b/app/src/main/res/xml/preferences_privacy.xml @@ -36,13 +36,11 @@ - - - From 2da5c43f3f601b39be1bb49c2f82346f39a6dc37 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 23 Oct 2025 13:08:34 +1100 Subject: [PATCH 6/6] bumping code for dev --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d769563e80..f0b0765605 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,7 @@ configurations.configureEach { } val canonicalVersionCode = 427 -val canonicalVersionName = "1.28.2" +val canonicalVersionName = "1.30.0" val postFixSize = 10 val abiPostFix = mapOf(