Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,6 +101,7 @@ class ReceivedMessageHandler @Inject constructor(
@param:ManagerScope private val scope: CoroutineScope,
private val configFactory: ConfigFactoryProtocol,
private val messageRequestResponseHandler: Provider<MessageRequestResponseHandler>,
private val prefs: TextSecurePreferences,
) {

suspend fun handle(
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ class ConversationViewModel @AssistedInject constructor(
it.currentUserRole in EnumSet.of(GroupMemberRole.ADMIN, GroupMemberRole.HIDDEN_ADMIN)
}

val canModerate: StateFlow<Boolean> = recipientFlow.mapStateFlow(viewModelScope) {
it.currentUserRole.canModerate
}

private val _searchOpened = MutableStateFlow(false)

val appBarData: StateFlow<ConversationAppBarData> = combine(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ class TypingIndicatorViewContainer : LinearLayout {
}

fun setTypists(typists: List<Address>) {
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()
}
90 changes: 66 additions & 24 deletions app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -163,32 +167,42 @@ fun QRScannerScreen(

@Composable
fun ScanQrCode(errors: Flow<String>, 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()

Expand Down Expand Up @@ -224,12 +238,24 @@ fun ScanQrCode(errors: Flow<String>, 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)
Expand All @@ -238,6 +264,22 @@ fun ScanQrCode(errors: Flow<String>, 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)
}
}
)
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/res/layout/typing_indicator_preference.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_root"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">

<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorViewContainer
android:id="@+id/pref_typing_indicator_view"
android:paddingTop="@dimen/small_spacing"
android:paddingEnd="@dimen/small_spacing"
android:scaleX="0.6"
android:scaleY="0.6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_preference"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</LinearLayout>
4 changes: 1 addition & 3 deletions app/src/main/res/xml/preferences_privacy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,11 @@
</PreferenceCategory>

<PreferenceCategory android:title="@string/typingIndicators">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
<org.thoughtcrime.securesms.components.TypingIndicatorPreferenceCompat
android:defaultValue="false"
android:key="pref_typing_indicators"
android:title="@string/typingIndicators"
android:summary="@string/typingIndicatorsDescription" />
<!-- TODO ACL: Need to show a live typing indicator here! -->

</PreferenceCategory>

<PreferenceCategory android:title="@string/linkPreviews">
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down