Skip to content

Commit 0b62a36

Browse files
Merge pull request #1630 from session-foundation/feature/bom-2025-10-01
Bom: 2025-10-01
2 parents 9c79853 + 5329d43 commit 0b62a36

File tree

9 files changed

+188
-32
lines changed

9 files changed

+188
-32
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ configurations.configureEach {
2727
}
2828

2929
val canonicalVersionCode = 427
30-
val canonicalVersionName = "1.28.2"
30+
val canonicalVersionName = "1.30.0"
3131

3232
val postFixSize = 10
3333
val abiPostFix = mapOf(

app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import org.session.libsession.utilities.ConfigFactoryProtocol
5353
import org.session.libsession.utilities.GroupRecord
5454
import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
5555
import org.session.libsession.utilities.SSKEnvironment
56+
import org.session.libsession.utilities.TextSecurePreferences
5657
import org.session.libsession.utilities.recipients.MessageType
5758
import org.session.libsession.utilities.recipients.Recipient
5859
import org.session.libsession.utilities.recipients.RecipientData
@@ -100,6 +101,7 @@ class ReceivedMessageHandler @Inject constructor(
100101
@param:ManagerScope private val scope: CoroutineScope,
101102
private val configFactory: ConfigFactoryProtocol,
102103
private val messageRequestResponseHandler: Provider<MessageRequestResponseHandler>,
104+
private val prefs: TextSecurePreferences,
103105
) {
104106

105107
suspend fun handle(
@@ -183,6 +185,9 @@ class ReceivedMessageHandler @Inject constructor(
183185
}
184186

185187
private fun showTypingIndicatorIfNeeded(senderPublicKey: String) {
188+
// We don't want to show other people's indicators if the toggle is off
189+
if(!prefs.isTypingIndicatorsEnabled()) return
190+
186191
val address = Address.fromSerialized(senderPublicKey)
187192
val threadID = storage.getThreadId(address) ?: return
188193
typingIndicators.didReceiveTypingStartedMessage(threadID, address, 1)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.thoughtcrime.securesms.components
2+
3+
import android.content.Context
4+
import android.util.AttributeSet
5+
import android.view.View
6+
import androidx.compose.runtime.collectAsState
7+
import androidx.compose.ui.platform.ComposeView
8+
import androidx.preference.PreferenceViewHolder
9+
import androidx.preference.TwoStatePreference
10+
import kotlinx.coroutines.flow.MutableStateFlow
11+
import network.loki.messenger.R
12+
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
13+
import org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
14+
import org.thoughtcrime.securesms.ui.components.SessionSwitch
15+
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
16+
import org.thoughtcrime.securesms.ui.setThemedContent
17+
18+
class TypingIndicatorPreferenceCompat : TwoStatePreference {
19+
private var listener: OnPreferenceClickListener? = null
20+
21+
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
22+
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
23+
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, androidx.preference.R.attr.switchPreferenceCompatStyle)
24+
constructor(context: Context) : this(context, null, androidx.preference.R.attr.switchPreferenceCompatStyle)
25+
26+
private val checkState = MutableStateFlow(isChecked)
27+
private val enableState = MutableStateFlow(isEnabled)
28+
29+
init {
30+
widgetLayoutResource = R.layout.typing_indicator_preference
31+
}
32+
33+
override fun setChecked(checked: Boolean) {
34+
super.setChecked(checked)
35+
36+
checkState.value = checked
37+
}
38+
39+
override fun setEnabled(enabled: Boolean) {
40+
super.setEnabled(enabled)
41+
42+
enableState.value = enabled
43+
}
44+
45+
override fun onBindViewHolder(holder: PreferenceViewHolder) {
46+
super.onBindViewHolder(holder)
47+
48+
val composeView = holder.findViewById(R.id.compose_preference) as ComposeView
49+
composeView.setThemedContent {
50+
SessionSwitch(
51+
checked = checkState.collectAsState().value,
52+
onCheckedChange = null,
53+
enabled = isEnabled
54+
)
55+
}
56+
57+
val typingView = holder.findViewById(R.id.pref_typing_indicator_view) as TypingIndicatorViewContainer
58+
typingView.apply {
59+
startAnimation()
60+
61+
// stop animation if the preference row is detached
62+
holder.itemView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
63+
override fun onViewAttachedToWindow(v: View) = Unit
64+
override fun onViewDetachedFromWindow(v: View) {
65+
stopAnimation()
66+
v.removeOnAttachStateChangeListener(this)
67+
}
68+
})
69+
}
70+
}
71+
72+
override fun setOnPreferenceClickListener(listener: OnPreferenceClickListener?) {
73+
this.listener = listener
74+
}
75+
76+
override fun onClick() {
77+
if (listener == null || !listener!!.onPreferenceClick(this)) {
78+
super.onClick()
79+
}
80+
}
81+
}

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ class ConversationViewModel @AssistedInject constructor(
225225
it.currentUserRole in EnumSet.of(GroupMemberRole.ADMIN, GroupMemberRole.HIDDEN_ADMIN)
226226
}
227227

228+
val canModerate: StateFlow<Boolean> = recipientFlow.mapStateFlow(viewModelScope) {
229+
it.currentUserRole.canModerate
230+
}
231+
228232
private val _searchOpened = MutableStateFlow(false)
229233

230234
val appBarData: StateFlow<ConversationAppBarData> = combine(
@@ -750,7 +754,7 @@ class ConversationViewModel @AssistedInject constructor(
750754
}
751755

752756
// If the user is an admin or is interacting with their own message And are allowed to delete for everyone
753-
(isAdmin.value || allSentByCurrentUser) && canDeleteForEveryone -> {
757+
(canModerate.value || allSentByCurrentUser) && canDeleteForEveryone -> {
754758
_dialogsState.update {
755759
it.copy(
756760
deleteEveryone = DeleteForEveryoneDialogData(

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ class TypingIndicatorViewContainer : LinearLayout {
1919
}
2020

2121
fun setTypists(typists: List<Address>) {
22-
if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return }
23-
binding.typingIndicator.root.startAnimation()
22+
if (typists.isEmpty()) { stopAnimation(); return }
23+
startAnimation()
2424
}
25+
26+
fun startAnimation() = binding.typingIndicator.root.startAnimation()
27+
fun stopAnimation() = binding.typingIndicator.root.stopAnimation()
2528
}

app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import androidx.camera.core.ImageAnalysis
1010
import androidx.camera.core.ImageProxy
1111
import androidx.camera.core.Preview
1212
import androidx.camera.lifecycle.ProcessCameraProvider
13+
import androidx.camera.view.CameraController
14+
import androidx.camera.view.LifecycleCameraController
1315
import androidx.camera.view.PreviewView
1416
import androidx.compose.foundation.background
17+
import androidx.compose.foundation.gestures.detectTransformGestures
1518
import androidx.compose.foundation.layout.Box
1619
import androidx.compose.foundation.layout.Column
1720
import androidx.compose.foundation.layout.Spacer
@@ -38,6 +41,7 @@ import androidx.compose.ui.Alignment
3841
import androidx.compose.ui.Modifier
3942
import androidx.compose.ui.draw.clip
4043
import androidx.compose.ui.graphics.Color
44+
import androidx.compose.ui.input.pointer.pointerInput
4145
import androidx.compose.ui.platform.LocalContext
4246
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
4347
import androidx.compose.ui.res.stringResource
@@ -163,32 +167,42 @@ fun QRScannerScreen(
163167

164168
@Composable
165169
fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
166-
val localContext = LocalContext.current
167-
val cameraProvider = remember { ProcessCameraProvider.getInstance(localContext) }
168-
169-
val preview = Preview.Builder().build()
170-
val selector = CameraSelector.Builder()
171-
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
172-
.build()
173-
174-
runCatching {
175-
cameraProvider.get().unbindAll()
176-
177-
cameraProvider.get().bindToLifecycle(
178-
LocalLifecycleOwner.current,
179-
selector,
180-
preview,
181-
buildAnalysisUseCase(QRCodeReader(), onScan)
182-
)
183-
184-
}.onFailure { Log.e(TAG, "error binding camera", it) }
170+
val context = LocalContext.current
171+
172+
// Setting up camera objects
173+
val lifecycleOwner = LocalLifecycleOwner.current
174+
val controller = remember {
175+
LifecycleCameraController(context).apply {
176+
setEnabledUseCases(CameraController.IMAGE_ANALYSIS)
177+
setTapToFocusEnabled(true)
178+
setPinchToZoomEnabled(true)
179+
}
180+
}
185181

186-
DisposableEffect(cameraProvider) {
182+
DisposableEffect(Unit) {
183+
val executor = Executors.newSingleThreadExecutor()
184+
controller.setImageAnalysisAnalyzer(executor, QRCodeAnalyzer(QRCodeReader(), onScan))
187185
onDispose {
188-
cameraProvider.get().unbindAll()
186+
controller.clearImageAnalysisAnalyzer()
187+
executor.shutdown()
189188
}
190189
}
191190

191+
LaunchedEffect(controller, lifecycleOwner) {
192+
controller.bindToLifecycle(lifecycleOwner)
193+
controller.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
194+
}
195+
196+
AndroidView(
197+
modifier = Modifier.fillMaxSize(),
198+
factory = { ctx ->
199+
PreviewView(ctx).apply {
200+
controller.let { this.controller = it }
201+
}
202+
}
203+
)
204+
205+
192206
val snackbarHostState = remember { SnackbarHostState() }
193207
val scope = rememberCoroutineScope()
194208

@@ -224,12 +238,24 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
224238
}
225239
}
226240
) { padding ->
227-
Box {
241+
var cachedZoom by remember { mutableStateOf(1f) }
242+
243+
val zoomRange = controller.zoomState.value?.let {
244+
it.minZoomRatio..it.maxZoomRatio
245+
} ?: 1f..4f
246+
247+
Box(Modifier.fillMaxSize()
248+
.padding(padding)) {
228249
AndroidView(
229-
modifier = Modifier.fillMaxSize(),
230-
factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } }
250+
modifier = Modifier.matchParentSize(),
251+
factory = { ctx ->
252+
PreviewView(ctx).apply {
253+
this.controller = controller
254+
}
255+
}
231256
)
232257

258+
// visual cue for middle part
233259
Box(
234260
Modifier
235261
.aspectRatio(1f)
@@ -238,6 +264,22 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
238264
.background(Color(0x33ffffff))
239265
.align(Alignment.Center)
240266
)
267+
268+
// Fullscreen overlay that captures gestures and updates camera zoom
269+
// Without this, the bottom sheet in start-conversation, or the viewpagers
270+
// all fight for gesture handling and the zoom doesn't work
271+
Box(
272+
Modifier
273+
.matchParentSize()
274+
.pointerInput(controller) {
275+
detectTransformGestures { _, _, zoom, _ ->
276+
val new = (cachedZoom * zoom)
277+
.coerceIn(zoomRange.start, zoomRange.endInclusive)
278+
cachedZoom = new
279+
controller.cameraControl?.setZoomRatio(new)
280+
}
281+
}
282+
)
241283
}
242284
}
243285
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:id="@+id/widget_root"
4+
android:layout_width="wrap_content"
5+
android:layout_height="wrap_content"
6+
android:orientation="horizontal"
7+
android:gravity="center_vertical">
8+
9+
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
10+
android:id="@+id/pref_typing_indicator_view"
11+
android:paddingTop="@dimen/small_spacing"
12+
android:paddingEnd="@dimen/small_spacing"
13+
android:scaleX="0.6"
14+
android:scaleY="0.6"
15+
android:layout_width="wrap_content"
16+
android:layout_height="wrap_content"/>
17+
18+
<androidx.compose.ui.platform.ComposeView
19+
android:id="@+id/compose_preference"
20+
android:layout_width="wrap_content"
21+
android:layout_height="wrap_content" />
22+
23+
</LinearLayout>

app/src/main/res/xml/preferences_privacy.xml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,11 @@
3636
</PreferenceCategory>
3737

3838
<PreferenceCategory android:title="@string/typingIndicators">
39-
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
39+
<org.thoughtcrime.securesms.components.TypingIndicatorPreferenceCompat
4040
android:defaultValue="false"
4141
android:key="pref_typing_indicators"
4242
android:title="@string/typingIndicators"
4343
android:summary="@string/typingIndicatorsDescription" />
44-
<!-- TODO ACL: Need to show a live typing indicator here! -->
45-
4644
</PreferenceCategory>
4745

4846
<PreferenceCategory android:title="@string/linkPreviews">

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ assertjCoreVersion = "3.27.6"
1010
biometricVersion = "1.1.0"
1111
cameraCamera2Version = "1.5.0"
1212
cardviewVersion = "1.0.0"
13-
composeBomVersion = "2025.09.01"
13+
composeBomVersion = "2025.10.01"
1414
conscryptAndroidVersion = "2.5.3"
1515
conscryptJavaVersion = "2.5.2"
1616
constraintlayoutVersion = "2.2.1"

0 commit comments

Comments
 (0)