diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c04179745f5..d240aed87da 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -85,18 +85,26 @@
android:name=".ui.AppLockActivity"
android:exported="true"
android:hardwareAccelerated="true"
- android:launchMode="singleTask"
+ android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppTheme" />
+
+
+
diff --git a/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt b/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt
index 5399d72263c..facbccec657 100644
--- a/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt
+++ b/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt
@@ -27,13 +27,3 @@ object SlideNavigationAnimation : WireDestinationStyleAnimated {
object PopUpNavigationAnimation : WireDestinationStyleAnimated {
override fun animationType(): TransitionAnimationType = TransitionAnimationType.POP_UP
}
-
-object WakeUpScreenPopUpNavigationAnimation : WireDestinationStyleAnimated, ScreenModeStyle {
- override fun animationType(): TransitionAnimationType = TransitionAnimationType.POP_UP
- override fun screenMode(): ScreenMode = ScreenMode.WAKE_UP
-}
-
-object KeepOnScreenPopUpNavigationAnimation : WireDestinationStyleAnimated, ScreenModeStyle {
- override fun animationType(): TransitionAnimationType = TransitionAnimationType.POP_UP
- override fun screenMode(): ScreenMode = ScreenMode.KEEP_ON
-}
diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt
index d5d4077830a..c187e94eeac 100644
--- a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt
+++ b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt
@@ -51,7 +51,7 @@ fun getActionReply(
fun getOpenIncomingCallAction(context: Context, conversationId: String, userId: String) = getAction(
context.getString(R.string.notification_action_open_call),
- openIncomingCallPendingIntent(context, conversationId, userId)
+ fullScreenIncomingCallPendingIntent(context, conversationId, userId)
)
fun getDeclineCallAction(context: Context, conversationId: String, userId: String) = getAction(
diff --git a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt
index 7c0c8e91130..2e207272458 100644
--- a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt
+++ b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt
@@ -28,6 +28,8 @@ import com.wire.android.notification.broadcastreceivers.CallNotificationDismissR
import com.wire.android.notification.broadcastreceivers.EndOngoingCallReceiver
import com.wire.android.notification.broadcastreceivers.NotificationReplyReceiver
import com.wire.android.ui.WireActivity
+import com.wire.android.ui.calling.CallActivity
+import com.wire.android.ui.calling.CallScreenType
import com.wire.android.util.deeplink.DeepLinkProcessor
fun messagePendingIntent(context: Context, conversationId: String, userId: String?): PendingIntent {
@@ -81,17 +83,6 @@ fun replyMessagePendingIntent(context: Context, conversationId: String, userId:
PendingIntent.FLAG_MUTABLE
)
-fun openIncomingCallPendingIntent(context: Context, conversationId: String, userId: String): PendingIntent {
- val intent = openIncomingCallIntent(context, conversationId, userId)
-
- return PendingIntent.getActivity(
- context.applicationContext,
- OPEN_INCOMING_CALL_REQUEST_CODE,
- intent,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- )
-}
-
fun openOngoingCallPendingIntent(context: Context, conversationId: String): PendingIntent {
val intent = openOngoingCallIntent(context, conversationId)
@@ -132,27 +123,21 @@ fun fullScreenIncomingCallPendingIntent(context: Context, conversationId: String
context,
FULL_SCREEN_REQUEST_CODE,
intent,
- PendingIntent.FLAG_IMMUTABLE
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
private fun openIncomingCallIntent(context: Context, conversationId: String, userId: String) =
- Intent(context.applicationContext, WireActivity::class.java).apply {
- data = Uri.Builder()
- .scheme(DeepLinkProcessor.DEEP_LINK_SCHEME)
- .authority(DeepLinkProcessor.INCOMING_CALL_DEEPLINK_HOST)
- .appendPath(conversationId)
- .appendQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM, userId)
- .build()
+ Intent(context.applicationContext, CallActivity::class.java).apply {
+ putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId)
+ putExtra(CallActivity.EXTRA_USER_ID, userId)
+ putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Incoming.name)
}
private fun openOngoingCallIntent(context: Context, conversationId: String) =
- Intent(context.applicationContext, WireActivity::class.java).apply {
- data = Uri.Builder()
- .scheme(DeepLinkProcessor.DEEP_LINK_SCHEME)
- .authority(DeepLinkProcessor.ONGOING_CALL_DEEPLINK_HOST)
- .appendPath(conversationId)
- .build()
+ Intent(context.applicationContext, CallActivity::class.java).apply {
+ putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId)
+ putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Ongoing.name)
}
private fun openMigrationLoginIntent(context: Context, userHandle: String) =
@@ -188,14 +173,12 @@ fun openAppPendingIntent(context: Context): PendingIntent {
private const val MESSAGE_NOTIFICATIONS_SUMMARY_REQUEST_CODE = 0
private const val DECLINE_CALL_REQUEST_CODE = "decline_call_"
-private const val OPEN_INCOMING_CALL_REQUEST_CODE = 2
private const val FULL_SCREEN_REQUEST_CODE = 3
private const val OPEN_ONGOING_CALL_REQUEST_CODE = 4
private const val OPEN_MIGRATION_LOGIN_REQUEST_CODE = 5
private const val END_ONGOING_CALL_REQUEST_CODE = "hang_up_call_"
private const val OPEN_MESSAGE_REQUEST_CODE_PREFIX = "open_message_"
private const val OPEN_OTHER_USER_PROFILE_CODE_PREFIX = "open_other_user_profile_"
-private const val CALL_REQUEST_CODE_PREFIX = "call_"
private const val REPLY_MESSAGE_REQUEST_CODE_PREFIX = "reply_"
private fun getRequestCode(conversationId: String, prefix: String): Int = (prefix + conversationId).hashCode()
diff --git a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt
index 7bf369a96ff..69f29465d36 100644
--- a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt
+++ b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt
@@ -305,7 +305,6 @@ class WireNotificationManager @Inject constructor(
when (screens) {
is CurrentScreen.Conversation -> messagesNotificationManager.hideNotification(screens.id, userId)
is CurrentScreen.OtherUserProfile -> messagesNotificationManager.hideNotification(screens.id, userId)
- is CurrentScreen.IncomingCallScreen -> callNotificationManager.hideIncomingCallNotification()
else -> {}
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt
index 47769edce5f..7add69f70fb 100644
--- a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt
@@ -74,6 +74,7 @@ class AppLockActivity : AppCompatActivity() {
}
}
}
+
companion object {
const val SET_TEAM_APP_LOCK = "set_team_app_lock"
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
index a8fe5eba5c6..a1e5dae3b7f 100644
--- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
@@ -62,7 +62,7 @@ import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.NavigationGraph
import com.wire.android.navigation.navigateToItem
import com.wire.android.navigation.rememberNavigator
-import com.wire.android.ui.calling.ProximitySensorManager
+import com.wire.android.ui.calling.getOngoingCallIntent
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.common.topappbar.CommonTopAppBar
import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel
@@ -72,10 +72,8 @@ import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.ImportMediaScreenDestination
-import com.wire.android.ui.destinations.IncomingCallScreenDestination
import com.wire.android.ui.destinations.LoginScreenDestination
import com.wire.android.ui.destinations.MigrationScreenDestination
-import com.wire.android.ui.destinations.OngoingCallScreenDestination
import com.wire.android.ui.destinations.OtherUserProfileScreenDestination
import com.wire.android.ui.destinations.SelfDevicesScreenDestination
import com.wire.android.ui.destinations.SelfUserProfileScreenDestination
@@ -103,7 +101,6 @@ import com.wire.android.util.SyncStateObserver
import com.wire.android.util.debug.FeatureVisibilityFlags
import com.wire.android.util.debug.LocalFeatureVisibilityFlags
import com.wire.android.util.deeplink.DeepLinkResult
-import com.wire.android.util.ui.updateScreenSettings
import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
@@ -122,9 +119,6 @@ class WireActivity : AppCompatActivity() {
@Inject
lateinit var currentScreenManager: CurrentScreenManager
- @Inject
- lateinit var proximitySensorManager: ProximitySensorManager
-
@Inject
lateinit var lockCodeTimeManager: Lazy
@@ -154,9 +148,6 @@ class WireActivity : AppCompatActivity() {
lifecycle.addObserver(currentScreenManager)
WindowCompat.setDecorFitsSystemWindows(window, false)
- appLogger.i("$TAG proximity sensor")
- proximitySensorManager.initialize()
-
lifecycleScope.launch(Dispatchers.Default) {
appLogger.i("$TAG persistent connection status")
@@ -219,7 +210,9 @@ class WireActivity : AppCompatActivity() {
CommonTopAppBar(
commonTopAppBarState = commonTopAppBarViewModel.state,
onReturnToCallClick = { establishedCall ->
- navigator.navigate(NavigationCommand(OngoingCallScreenDestination(establishedCall.conversationId)))
+ getOngoingCallIntent(this@WireActivity, establishedCall.conversationId.toString()).run {
+ startActivity(this)
+ }
},
)
CompositionLocalProvider(LocalNavigator provides navigator) {
@@ -263,7 +256,6 @@ class WireActivity : AppCompatActivity() {
DisposableEffect(navController) {
val updateScreenSettingsListener = NavController.OnDestinationChangedListener { _, navDestination, _ ->
currentKeyboardController?.hide()
- updateScreenSettings(navDestination)
}
navController.addOnDestinationChangedListener(updateScreenSettingsListener)
navController.addOnDestinationChangedListener(currentScreenManager)
@@ -480,13 +472,6 @@ class WireActivity : AppCompatActivity() {
}
}
}
-
- proximitySensorManager.registerListener()
- }
-
- override fun onPause() {
- super.onPause()
- proximitySensorManager.unRegisterListener()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -532,15 +517,6 @@ class WireActivity : AppCompatActivity() {
// do nothing, already handled in ViewModel
}
- is DeepLinkResult.IncomingCall -> {
- if (result.switchedAccount) navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE))
- navigate(NavigationCommand(IncomingCallScreenDestination(result.conversationsId)))
- }
-
- is DeepLinkResult.OngoingCall -> {
- navigate(NavigationCommand(OngoingCallScreenDestination(result.conversationsId)))
- }
-
is DeepLinkResult.OpenConversation -> {
if (result.switchedAccount) navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE))
navigate(NavigationCommand(ConversationScreenDestination(result.conversationsId), BackStackMode.UPDATE_EXISTED))
diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
index 12ba8586963..79ae1d5e2fc 100644
--- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
@@ -486,7 +486,6 @@ class WireActivityViewModel @Inject constructor(
CurrentScreen.InBackground,
is CurrentScreen.Conversation,
CurrentScreen.Home,
- is CurrentScreen.CallScreen,
is CurrentScreen.OtherUserProfile,
CurrentScreen.AuthRelated,
CurrentScreen.SomeOther -> true
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt
new file mode 100644
index 00000000000..6dfd4a3333a
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt
@@ -0,0 +1,174 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.calling
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.togetherWith
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.core.view.WindowCompat
+import com.wire.android.appLogger
+import com.wire.android.navigation.style.TransitionAnimationType
+import com.wire.android.notification.CallNotificationManager
+import com.wire.android.ui.AppLockActivity
+import com.wire.android.ui.LocalActivity
+import com.wire.android.ui.calling.incoming.IncomingCallScreen
+import com.wire.android.ui.calling.initiating.InitiatingCallScreen
+import com.wire.android.ui.calling.ongoing.OngoingCallScreen
+import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
+import com.wire.android.ui.theme.WireTheme
+import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class CallActivity : AppCompatActivity() {
+
+ @Inject
+ lateinit var callNotificationManager: CallNotificationManager
+
+ @Inject
+ lateinit var proximitySensorManager: ProximitySensorManager
+
+ private val qualifiedIdMapper = QualifiedIdMapperImpl(null)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ callNotificationManager.hideAllNotifications()
+
+ setUpCallingFlags()
+
+ appLogger.i("$TAG Initializing proximity sensor..")
+ proximitySensorManager.initialize()
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ val conversationId = intent.extras?.getString(EXTRA_CONVERSATION_ID)
+ val screenType = intent.extras?.getString(EXTRA_SCREEN_TYPE)
+
+ setContent {
+ val snackbarHostState = remember { SnackbarHostState() }
+ CompositionLocalProvider(
+ LocalSnackbarHostState provides snackbarHostState,
+ LocalActivity provides this
+ ) {
+ WireTheme {
+ var currentCallScreenType by remember { mutableStateOf(screenType) }
+ currentCallScreenType?.let { currentScreenType ->
+ AnimatedContent(
+ targetState = currentScreenType,
+ transitionSpec = {
+ TransitionAnimationType.POP_UP.enterTransition.togetherWith(
+ TransitionAnimationType.POP_UP.exitTransition
+ )
+ },
+ label = currentScreenType
+ ) { screenType ->
+ conversationId?.let {
+ when (screenType) {
+ CallScreenType.Initiating.name -> InitiatingCallScreen(
+ qualifiedIdMapper.fromStringToQualifiedID(it)
+ ) {
+ currentCallScreenType = CallScreenType.Ongoing.name
+ }
+
+ CallScreenType.Ongoing.name -> OngoingCallScreen(
+ qualifiedIdMapper.fromStringToQualifiedID(it)
+ )
+
+ CallScreenType.Incoming.name -> IncomingCallScreen(
+ qualifiedIdMapper.fromStringToQualifiedID(it)
+ ) {
+ currentCallScreenType = CallScreenType.Ongoing.name
+ }
+ }
+ }
+ }
+ } ?: run {
+ finish()
+ }
+ }
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ proximitySensorManager.registerListener()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ proximitySensorManager.unRegisterListener()
+ }
+
+ companion object {
+ private const val TAG = "CallActivity"
+ const val EXTRA_CONVERSATION_ID = "conversation_id"
+ const val EXTRA_USER_ID = "user_id"
+ const val EXTRA_SCREEN_TYPE = "screen_type"
+ }
+}
+
+fun CallActivity.setUpCallingFlags() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true)
+ setTurnScreenOn(true)
+ } else {
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON,
+ )
+ }
+}
+
+fun getOngoingCallIntent(
+ activity: Activity,
+ conversationId: String
+) = Intent(activity, CallActivity::class.java).apply {
+ putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId)
+ putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Ongoing.name)
+}
+
+fun getInitiatingCallIntent(
+ activity: Activity,
+ conversationId: String
+) = Intent(activity, CallActivity::class.java).apply {
+ putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId)
+ putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Initiating.name)
+}
+
+fun CallActivity.openAppLockActivity() {
+ Intent(this, AppLockActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
+ }.run {
+ startActivity(this)
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallingNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt
similarity index 85%
rename from app/src/main/kotlin/com/wire/android/ui/calling/CallingNavArgs.kt
rename to app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt
index c8892b46466..e5b43bf08ca 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/CallingNavArgs.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt
@@ -15,10 +15,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
-package com.wire.android.ui.calling
-import com.wire.kalium.logic.data.id.ConversationId
+package com.wire.android.ui.calling
-data class CallingNavArgs(
- val conversationId: ConversationId
-)
+enum class CallScreenType {
+ Incoming,
+ Ongoing,
+ Initiating
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt
index 26d101a46e1..c3d0c3ddf82 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt
@@ -22,7 +22,6 @@ import android.view.View
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.appLogger
@@ -30,7 +29,6 @@ import com.wire.android.mapper.UICallParticipantMapper
import com.wire.android.mapper.UserTypeMapper
import com.wire.android.media.CallRinger
import com.wire.android.model.ImageAsset
-import com.wire.android.ui.navArgs
import com.wire.android.util.CurrentScreen
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.dispatchers.DispatcherProvider
@@ -41,7 +39,7 @@ import com.wire.kalium.logic.data.call.ConversationType
import com.wire.kalium.logic.data.call.VideoState
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.ConversationDetails
-import com.wire.kalium.logic.data.id.QualifiedID
+import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase
import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase
import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase
@@ -55,6 +53,9 @@ import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase
import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase
import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase
import com.wire.kalium.logic.util.PlatformView
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharedFlow
@@ -66,12 +67,11 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
-import javax.inject.Inject
@Suppress("LongParameterList", "TooManyFunctions")
-@HiltViewModel
-class SharedCallingViewModel @Inject constructor(
- savedStateHandle: SavedStateHandle,
+@HiltViewModel(assistedFactory = SharedCallingViewModel.Factory::class)
+class SharedCallingViewModel @AssistedInject constructor(
+ @Assisted val conversationId: ConversationId,
private val conversationDetails: ObserveConversationDetailsUseCase,
private val allCalls: GetAllCallsWithSortedParticipantsUseCase,
private val endCall: EndCallUseCase,
@@ -92,9 +92,6 @@ class SharedCallingViewModel @Inject constructor(
private val dispatchers: DispatcherProvider
) : ViewModel() {
- private val callingNavArgs: CallingNavArgs = savedStateHandle.navArgs()
- val conversationId: QualifiedID = callingNavArgs.conversationId
-
var callState by mutableStateOf(CallState(conversationId))
init {
@@ -302,4 +299,9 @@ class SharedCallingViewModel @Inject constructor(
setVideoPreview(conversationId, PlatformView(view))
}
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(conversationId: ConversationId): SharedCallingViewModel
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt
index 83bbc428d0a..a9b5baf3c37 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt
@@ -71,7 +71,7 @@ fun CallerDetails(
proteusVerificationStatus: Conversation.VerificationStatus?,
) {
Column(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier.fillMaxSize().padding(top = dimensions().spacing32x),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt
index b222ffe6876..6968d6dd00d 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt
@@ -36,29 +36,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
-import com.ramcosta.composedestinations.annotation.Destination
-import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.wire.android.R
import com.wire.android.appLogger
-import com.wire.android.navigation.BackStackMode
-import com.wire.android.navigation.NavigationCommand
-import com.wire.android.navigation.Navigator
-import com.wire.android.navigation.style.WakeUpScreenPopUpNavigationAnimation
+import com.wire.android.ui.LocalActivity
+import com.wire.android.ui.calling.CallActivity
import com.wire.android.ui.calling.CallState
-import com.wire.android.ui.calling.CallingNavArgs
import com.wire.android.ui.calling.SharedCallingViewModel
import com.wire.android.ui.calling.common.CallVideoPreview
import com.wire.android.ui.calling.common.CallerDetails
import com.wire.android.ui.calling.controlbuttons.AcceptButton
import com.wire.android.ui.calling.controlbuttons.CallOptionsControls
import com.wire.android.ui.calling.controlbuttons.HangUpButton
+import com.wire.android.ui.calling.openAppLockActivity
import com.wire.android.ui.common.bottomsheet.WireBottomSheetScaffold
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog
import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.visbility.rememberVisibilityState
-import com.wire.android.ui.destinations.OngoingCallScreenDestination
import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.permission.PermissionDenialType
@@ -66,21 +61,29 @@ import com.wire.android.util.permission.rememberCallingRecordAudioRequestFlow
import com.wire.kalium.logic.data.call.ConversationType
import com.wire.kalium.logic.data.id.ConversationId
-@RootNavGraph
-@Destination(
- navArgsDelegate = CallingNavArgs::class,
- style = WakeUpScreenPopUpNavigationAnimation::class
-)
+@Suppress("ParameterWrapping")
@Composable
fun IncomingCallScreen(
- navigator: Navigator,
- sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(),
- incomingCallViewModel: IncomingCallViewModel = hiltViewModel()
+ conversationId: ConversationId,
+ incomingCallViewModel: IncomingCallViewModel = hiltViewModel(
+ creationCallback = { factory -> factory.create(conversationId = conversationId) }
+ ),
+ sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(
+ creationCallback = { factory -> factory.create(conversationId = conversationId) }
+ ),
+ onCallAccepted: () -> Unit
) {
- val permissionPermanentlyDeniedDialogState = rememberVisibilityState()
+ val activity = LocalActivity.current
+
+ val permissionPermanentlyDeniedDialogState =
+ rememberVisibilityState()
val audioPermissionCheck = AudioPermissionCheckFlow(
- onAcceptCall = incomingCallViewModel::acceptCall,
+ onAcceptCall = {
+ incomingCallViewModel.acceptCall {
+ (activity as CallActivity).openAppLockActivity()
+ }
+ },
onPermanentPermissionDecline = {
permissionPermanentlyDeniedDialogState.show(
PermissionPermanentlyDeniedDialogState.Visible(
@@ -95,19 +98,23 @@ fun IncomingCallScreen(
if (incomingCallState.shouldShowJoinCallAnywayDialog) {
JoinAnywayDialog(
onDismiss = ::dismissJoinCallAnywayDialog,
- onConfirm = ::acceptCallAnyway
+ onConfirm = {
+ acceptCallAnyway {
+ (activity as CallActivity).openAppLockActivity()
+ }
+ }
)
}
}
LaunchedEffect(incomingCallViewModel.incomingCallState.flowState) {
- when (val flowState = incomingCallViewModel.incomingCallState.flowState) {
- is IncomingCallState.FlowState.CallClosed -> navigator.navigateBack()
- is IncomingCallState.FlowState.CallAccepted -> navigator.navigate(
- NavigationCommand(
- OngoingCallScreenDestination(flowState.conversationId),
- BackStackMode.REMOVE_CURRENT_AND_REPLACE
- )
- )
+ when (incomingCallViewModel.incomingCallState.flowState) {
+ is IncomingCallState.FlowState.CallClosed -> {
+ activity.finish()
+ }
+
+ is IncomingCallState.FlowState.CallAccepted -> {
+ onCallAccepted()
+ }
is IncomingCallState.FlowState.Default -> { /* do nothing */
}
@@ -119,7 +126,16 @@ fun IncomingCallScreen(
toggleMute = { sharedCallingViewModel.toggleMute(true) },
toggleSpeaker = ::toggleSpeaker,
toggleVideo = ::toggleVideo,
- declineCall = incomingCallViewModel::declineCall,
+ declineCall = {
+ incomingCallViewModel.declineCall(
+ onAppLocked = {
+ (activity as CallActivity).openAppLockActivity()
+ },
+ onCallRejected = {
+ activity.finish()
+ }
+ )
+ },
acceptCall = audioPermissionCheck::launch,
onVideoPreviewCreated = ::setVideoPreview,
onSelfClearVideoPreview = ::clearVideoPreview,
@@ -266,5 +282,15 @@ fun AudioPermissionCheckFlow(
@Preview
@Composable
fun PreviewIncomingCallScreen() {
- IncomingCallContent(CallState(ConversationId("value", "domain")), {}, {}, {}, {}, {}, {}, {}, {})
+ IncomingCallContent(
+ callState = CallState(ConversationId("value", "domain")),
+ toggleMute = { },
+ toggleSpeaker = { },
+ toggleVideo = { },
+ declineCall = { },
+ acceptCall = { },
+ onVideoPreviewCreated = { },
+ onSelfClearVideoPreview = { },
+ onPermissionPermanentlyDenied = { },
+ )
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt
index ddf6c5b3e8f..3b91915fe63 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt
@@ -21,32 +21,32 @@ package com.wire.android.ui.calling.incoming
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.R
import com.wire.android.media.CallRinger
-import com.wire.android.ui.calling.CallingNavArgs
-import com.wire.android.ui.navArgs
+import com.wire.android.ui.home.appLock.LockCodeTimeManager
import com.wire.kalium.logic.data.id.ConversationId
-import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase
import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase
import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase
import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase
import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.call.usecase.RejectCallUseCase
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
-import javax.inject.Inject
@Suppress("LongParameterList")
-@HiltViewModel
-class IncomingCallViewModel @Inject constructor(
- savedStateHandle: SavedStateHandle,
+@HiltViewModel(assistedFactory = IncomingCallViewModel.Factory::class)
+class IncomingCallViewModel @AssistedInject constructor(
+ @Assisted val conversationId: ConversationId,
private val incomingCalls: GetIncomingCallsUseCase,
private val rejectCall: RejectCallUseCase,
private val acceptCall: AnswerCallUseCase,
@@ -54,11 +54,9 @@ class IncomingCallViewModel @Inject constructor(
private val muteCall: MuteCallUseCase,
private val observeEstablishedCalls: ObserveEstablishedCallsUseCase,
private val endCall: EndCallUseCase,
+ private val lockCodeTimeManager: LockCodeTimeManager
) : ViewModel() {
- private val incomingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs()
- private val conversationId: QualifiedID = incomingCallNavArgs.conversationId
-
private lateinit var observeIncomingCallJob: Job
private var establishedCallConversationId: ConversationId? = null
@@ -94,19 +92,31 @@ class IncomingCallViewModel @Inject constructor(
calls.find { call -> call.conversationId == conversationId }.also {
if (it == null) {
callRinger.stop()
- incomingCallState = incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed)
+ incomingCallState =
+ incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed)
}
}
}
}
- fun declineCall() {
+ fun declineCall(
+ onAppLocked: () -> Unit,
+ onCallRejected: () -> Unit
+ ) {
viewModelScope.launch {
- observeIncomingCallJob.cancel()
- launch { rejectCall(conversationId = conversationId) }
- launch {
- callRinger.stop()
- incomingCallState = incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed)
+ lockCodeTimeManager.observeAppLock().first().let {
+ if (it) {
+ onAppLocked()
+ } else {
+ observeIncomingCallJob.cancel()
+ launch { rejectCall(conversationId = conversationId) }
+ launch {
+ callRinger.stop()
+ incomingCallState =
+ incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed)
+ }
+ onCallRejected()
+ }
}
}
}
@@ -119,30 +129,44 @@ class IncomingCallViewModel @Inject constructor(
incomingCallState = incomingCallState.copy(shouldShowJoinCallAnywayDialog = false)
}
- fun acceptCallAnyway() {
+ fun acceptCallAnyway(onAppLocked: () -> Unit) {
viewModelScope.launch {
- establishedCallConversationId?.let {
- endCall(it)
- // we need to update mute state to false, so if the user re-join the call te mic will will be muted
- muteCall(it, false)
- delay(DELAY_END_CALL)
+ lockCodeTimeManager.observeAppLock().first().let {
+ if (it) {
+ onAppLocked()
+ } else {
+ establishedCallConversationId?.let {
+ endCall(it)
+ // we need to update mute state to false, so if the user re-join the call te mic will will be muted
+ muteCall(it, false)
+ delay(DELAY_END_CALL)
+ }
+ acceptCall(onAppLocked)
+ }
}
- acceptCall()
}
}
- fun acceptCall() {
+ fun acceptCall(onAppLocked: () -> Unit) {
viewModelScope.launch {
- if (incomingCallState.hasEstablishedCall) {
- showJoinCallAnywayDialog()
- } else {
- callRinger.stop()
-
- dismissJoinCallAnywayDialog()
- observeIncomingCallJob.cancel()
-
- acceptCall(conversationId = conversationId)
- incomingCallState = incomingCallState.copy(flowState = IncomingCallState.FlowState.CallAccepted(conversationId))
+ lockCodeTimeManager.observeAppLock().first().let {
+ if (it) {
+ onAppLocked()
+ } else {
+ if (incomingCallState.hasEstablishedCall) {
+ showJoinCallAnywayDialog()
+ } else {
+ callRinger.stop()
+
+ dismissJoinCallAnywayDialog()
+ observeIncomingCallJob.cancel()
+
+ acceptCall(conversationId = conversationId)
+ incomingCallState = incomingCallState.copy(
+ flowState = IncomingCallState.FlowState.CallAccepted(conversationId)
+ )
+ }
+ }
}
}
}
@@ -150,4 +174,9 @@ class IncomingCallViewModel @Inject constructor(
companion object {
const val DELAY_END_CALL = 200L
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(conversationId: ConversationId): IncomingCallViewModel
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt
index 7d6440df304..1bf0dc8da96 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt
@@ -38,15 +38,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import com.ramcosta.composedestinations.annotation.Destination
-import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.wire.android.R
-import com.wire.android.navigation.BackStackMode
-import com.wire.android.navigation.style.KeepOnScreenPopUpNavigationAnimation
-import com.wire.android.navigation.NavigationCommand
-import com.wire.android.navigation.Navigator
+import com.wire.android.ui.LocalActivity
import com.wire.android.ui.calling.CallState
-import com.wire.android.ui.calling.CallingNavArgs
import com.wire.android.ui.calling.SharedCallingViewModel
import com.wire.android.ui.calling.common.CallVideoPreview
import com.wire.android.ui.calling.common.CallerDetails
@@ -56,31 +50,37 @@ import com.wire.android.ui.common.bottomsheet.WireBottomSheetScaffold
import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.visbility.rememberVisibilityState
-import com.wire.android.ui.destinations.OngoingCallScreenDestination
import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.util.permission.PermissionDenialType
import com.wire.kalium.logic.data.id.ConversationId
-@RootNavGraph
-@Destination(
- navArgsDelegate = CallingNavArgs::class,
- style = KeepOnScreenPopUpNavigationAnimation::class
-)
+@Suppress("ParameterWrapping")
@Composable
fun InitiatingCallScreen(
- navigator: Navigator,
- navArgs: CallingNavArgs,
- sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(),
- initiatingCallViewModel: InitiatingCallViewModel = hiltViewModel()
+ conversationId: ConversationId,
+ sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(
+ creationCallback = { factory -> factory.create(conversationId = conversationId) }
+ ),
+ initiatingCallViewModel: InitiatingCallViewModel = hiltViewModel(
+ creationCallback = { factory -> factory.create(conversationId = conversationId) }
+ ),
+ onCallAccepted: () -> Unit
) {
- val permissionPermanentlyDeniedDialogState = rememberVisibilityState()
+ val permissionPermanentlyDeniedDialogState =
+ rememberVisibilityState()
+
+ val activity = LocalActivity.current
LaunchedEffect(initiatingCallViewModel.state.flowState) {
when (initiatingCallViewModel.state.flowState) {
- InitiatingCallState.FlowState.CallClosed -> navigator.navigateBack()
- InitiatingCallState.FlowState.CallEstablished ->
- navigator.navigate(NavigationCommand(OngoingCallScreenDestination(navArgs.conversationId), BackStackMode.REMOVE_CURRENT))
+ InitiatingCallState.FlowState.CallClosed -> {
+ activity.finish()
+ }
+
+ InitiatingCallState.FlowState.CallEstablished -> {
+ onCallAccepted()
+ }
InitiatingCallState.FlowState.Default -> { /* do nothing */
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt
index f57349cbb4e..45f5a109c30 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt
@@ -21,29 +21,28 @@ package com.wire.android.ui.calling.initiating
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.R
import com.wire.android.media.CallRinger
-import com.wire.android.ui.calling.CallingNavArgs
-import com.wire.android.ui.navArgs
-import com.wire.kalium.logic.data.id.QualifiedID
+import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase
import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase
import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.util.Calendar
-import javax.inject.Inject
@Suppress("LongParameterList")
-@HiltViewModel
-class InitiatingCallViewModel @Inject constructor(
- savedStateHandle: SavedStateHandle,
+@HiltViewModel(assistedFactory = InitiatingCallViewModel.Factory::class)
+class InitiatingCallViewModel @AssistedInject constructor(
+ @Assisted val conversationId: ConversationId,
private val observeEstablishedCalls: ObserveEstablishedCallsUseCase,
private val startCall: StartCallUseCase,
private val endCall: EndCallUseCase,
@@ -51,9 +50,6 @@ class InitiatingCallViewModel @Inject constructor(
private val callRinger: CallRinger
) : ViewModel() {
- private val initiatingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs()
- private val conversationId: QualifiedID = initiatingCallNavArgs.conversationId
-
private val callStartTime: Long = Calendar.getInstance().timeInMillis
private var wasCallHangUp: Boolean = false
@@ -108,7 +104,11 @@ class InitiatingCallViewModel @Inject constructor(
conversationId = conversationId
)
when (result) {
- StartCallUseCase.Result.Success -> callRinger.ring(resource = R.raw.ringing_from_me, isIncomingCall = false)
+ StartCallUseCase.Result.Success -> callRinger.ring(
+ resource = R.raw.ringing_from_me,
+ isIncomingCall = false
+ )
+
StartCallUseCase.Result.SyncFailure -> {} // TODO: handle case where start call fails
}
}
@@ -120,4 +120,9 @@ class InitiatingCallViewModel @Inject constructor(
state = state.copy(flowState = InitiatingCallState.FlowState.CallClosed)
}
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(conversationId: ConversationId): InitiatingCallViewModel
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt
index 9b7ad29ce64..90cfd276271 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt
@@ -51,12 +51,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import com.ramcosta.composedestinations.annotation.Destination
-import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.wire.android.R
-import com.wire.android.navigation.Navigator
-import com.wire.android.navigation.style.WakeUpScreenPopUpNavigationAnimation
-import com.wire.android.ui.calling.CallingNavArgs
+import com.wire.android.ui.LocalActivity
import com.wire.android.ui.calling.ConversationName
import com.wire.android.ui.calling.SharedCallingViewModel
import com.wire.android.ui.calling.controlbuttons.CameraButton
@@ -89,27 +85,33 @@ import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
import java.util.Locale
-@RootNavGraph
-@Destination(
- navArgsDelegate = CallingNavArgs::class,
- style = WakeUpScreenPopUpNavigationAnimation::class
-)
+@Suppress("ParameterWrapping")
@Composable
fun OngoingCallScreen(
- navigator: Navigator,
- ongoingCallViewModel: OngoingCallViewModel = hiltViewModel(),
- sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(),
+ conversationId: ConversationId,
+ ongoingCallViewModel: OngoingCallViewModel = hiltViewModel(
+ creationCallback = { factory -> factory.create(conversationId = conversationId) }
+ ),
+ sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(
+ creationCallback = { factory -> factory.create(conversationId = conversationId) }
+ )
) {
val permissionPermanentlyDeniedDialogState =
rememberVisibilityState()
+ val activity = LocalActivity.current
+
LaunchedEffect(ongoingCallViewModel.state.flowState) {
when (ongoingCallViewModel.state.flowState) {
- OngoingCallState.FlowState.CallClosed -> navigator.navigateBack()
+ OngoingCallState.FlowState.CallClosed -> {
+ activity.finish()
+ }
+
OngoingCallState.FlowState.Default -> { /* do nothing */
}
}
}
+
with(sharedCallingViewModel.callState) {
OngoingCallContent(
conversationId = conversationId,
@@ -126,7 +128,7 @@ fun OngoingCallScreen(
shouldShowDoubleTapToast = ongoingCallViewModel.shouldShowDoubleTapToast,
toggleSpeaker = sharedCallingViewModel::toggleSpeaker,
toggleMute = sharedCallingViewModel::toggleMute,
- hangUpCall = { sharedCallingViewModel.hangUpCall(navigator::navigateBack) },
+ hangUpCall = { sharedCallingViewModel.hangUpCall { activity.finish() } },
toggleVideo = sharedCallingViewModel::toggleVideo,
flipCamera = sharedCallingViewModel::flipCamera,
setVideoPreview = {
@@ -137,7 +139,7 @@ fun OngoingCallScreen(
sharedCallingViewModel.clearVideoPreview()
ongoingCallViewModel.stopSendingVideoFeed()
},
- navigateBack = navigator::navigateBack,
+ navigateBack = { activity.finish() },
requestVideoStreams = ongoingCallViewModel::requestVideoStreams,
hideDoubleTapToast = ongoingCallViewModel::hideDoubleTapToast,
onPermissionPermanentlyDenied = {
@@ -151,7 +153,9 @@ fun OngoingCallScreen(
}
}
)
- BackHandler(enabled = isCameraOn, navigator::navigateBack)
+ BackHandler {
+ activity.finish()
+ }
}
PermissionPermanentlyDeniedDialog(
diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt
index 9b3abb360b7..b61a23d7060 100644
--- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt
@@ -22,36 +22,36 @@ import android.os.CountDownTimer
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.appLogger
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.CurrentAccount
-import com.wire.android.ui.calling.CallingNavArgs
import com.wire.android.ui.calling.model.UICallParticipant
-import com.wire.android.ui.navArgs
import com.wire.android.util.CurrentScreen
import com.wire.android.util.CurrentScreenManager
import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.call.CallClient
import com.wire.kalium.logic.data.call.VideoState
-import com.wire.kalium.logic.data.id.QualifiedID
+import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase
import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
-import javax.inject.Inject
@Suppress("LongParameterList")
-@HiltViewModel
-class OngoingCallViewModel @Inject constructor(
- savedStateHandle: SavedStateHandle,
+@HiltViewModel(assistedFactory = OngoingCallViewModel.Factory::class)
+class OngoingCallViewModel @AssistedInject constructor(
+ @Assisted
+ val conversationId: ConversationId,
@CurrentAccount
private val currentUserId: UserId,
private val globalDataStore: GlobalDataStore,
@@ -60,10 +60,6 @@ class OngoingCallViewModel @Inject constructor(
private val setVideoSendState: SetVideoSendStateUseCase,
private val currentScreenManager: CurrentScreenManager
) : ViewModel() {
-
- private val ongoingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs()
- private val conversationId: QualifiedID = ongoingCallNavArgs.conversationId
-
var shouldShowDoubleTapToast: Boolean by mutableStateOf(false)
private set
private var doubleTapIndicatorCountDownTimer: CountDownTimer? = null
@@ -98,6 +94,7 @@ class OngoingCallViewModel @Inject constructor(
setVideoSendState(conversationId, VideoState.STARTED)
}
}
+
fun stopSendingVideoFeed() {
viewModelScope.launch {
setVideoSendState(conversationId, VideoState.STOPPED)
@@ -109,10 +106,10 @@ class OngoingCallViewModel @Inject constructor(
.distinctUntilChanged()
.collect { calls ->
val currentCall = calls.find { call -> call.conversationId == conversationId }
- val currentScreen = currentScreenManager.observeCurrentScreen(viewModelScope).first()
- val isCurrentlyOnOngoingScreen = currentScreen is CurrentScreen.OngoingCallScreen
+ val currentScreen =
+ currentScreenManager.observeCurrentScreen(viewModelScope).first()
val isOnBackground = currentScreen is CurrentScreen.InBackground
- if (currentCall == null && (isCurrentlyOnOngoingScreen || isOnBackground)) {
+ if (currentCall == null && isOnBackground) {
state = state.copy(flowState = OngoingCallState.FlowState.CallClosed)
}
}
@@ -137,25 +134,30 @@ class OngoingCallViewModel @Inject constructor(
private fun startDoubleTapToastDisplayCountDown() {
doubleTapIndicatorCountDownTimer?.cancel()
- doubleTapIndicatorCountDownTimer = object : CountDownTimer(DOUBLE_TAP_TOAST_DISPLAY_TIME, COUNT_DOWN_INTERVAL) {
- override fun onTick(p0: Long) {
- appLogger.i("startDoubleTapToastDisplayCountDown: $p0")
- }
+ doubleTapIndicatorCountDownTimer =
+ object : CountDownTimer(DOUBLE_TAP_TOAST_DISPLAY_TIME, COUNT_DOWN_INTERVAL) {
+ override fun onTick(p0: Long) {
+ appLogger.i("startDoubleTapToastDisplayCountDown: $p0")
+ }
- override fun onFinish() {
- shouldShowDoubleTapToast = false
- viewModelScope.launch {
- globalDataStore.setShouldShowDoubleTapToastStatus(currentUserId.toString(), false)
+ override fun onFinish() {
+ shouldShowDoubleTapToast = false
+ viewModelScope.launch {
+ globalDataStore.setShouldShowDoubleTapToastStatus(
+ currentUserId.toString(),
+ false
+ )
+ }
}
}
- }
doubleTapIndicatorCountDownTimer?.start()
}
private fun showDoubleTapToast() {
viewModelScope.launch {
delay(DELAY_TO_SHOW_DOUBLE_TAP_TOAST)
- shouldShowDoubleTapToast = globalDataStore.getShouldShowDoubleTapToast(currentUserId.toString())
+ shouldShowDoubleTapToast =
+ globalDataStore.getShouldShowDoubleTapToast(currentUserId.toString())
if (shouldShowDoubleTapToast) {
startDoubleTapToastDisplayCountDown()
}
@@ -174,4 +176,9 @@ class OngoingCallViewModel @Inject constructor(
const val COUNT_DOWN_INTERVAL = 1000L
const val DELAY_TO_SHOW_DOUBLE_TAP_TOAST = 500L
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(conversationId: ConversationId): OngoingCallViewModel
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt b/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt
index 1e1a8fe5006..dab64da1e8e 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt
@@ -18,8 +18,6 @@
package com.wire.android.ui.common
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -40,7 +38,6 @@ import androidx.lifecycle.flowWithLifecycle
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.placeholder
import com.google.accompanist.placeholder.shimmer
-import com.wire.android.model.Clickable
import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
@@ -82,20 +79,6 @@ fun Modifier.shimmerPlaceholder(
shape = shape,
)
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun Modifier.clickable(clickable: Clickable?) = clickable?.let {
- val onClick = rememberClickBlockAction(clickable.clickBlockParams, clickable.onClick)
- val onLongClick = clickable.onLongClick?.let { onLongClick ->
- rememberClickBlockAction(clickable.clickBlockParams, onLongClick)
- }
- this.combinedClickable(
- enabled = clickable.enabled,
- onClick = onClick,
- onLongClick = onLongClick
- )
-} ?: this
-
@Composable
fun rememberFlow(
flow: Flow,
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/PreviewWireDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/PreviewWireDialog.kt
new file mode 100644
index 00000000000..c33f8f2e8ef
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/common/PreviewWireDialog.kt
@@ -0,0 +1,174 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.common
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import com.wire.android.ui.common.button.WireButtonState
+import com.wire.android.ui.common.textfield.WirePasswordTextField
+import com.wire.android.ui.theme.WireTheme
+import com.wire.android.ui.theme.wireTypography
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewWireDialog() {
+ var password by remember { mutableStateOf(TextFieldValue("")) }
+ WireTheme {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ WireDialogContent(
+ optionButton1Properties = WireDialogButtonProperties(
+ text = "OK",
+ onClick = { },
+ type = WireDialogButtonType.Primary,
+ state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
+ ),
+ dismissButtonProperties = WireDialogButtonProperties(
+ text = "Cancel",
+ onClick = { }
+ ),
+ title = "title",
+ text = buildAnnotatedString {
+ val style = SpanStyle(
+ color = colorsScheme().onBackground,
+ fontWeight = MaterialTheme.wireTypography.body01.fontWeight,
+ fontSize = MaterialTheme.wireTypography.body01.fontSize,
+ fontFamily = MaterialTheme.wireTypography.body01.fontFamily,
+ fontStyle = MaterialTheme.wireTypography.body01.fontStyle
+ )
+ withStyle(style) { append("text\nsecond line\nthirdLine\nfourth line\nfifth line\nsixth line\nseventh line") }
+ },
+ ) {
+ WirePasswordTextField(
+ value = password,
+ onValueChange = { password = it },
+ autofill = false
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Preview(showBackground = true)
+@Composable
+fun PreviewWireDialogWith2OptionButtons() {
+ var password by remember { mutableStateOf(TextFieldValue("")) }
+ WireTheme {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ WireDialogContent(
+ optionButton1Properties = WireDialogButtonProperties(
+ text = "OK",
+ onClick = { },
+ type = WireDialogButtonType.Primary,
+ state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
+ ),
+ optionButton2Properties = WireDialogButtonProperties(
+ text = "Later",
+ onClick = { },
+ type = WireDialogButtonType.Primary,
+ state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
+ ),
+ dismissButtonProperties = WireDialogButtonProperties(
+ text = "Cancel",
+ onClick = { }
+ ),
+ title = "title",
+ text = buildAnnotatedString {
+ val style = SpanStyle(
+ color = colorsScheme().onBackground,
+ fontWeight = MaterialTheme.wireTypography.body01.fontWeight,
+ fontSize = MaterialTheme.wireTypography.body01.fontSize,
+ fontFamily = MaterialTheme.wireTypography.body01.fontFamily,
+ fontStyle = MaterialTheme.wireTypography.body01.fontStyle
+ )
+ withStyle(style) { append("text") }
+ },
+ buttonsHorizontalAlignment = false
+ ) {
+ WirePasswordTextField(
+ value = password,
+ onValueChange = { password = it },
+ autofill = true
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewWireDialogCentered() {
+ var password by remember { mutableStateOf(TextFieldValue("")) }
+ WireTheme {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ WireDialogContent(
+ optionButton1Properties = WireDialogButtonProperties(
+ text = "OK",
+ onClick = { },
+ type = WireDialogButtonType.Primary,
+ state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
+ ),
+ dismissButtonProperties = WireDialogButtonProperties(
+ text = "Cancel",
+ onClick = { }
+ ),
+ centerContent = true,
+ title = "title",
+ text = buildAnnotatedString {
+ val style = SpanStyle(
+ color = colorsScheme().onBackground,
+ fontWeight = MaterialTheme.wireTypography.body01.fontWeight,
+ fontSize = MaterialTheme.wireTypography.body01.fontSize,
+ fontFamily = MaterialTheme.wireTypography.body01.fontFamily,
+ fontStyle = MaterialTheme.wireTypography.body01.fontStyle
+ )
+ withStyle(style) { append("text\nsecond line\nthirdLine\nfourth line\nfifth line\nsixth line\nseventh line") }
+ },
+ ) {
+ WirePasswordTextField(
+ value = password,
+ onValueChange = { password = it },
+ autofill = false
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/PreviewMenuBottomSheetItem.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/PreviewMenuBottomSheetItem.kt
new file mode 100644
index 00000000000..d0567d0dd46
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/PreviewMenuBottomSheetItem.kt
@@ -0,0 +1,60 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.common.bottomsheet
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import com.wire.android.R
+import com.wire.android.ui.common.ArrowRightIcon
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.theme.wireTypography
+
+@Preview
+@Composable
+fun PreviewMenuBottomSheetItem() {
+ MenuBottomSheetItem(
+ title = "very long looooooong title",
+ icon = {
+ MenuItemIcon(
+ id = R.drawable.ic_erase,
+ contentDescription = "",
+ )
+ },
+ action = {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = "very long looooooong action",
+ style = MaterialTheme.wireTypography.body01,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ modifier = Modifier.weight(weight = 1f, fill = false)
+ )
+ Spacer(modifier = Modifier.size(dimensions().spacing16x))
+ ArrowRightIcon()
+ }
+ }
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/PreviewWireModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/PreviewWireModalSheetLayout.kt
new file mode 100644
index 00000000000..2c39752fb6c
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/PreviewWireModalSheetLayout.kt
@@ -0,0 +1,43 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.common.bottomsheet
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.edit.ReactionOption
+import com.wire.android.ui.home.conversationslist.common.GroupConversationAvatar
+
+@Preview
+@Composable
+fun PreviewMenuModalSheetContentWithoutHeader() {
+ MenuModalSheetContent(
+ MenuModalSheetHeader.Gone,
+ listOf { ReactionOption({}) }
+ )
+}
+
+@Preview
+@Composable
+fun PreviewMenuModalSheetContentWithHeader() {
+ MenuModalSheetContent(
+ MenuModalSheetHeader.Visible("Title", { GroupConversationAvatar(colorsScheme().primary) }, dimensions().spacing8x),
+ listOf { ReactionOption({}) }
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt
index 1747775931b..0e5f5b0123a 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt
@@ -32,7 +32,6 @@ import com.wire.kalium.logic.data.sync.SyncState
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
@@ -44,7 +43,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
-@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class CommonTopAppBarViewModel @Inject constructor(
private val currentScreenManager: CurrentScreenManager,
@@ -114,11 +112,10 @@ class CommonTopAppBarViewModel @Inject constructor(
connectivity: Connectivity,
activeCall: Call?
): ConnectivityUIState {
- val canDisplayActiveCall = currentScreen !is CurrentScreen.OngoingCallScreen
val canDisplayConnectivityIssues = currentScreen !is CurrentScreen.AuthRelated
- if (activeCall != null && canDisplayActiveCall) {
+ if (activeCall != null) {
return ConnectivityUIState.EstablishedCall(activeCall.conversationId, activeCall.isMuted)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
index 75e183b8bf8..030be999d54 100644
--- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
@@ -17,7 +17,6 @@
*/
package com.wire.android.ui.debug
-import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -26,19 +25,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
import com.wire.android.BuildConfig
import com.wire.android.R
-import com.wire.android.datastore.GlobalDataStore
-import com.wire.android.di.CurrentAccount
-import com.wire.android.migration.failure.UserMigrationStatus
import com.wire.android.model.Clickable
import com.wire.android.ui.common.RowItemTemplate
import com.wire.android.ui.common.WireDialog
@@ -53,214 +45,13 @@ import com.wire.android.ui.home.settings.SettingsItem
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
-import com.wire.android.util.getDependenciesVersion
-import com.wire.android.util.getDeviceIdString
-import com.wire.android.util.getGitBuildId
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.kalium.logic.CoreFailure
-import com.wire.kalium.logic.E2EIFailure
import com.wire.kalium.logic.data.user.UserId
-import com.wire.kalium.logic.feature.debug.DisableEventProcessingUseCase
-import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase
import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult
-import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult
-import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase
import com.wire.kalium.logic.functional.Either
-import com.wire.kalium.logic.functional.fold
-import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler
-import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase
-import dagger.hilt.android.lifecycle.HiltViewModel
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
-import kotlinx.collections.immutable.toImmutableMap
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-//region DebugDataOptionsViewModel
-data class DebugDataOptionsState(
- val isEncryptedProteusStorageEnabled: Boolean = false,
- val isEventProcessingDisabled: Boolean = false,
- val keyPackagesCount: Int = 0,
- val mslClientId: String = "null",
- val mlsErrorMessage: String = "null",
- val isManualMigrationAllowed: Boolean = false,
- val debugId: String = "null",
- val commitish: String = "null",
- val certificate: String = "null",
- val showCertificate: Boolean = false,
- val startGettingE2EICertificate: Boolean = false,
- val dependencies: ImmutableMap = persistentMapOf()
-)
-
-@Suppress("LongParameterList")
-@HiltViewModel
-class DebugDataOptionsViewModel
-@Inject constructor(
- @ApplicationContext private val context: Context,
- @CurrentAccount val currentAccount: UserId,
- private val globalDataStore: GlobalDataStore,
- private val updateApiVersions: UpdateApiVersionsScheduler,
- private val mlsKeyPackageCountUseCase: MLSKeyPackageCountUseCase,
- private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase,
- private val disableEventProcessingUseCase: DisableEventProcessingUseCase,
- private val checkCrlRevocationListUseCase: CheckCrlRevocationListUseCase
-) : ViewModel() {
-
- var state by mutableStateOf(
- DebugDataOptionsState()
- )
-
- init {
- observeEncryptedProteusStorageState()
- observeMlsMetadata()
- checkIfCanTriggerManualMigration()
- checkDependenciesVersion()
- setGitHashAndDeviceId()
- }
-
- private fun setGitHashAndDeviceId() {
- viewModelScope.launch {
- val deviceId = context.getDeviceIdString() ?: "null"
- val gitBuildId = context.getGitBuildId()
- state = state.copy(
- debugId = deviceId,
- commitish = gitBuildId
- )
- }
- }
-
- fun checkDependenciesVersion() {
- viewModelScope.launch {
- val dependencies = context.getDependenciesVersion().toImmutableMap()
- state = state.copy(
- dependencies = dependencies
- )
- }
- }
-
- fun checkCrlRevocationList() {
- viewModelScope.launch {
- checkCrlRevocationListUseCase(
- forceUpdate = true
- )
- }
- }
-
- fun enableEncryptedProteusStorage(enabled: Boolean) {
- if (enabled) {
- viewModelScope.launch {
- globalDataStore.setEncryptedProteusStorageEnabled(true)
- }
- }
- }
-
- fun restartSlowSyncForRecovery() {
- viewModelScope.launch {
- restartSlowSyncProcessForRecovery()
- }
- }
-
- fun enrollE2EICertificate() {
- state = state.copy(startGettingE2EICertificate = true)
- }
-
- fun handleE2EIEnrollmentResult(result: Either) {
- result.fold({
- state = state.copy(
- certificate = (it as E2EIFailure.OAuth).reason,
- showCertificate = true,
- startGettingE2EICertificate = false
- )
- }, {
- if (it is E2EIEnrollmentResult.Finalized) {
- state = state.copy(
- certificate = it.certificate,
- showCertificate = true,
- startGettingE2EICertificate = false
- )
- } else {
- state.copy(
- certificate = it.toString(),
- showCertificate = true,
- startGettingE2EICertificate = false
- )
- }
- })
- }
-
- fun dismissCertificateDialog() {
- state = state.copy(
- showCertificate = false,
- )
- }
-
- fun forceUpdateApiVersions() {
- updateApiVersions.scheduleImmediateApiVersionUpdate()
- }
-
- fun disableEventProcessing(disabled: Boolean) {
- viewModelScope.launch {
- disableEventProcessingUseCase(disabled)
- state = state.copy(isEventProcessingDisabled = disabled)
- }
- }
-
- //region Private
- private fun observeEncryptedProteusStorageState() {
- viewModelScope.launch {
- globalDataStore.isEncryptedProteusStorageEnabled().collect {
- state = state.copy(isEncryptedProteusStorageEnabled = it)
- }
- }
- }
-
- // If status is NoNeed, it means that the user has already been migrated in and older app version,
- // or it is a new install
- // this is why we check the existence of the database file
- private fun checkIfCanTriggerManualMigration() {
- viewModelScope.launch {
- globalDataStore.getUserMigrationStatus(currentAccount.value).first()
- .let { migrationStatus ->
- if (migrationStatus != UserMigrationStatus.NoNeed) {
- context.getDatabasePath(currentAccount.value).let {
- state = state.copy(
- isManualMigrationAllowed = (it.exists() && it.isFile)
- )
- }
- }
- }
- }
- }
-
- private fun observeMlsMetadata() {
- viewModelScope.launch {
- mlsKeyPackageCountUseCase().let {
- when (it) {
- is MLSKeyPackageCountResult.Success -> {
- state = state.copy(
- keyPackagesCount = it.count,
- mslClientId = it.clientId.value
- )
- }
-
- is MLSKeyPackageCountResult.Failure.NetworkCallFailure -> {
- state = state.copy(mlsErrorMessage = "Network Error!")
- }
-
- is MLSKeyPackageCountResult.Failure.FetchClientIdFailure -> {
- state = state.copy(mlsErrorMessage = "ClientId Fetch Error!")
- }
-
- is MLSKeyPackageCountResult.Failure.Generic -> {}
- }
- }
- }
- }
- //endregion
-}
-//endregion
@Composable
fun DebugDataOptions(
@@ -283,7 +74,8 @@ fun DebugDataOptions(
enrollE2EICertificate = viewModel::enrollE2EICertificate,
handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult,
dismissCertificateDialog = viewModel::dismissCertificateDialog,
- checkCrlRevocationList = viewModel::checkCrlRevocationList
+ checkCrlRevocationList = viewModel::checkCrlRevocationList,
+ dependenciesMap = viewModel.state.dependencies
)
}
@@ -302,7 +94,8 @@ fun DebugDataOptionsContent(
enrollE2EICertificate: () -> Unit,
handleE2EIEnrollmentResult: (Either) -> Unit,
dismissCertificateDialog: () -> Unit,
- checkCrlRevocationList: () -> Unit
+ checkCrlRevocationList: () -> Unit,
+ dependenciesMap: ImmutableMap
) {
Column {
@@ -337,6 +130,7 @@ fun DebugDataOptionsContent(
onClick = { onCopyText(state.commitish) }
)
)
+ DependenciesItem(dependenciesMap)
if (BuildConfig.PRIVATE_BUILD) {
SettingsItem(
@@ -399,7 +193,6 @@ fun DebugDataOptionsContent(
onDisableEventProcessingChange = onDisableEventProcessingChange,
onRestartSlowSyncForRecovery = onRestartSlowSyncForRecovery,
onForceUpdateApiVersions = onForceUpdateApiVersions,
- dependenciesMap = state.dependencies,
checkCrlRevocationList = checkCrlRevocationList
)
}
@@ -569,7 +362,6 @@ private fun DebugToolsOptions(
onDisableEventProcessingChange: (Boolean) -> Unit,
onRestartSlowSyncForRecovery: () -> Unit,
onForceUpdateApiVersions: () -> Unit,
- dependenciesMap: ImmutableMap,
checkCrlRevocationList: () -> Unit
) {
FolderHeader(stringResource(R.string.label_debug_tools_title))
@@ -641,20 +433,32 @@ private fun DebugToolsOptions(
)
}
)
- RowItemTemplate(
- modifier = Modifier.wrapContentWidth(),
- title = {
- Text(
- style = MaterialTheme.wireTypography.body01,
- color = MaterialTheme.wireColorScheme.onBackground,
- text = prettyPrintMap(dependenciesMap),
- modifier = Modifier.padding(start = dimensions().spacing8x)
- )
- }
- )
}
}
+/**
+ * Compose function that will display the list of dependencies
+ * @param dependencies an Immutable map of a dependency name to its version number
+ */
+@Composable
+fun DependenciesItem(dependencies: ImmutableMap) {
+ val title = stringResource(id = R.string.item_dependencies_title)
+ val text = remember {
+ prettyPrintMap(dependencies, title)
+ }
+ RowItemTemplate(
+ modifier = Modifier.wrapContentWidth(),
+ title = {
+ Text(
+ style = MaterialTheme.wireTypography.body01,
+ color = MaterialTheme.wireColorScheme.onBackground,
+ text = text,
+ modifier = Modifier.padding(start = dimensions().spacing8x)
+ )
+ }
+ )
+}
+
@Composable
private fun DisableEventProcessingSwitch(
isEnabled: Boolean = false,
@@ -685,8 +489,8 @@ private fun DisableEventProcessingSwitch(
}
@Stable
-private fun prettyPrintMap(map: ImmutableMap): String = StringBuilder().apply {
- append("Dependencies:\n")
+private fun prettyPrintMap(map: ImmutableMap, title: String): String = StringBuilder().apply {
+ append("$title\n")
map.forEach { (key, value) ->
append("$key: $value\n")
}
@@ -718,6 +522,7 @@ fun PreviewOtherDebugOptions() {
enrollE2EICertificate = {},
handleE2EIEnrollmentResult = {},
dismissCertificateDialog = {},
- checkCrlRevocationList = {}
+ checkCrlRevocationList = {},
+ dependenciesMap = persistentMapOf()
)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt
new file mode 100644
index 00000000000..c0f648184dc
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt
@@ -0,0 +1,36 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.debug
+
+import kotlinx.collections.immutable.ImmutableMap
+import kotlinx.collections.immutable.persistentMapOf
+
+data class DebugDataOptionsState(
+ val isEncryptedProteusStorageEnabled: Boolean = false,
+ val isEventProcessingDisabled: Boolean = false,
+ val keyPackagesCount: Int = 0,
+ val mslClientId: String = "null",
+ val mlsErrorMessage: String = "null",
+ val isManualMigrationAllowed: Boolean = false,
+ val debugId: String = "null",
+ val commitish: String = "null",
+ val certificate: String = "null",
+ val showCertificate: Boolean = false,
+ val startGettingE2EICertificate: Boolean = false,
+ val dependencies: ImmutableMap = persistentMapOf()
+)
diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt
new file mode 100644
index 00000000000..944c27e0336
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt
@@ -0,0 +1,215 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.ui.debug
+
+import android.content.Context
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.wire.android.datastore.GlobalDataStore
+import com.wire.android.di.CurrentAccount
+import com.wire.android.migration.failure.UserMigrationStatus
+import com.wire.android.util.getDependenciesVersion
+import com.wire.android.util.getDeviceIdString
+import com.wire.android.util.getGitBuildId
+import com.wire.kalium.logic.CoreFailure
+import com.wire.kalium.logic.E2EIFailure
+import com.wire.kalium.logic.data.user.UserId
+import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase
+import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult
+import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult
+import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase
+import com.wire.kalium.logic.functional.Either
+import com.wire.kalium.logic.functional.fold
+import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler
+import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.collections.immutable.toImmutableMap
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@Suppress("LongParameterList")
+@HiltViewModel
+class DebugDataOptionsViewModel
+@Inject constructor(
+ @ApplicationContext private val context: Context,
+ @CurrentAccount val currentAccount: UserId,
+ private val globalDataStore: GlobalDataStore,
+ private val updateApiVersions: UpdateApiVersionsScheduler,
+ private val mlsKeyPackageCount: MLSKeyPackageCountUseCase,
+ private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase,
+ private val checkCrlRevocationList: CheckCrlRevocationListUseCase
+) : ViewModel() {
+
+ var state by mutableStateOf(
+ DebugDataOptionsState()
+ )
+
+ init {
+ observeEncryptedProteusStorageState()
+ observeMlsMetadata()
+ checkIfCanTriggerManualMigration()
+ setGitHashAndDeviceId()
+ checkDependenciesVersion()
+ }
+
+ private fun checkDependenciesVersion() {
+ viewModelScope.launch {
+ val dependencies = context.getDependenciesVersion().toImmutableMap()
+ state = state.copy(
+ dependencies = dependencies
+ )
+ }
+ }
+
+ private fun setGitHashAndDeviceId() {
+ viewModelScope.launch {
+ val deviceId = context.getDeviceIdString() ?: "null"
+ val gitBuildId = context.getGitBuildId()
+ state = state.copy(
+ debugId = deviceId,
+ commitish = gitBuildId
+ )
+ }
+ }
+
+ fun checkCrlRevocationList() {
+ viewModelScope.launch {
+ checkCrlRevocationList(
+ forceUpdate = true
+ )
+ }
+ }
+
+ fun enableEncryptedProteusStorage(enabled: Boolean) {
+ if (enabled) {
+ viewModelScope.launch {
+ globalDataStore.setEncryptedProteusStorageEnabled(true)
+ }
+ }
+ }
+
+ fun restartSlowSyncForRecovery() {
+ viewModelScope.launch {
+ restartSlowSyncProcessForRecovery()
+ }
+ }
+
+ fun enrollE2EICertificate() {
+ state = state.copy(startGettingE2EICertificate = true)
+ }
+
+ fun handleE2EIEnrollmentResult(result: Either) {
+ result.fold({
+ state = state.copy(
+ certificate = (it as E2EIFailure.OAuth).reason,
+ showCertificate = true,
+ startGettingE2EICertificate = false
+ )
+ }, {
+ if (it is E2EIEnrollmentResult.Finalized) {
+ state = state.copy(
+ certificate = it.certificate,
+ showCertificate = true,
+ startGettingE2EICertificate = false
+ )
+ } else {
+ state.copy(
+ certificate = it.toString(),
+ showCertificate = true,
+ startGettingE2EICertificate = false
+ )
+ }
+ })
+ }
+
+ fun dismissCertificateDialog() {
+ state = state.copy(
+ showCertificate = false,
+ )
+ }
+
+ fun forceUpdateApiVersions() {
+ updateApiVersions.scheduleImmediateApiVersionUpdate()
+ }
+
+ fun disableEventProcessing(disabled: Boolean) {
+ viewModelScope.launch {
+ disableEventProcessing(disabled)
+ state = state.copy(isEventProcessingDisabled = disabled)
+ }
+ }
+
+ //region Private
+ private fun observeEncryptedProteusStorageState() {
+ viewModelScope.launch {
+ globalDataStore.isEncryptedProteusStorageEnabled().collect {
+ state = state.copy(isEncryptedProteusStorageEnabled = it)
+ }
+ }
+ }
+
+ // If status is NoNeed, it means that the user has already been migrated in and older app version,
+ // or it is a new install
+ // this is why we check the existence of the database file
+ private fun checkIfCanTriggerManualMigration() {
+ viewModelScope.launch {
+ globalDataStore.getUserMigrationStatus(currentAccount.value).first()
+ .let { migrationStatus ->
+ if (migrationStatus != UserMigrationStatus.NoNeed) {
+ context.getDatabasePath(currentAccount.value).let {
+ state = state.copy(
+ isManualMigrationAllowed = (it.exists() && it.isFile)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun observeMlsMetadata() {
+ viewModelScope.launch {
+ mlsKeyPackageCount().let {
+ when (it) {
+ is MLSKeyPackageCountResult.Success -> {
+ state = state.copy(
+ keyPackagesCount = it.count,
+ mslClientId = it.clientId.value
+ )
+ }
+
+ is MLSKeyPackageCountResult.Failure.NetworkCallFailure -> {
+ state = state.copy(mlsErrorMessage = "Network Error!")
+ }
+
+ is MLSKeyPackageCountResult.Failure.FetchClientIdFailure -> {
+ state = state.copy(mlsErrorMessage = "ClientId Fetch Error!")
+ }
+
+ is MLSKeyPackageCountResult.Failure.Generic -> {}
+ }
+ }
+ }
+ }
+ //endregion
+}
+//endregion
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt
index 70667c36575..6a56a3707a9 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt
@@ -86,8 +86,6 @@ fun EnterLockCodeScreen(
BackHandler {
if (navigator.navController.previousBackStackEntry?.destination() is AppUnlockWithBiometricsScreenDestination) {
navigator.navigateBack()
- } else {
- navigator.finish()
}
}
LaunchedEffect(viewModel.state.done) {
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
index 985fba4191d..243f645527e 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
@@ -81,6 +81,9 @@ import com.wire.android.model.SnackBarMessage
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
+import com.wire.android.ui.LocalActivity
+import com.wire.android.ui.calling.getInitiatingCallIntent
+import com.wire.android.ui.calling.getOngoingCallIntent
import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader
import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout
import com.wire.android.ui.common.colorsScheme
@@ -102,10 +105,8 @@ import com.wire.android.ui.common.visbility.rememberVisibilityState
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.destinations.GroupConversationDetailsScreenDestination
import com.wire.android.ui.destinations.ImagesPreviewScreenDestination
-import com.wire.android.ui.destinations.InitiatingCallScreenDestination
import com.wire.android.ui.destinations.MediaGalleryScreenDestination
import com.wire.android.ui.destinations.MessageDetailsScreenDestination
-import com.wire.android.ui.destinations.OngoingCallScreenDestination
import com.wire.android.ui.destinations.OtherUserProfileScreenDestination
import com.wire.android.ui.destinations.SelfUserProfileScreenDestination
import com.wire.android.ui.home.conversations.AuthorHeaderHelper.rememberShouldHaveSmallBottomPadding
@@ -217,6 +218,8 @@ fun ConversationScreen(
// then ViewModel also detects it's removed and calls onNotFound which can execute navigateBack again and close the app
var alreadyDeletedByUser by rememberSaveable { mutableStateOf(false) }
+ val activity = LocalActivity.current
+
LaunchedEffect(alreadyDeletedByUser) {
if (!alreadyDeletedByUser) {
conversationInfoViewModel.observeConversationDetails(navigator::navigateBack)
@@ -248,7 +251,13 @@ fun ConversationScreen(
appLogger.i("showing showJoinAnywayDialog..")
JoinAnywayDialog(
onDismiss = ::dismissJoinCallAnywayDialog,
- onConfirm = { joinAnyway { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) } }
+ onConfirm = {
+ joinAnyway {
+ getOngoingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
+ }
+ }
)
}
}
@@ -257,7 +266,9 @@ fun ConversationScreen(
ConversationScreenDialogType.ONGOING_ACTIVE_CALL -> {
OngoingActiveCallDialog(onJoinAnyways = {
conversationCallViewModel.endEstablishedCallIfAny {
- navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(conversationCallViewModel.conversationId)))
+ getInitiatingCallIntent(activity, conversationCallViewModel.conversationId.toString()).run {
+ activity.startActivity(this)
+ }
}
showDialog.value = ConversationScreenDialogType.NONE
}, onDialogDismiss = {
@@ -281,9 +292,15 @@ fun ConversationScreen(
coroutineScope,
conversationInfoViewModel.conversationInfoViewState.conversationType,
onOpenInitiatingCallScreen = {
- navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(it)))
+ getInitiatingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
}
- ) { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) }
+ ) {
+ getOngoingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
+ }
},
onDialogDismiss = {
showDialog.value = ConversationScreenDialogType.NONE
@@ -307,9 +324,15 @@ fun ConversationScreen(
coroutineScope,
conversationInfoViewModel.conversationInfoViewState.conversationType,
onOpenInitiatingCallScreen = {
- navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(it)))
+ getInitiatingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
+ }
+ ) {
+ getOngoingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
}
- ) { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) }
+ }
},
onDialogDismiss = { showDialog.value = ConversationScreenDialogType.NONE }
)
@@ -375,12 +398,22 @@ fun ConversationScreen(
coroutineScope,
conversationInfoViewModel.conversationInfoViewState.conversationType,
onOpenInitiatingCallScreen = {
- navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(it)))
+ getInitiatingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
}
- ) { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) }
+ ) {
+ getOngoingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
+ }
},
onJoinCall = {
- conversationCallViewModel.joinOngoingCall { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) }
+ conversationCallViewModel.joinOngoingCall {
+ getOngoingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
+ }
},
onReactionClick = { messageId, emoji ->
conversationMessagesViewModel.toggleReaction(messageId, emoji)
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt
index 9a936dbbc41..4eebe93a290 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt
@@ -19,6 +19,7 @@
package com.wire.android.ui.home.conversations.info
import com.wire.android.model.ImageAsset
+import com.wire.android.util.CurrentConversationDetailsCache
import com.wire.android.util.ui.UIText
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.QualifiedID
@@ -37,7 +38,11 @@ data class ConversationInfoViewState(
val mlsVerificationStatus: Conversation.VerificationStatus? = null,
val proteusVerificationStatus: Conversation.VerificationStatus? = null,
val legalHoldStatus: Conversation.LegalHoldStatus = Conversation.LegalHoldStatus.UNKNOWN,
-)
+) {
+ init {
+ CurrentConversationDetailsCache.updateConversationName(conversationName)
+ }
+}
sealed class ConversationDetailsData {
data object None : ConversationDetailsData()
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt
index 09a71cc127e..308b57ea5c2 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt
@@ -39,6 +39,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -62,7 +63,12 @@ import com.wire.android.ui.common.typography
import com.wire.android.ui.home.conversations.messages.QuotedMessageStyle.COMPLETE
import com.wire.android.ui.home.conversations.messages.QuotedMessageStyle.PREVIEW
import com.wire.android.ui.home.conversations.model.UIQuotedMessage
+import com.wire.android.ui.markdown.MarkdownInline
+import com.wire.android.ui.markdown.NodeData
+import com.wire.android.ui.markdown.getFirstInlines
+import com.wire.android.ui.markdown.toMarkdownDocument
import com.wire.android.ui.theme.wireColorScheme
+import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.ui.UIText
private const val TEXT_QUOTE_MAX_LINES = 7
@@ -338,14 +344,16 @@ private fun QuotedText(
modifier = modifier,
startContent = {
startContent()
- }, centerContent = {
+ },
+ centerContent = {
editedTimeDescription?.let {
if (style == COMPLETE) {
StatusBox(it.asString())
}
}
- MainContentText(text)
- }, footerContent = {
+ MainMarkdownText(text)
+ },
+ footerContent = {
QuotedMessageOriginalDate(originalDateTimeDescription)
},
clickable = clickable
@@ -515,6 +523,33 @@ fun QuotedAudioMessage(
)
}
+@Composable
+private fun MainMarkdownText(text: String, fontStyle: FontStyle = FontStyle.Normal) {
+ val nodeData = NodeData(
+ color = colorsScheme().onSurfaceVariant,
+ style = MaterialTheme.wireTypography.subline01.copy(fontStyle = fontStyle),
+ colorScheme = MaterialTheme.wireColorScheme,
+ typography = MaterialTheme.wireTypography,
+ searchQuery = "",
+ mentions = listOf(),
+ disableLinks = true,
+ )
+
+ val markdownPreview = remember(text) {
+ text.toMarkdownDocument().getFirstInlines()
+ }
+
+ if (markdownPreview != null) {
+ MarkdownInline(
+ inlines = markdownPreview.children,
+ maxLines = TEXT_QUOTE_MAX_LINES,
+ nodeData = nodeData
+ )
+ } else {
+ MainContentText(text, fontStyle)
+ }
+}
+
@Composable
private fun MainContentText(text: String, fontStyle: FontStyle = FontStyle.Normal) {
Text(
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt
index 4945a985b23..d85e719fdfd 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt
@@ -105,9 +105,13 @@ internal fun MessageBody(
)
)
- text?.also {
+ val markdownDocument = remember(text) {
+ text?.toMarkdownDocument()
+ }
+
+ markdownDocument?.also {
MarkdownDocument(
- it.toMarkdownDocument(),
+ it,
nodeData,
clickable
)
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt
index 668e09e7cc3..8489c7e8c36 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt
@@ -30,6 +30,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.wire.android.R
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
+import com.wire.android.ui.LocalActivity
+import com.wire.android.ui.calling.getOngoingCallIntent
import com.wire.android.ui.common.bottomsheet.conversation.ConversationOptionNavigation
import com.wire.android.ui.common.bottomsheet.conversation.ConversationSheetContent
import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationSheetState
@@ -44,7 +46,6 @@ import com.wire.android.ui.common.visbility.VisibilityState
import com.wire.android.ui.common.visbility.rememberVisibilityState
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.destinations.NewConversationSearchPeopleScreenDestination
-import com.wire.android.ui.destinations.OngoingCallScreenDestination
import com.wire.android.ui.destinations.OtherUserProfileScreenDestination
import com.wire.android.ui.home.HomeSnackbarState
import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState
@@ -86,6 +87,8 @@ fun ConversationRouterHomeBridge(
val viewModel: ConversationListViewModel = hiltViewModel()
+ val activity = LocalActivity.current
+
LaunchedEffect(conversationsSource) {
viewModel.updateConversationsSource(conversationsSource)
}
@@ -198,7 +201,11 @@ fun ConversationRouterHomeBridge(
{ userId -> navigator.navigate(NavigationCommand(OtherUserProfileScreenDestination(userId))) }
}
val onJoinedCall: (ConversationId) -> Unit = remember(navigator) {
- { conversationId -> navigator.navigate(NavigationCommand(OngoingCallScreenDestination(conversationId))) }
+ {
+ getOngoingCallIntent(activity, it.toString()).run {
+ activity.startActivity(this)
+ }
+ }
}
with(viewModel.conversationListState) {
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt
index 9b4b0836bec..00c2cc07817 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt
@@ -21,6 +21,7 @@ package com.wire.android.ui.home.conversationslist.common
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.text.style.TextOverflow
import com.wire.android.ui.markdown.MarkdownConstants
import com.wire.android.ui.markdown.MarkdownInline
@@ -59,8 +60,13 @@ private fun LastMessageMarkdown(text: String, leadingText: String = "") {
disableLinks = true
)
- val markdownPreview = text.toMarkdownDocument().getFirstInlines()
- val leadingInlines = leadingText.toMarkdownDocument().getFirstInlines()?.children ?: persistentListOf()
+ val markdownPreview = remember(text) {
+ text.toMarkdownDocument().getFirstInlines()
+ }
+
+ val leadingInlines = remember(leadingText) {
+ leadingText.toMarkdownDocument().getFirstInlines()?.children ?: persistentListOf()
+ }
if (markdownPreview != null) {
MarkdownInline(
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt
index fdaec494e6a..d59e07fe469 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt
@@ -58,6 +58,7 @@ import com.wire.android.ui.home.messagecomposer.state.AdditionalOptionSelectItem
import com.wire.android.ui.home.messagecomposer.state.AdditionalOptionSubMenuState
import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder
import com.wire.android.ui.home.messagecomposer.state.MessageCompositionType
+import com.wire.android.util.CurrentConversationDetailsCache
import com.wire.android.util.permission.PermissionDenialType
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
@@ -260,10 +261,7 @@ fun EnabledMessageComposer(
additionalOptionStateHolder.toRichTextEditing()
},
onCloseRichEditingButtonClicked = additionalOptionStateHolder::toAttachmentAndAdditionalOptionsMenu,
- onDrawingModeClicked = {
- inputStateHolder.collapseComposer()
- additionalOptionStateHolder.toDrawingMode()
- }
+ onDrawingModeClicked = additionalOptionStateHolder::toDrawingMode
)
}
Box(
@@ -302,7 +300,12 @@ fun EnabledMessageComposer(
onDismissSketch = {
inputStateHolder.collapseComposer(additionalOptionStateHolder.additionalOptionsSubMenuState)
},
- onSendSketch = onSendButtonClicked
+ onSendSketch = {
+ onAttachmentPicked(UriAsset(it))
+ inputStateHolder.collapseComposer(additionalOptionStateHolder.additionalOptionsSubMenuState)
+ },
+ conversationTitle = CurrentConversationDetailsCache.conversationName.asString(),
+ tempWritableImageUri = tempWritableImageUri
)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt
index b306bc7ed66..9f6ad118065 100644
--- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt
@@ -98,6 +98,7 @@ class LegalHoldRequestedViewModel @Inject constructor(
is LegalHoldRequestData.Pending -> {
LegalHoldRequestedState.Visible(
requiresPassword = legalHoldRequestData.isPasswordRequired,
+ acceptEnabled = !legalHoldRequestData.isPasswordRequired,
legalHoldDeviceFingerprint = legalHoldRequestData.fingerprint,
userId = legalHoldRequestData.userId,
)
@@ -115,7 +116,7 @@ class LegalHoldRequestedViewModel @Inject constructor(
fun passwordChanged(password: TextFieldValue) {
state.ifVisible {
- state = it.copy(password = password, acceptEnabled = validatePassword(password.text).isValid)
+ state = it.copy(password = password, acceptEnabled = !it.requiresPassword || validatePassword(password.text).isValid)
}
}
@@ -127,6 +128,7 @@ class LegalHoldRequestedViewModel @Inject constructor(
(legalHoldRequestDataStateFlow.value as? LegalHoldRequestData.Pending)?.let {
state = LegalHoldRequestedState.Visible(
requiresPassword = it.isPasswordRequired,
+ acceptEnabled = !it.isPasswordRequired,
legalHoldDeviceFingerprint = it.fingerprint,
userId = it.userId,
)
@@ -137,46 +139,40 @@ class LegalHoldRequestedViewModel @Inject constructor(
state.ifVisible {
state = it.copy(acceptEnabled = false, loading = true)
// the accept button is enabled if the password is valid, this check is for safety only
- validatePassword(it.password.text).let { validatePasswordResult ->
- when (validatePasswordResult.isValid) {
- false ->
- state = it.copy(
- loading = false,
- error = LegalHoldRequestedError.InvalidCredentialsError
- )
-
- true ->
- viewModelScope.launch {
- coreLogic.sessionScope(it.userId) {
- approveLegalHoldRequest(it.password.text).let { approveLegalHoldResult ->
- state = when (approveLegalHoldResult) {
- is ApproveLegalHoldRequestUseCase.Result.Success ->
- LegalHoldRequestedState.Hidden
-
- ApproveLegalHoldRequestUseCase.Result.Failure.InvalidPassword ->
- it.copy(
- loading = false,
- error = LegalHoldRequestedError.InvalidCredentialsError
- )
-
- ApproveLegalHoldRequestUseCase.Result.Failure.PasswordRequired ->
- it.copy(
- loading = false,
- requiresPassword = true,
- error = LegalHoldRequestedError.InvalidCredentialsError
- )
-
- is ApproveLegalHoldRequestUseCase.Result.Failure.GenericFailure -> {
- appLogger.e("$TAG: Failed to approve legal hold: ${approveLegalHoldResult.coreFailure}")
- it.copy(
- loading = false,
- error = LegalHoldRequestedError.GenericError(approveLegalHoldResult.coreFailure)
- )
- }
- }
+ if (it.requiresPassword && validatePassword(it.password.text).isValid.not()) {
+ state = it.copy(loading = false, error = LegalHoldRequestedError.InvalidCredentialsError)
+ } else {
+ val password = if (it.requiresPassword) it.password.text else null
+ viewModelScope.launch {
+ coreLogic.sessionScope(it.userId) {
+ approveLegalHoldRequest(password).let { approveLegalHoldResult ->
+ state = when (approveLegalHoldResult) {
+ is ApproveLegalHoldRequestUseCase.Result.Success ->
+ LegalHoldRequestedState.Hidden
+
+ ApproveLegalHoldRequestUseCase.Result.Failure.InvalidPassword ->
+ it.copy(
+ loading = false,
+ error = LegalHoldRequestedError.InvalidCredentialsError
+ )
+
+ ApproveLegalHoldRequestUseCase.Result.Failure.PasswordRequired ->
+ it.copy(
+ loading = false,
+ requiresPassword = true,
+ error = LegalHoldRequestedError.InvalidCredentialsError
+ )
+
+ is ApproveLegalHoldRequestUseCase.Result.Failure.GenericFailure -> {
+ appLogger.e("$TAG: Failed to approve legal hold: ${approveLegalHoldResult.coreFailure}")
+ it.copy(
+ loading = false,
+ error = LegalHoldRequestedError.GenericError(approveLegalHoldResult.coreFailure)
+ )
}
}
}
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownInline.kt b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownInline.kt
index 4367080c4b6..a7f25d1e11f 100644
--- a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownInline.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownInline.kt
@@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow
@Composable
fun MarkdownInline(
inlines: List,
+ maxLines: Int = 1,
nodeData: NodeData
) {
val annotatedString = buildAnnotatedString {
@@ -36,7 +37,7 @@ fun MarkdownInline(
style = nodeData.style,
color = nodeData.color,
clickable = false,
- maxLines = 1,
+ maxLines = maxLines,
overflow = TextOverflow.Ellipsis
)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/server/ApiVersioningDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/server/ApiVersioningDialogs.kt
index 3bc7c85e5e8..f93d04a3b9f 100644
--- a/app/src/main/kotlin/com/wire/android/ui/server/ApiVersioningDialogs.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/server/ApiVersioningDialogs.kt
@@ -18,26 +18,15 @@
package com.wire.android.ui.server
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.wire.android.R
import com.wire.android.ui.common.WireDialog
import com.wire.android.ui.common.WireDialogButtonProperties
import com.wire.android.ui.common.WireDialogButtonType
-import com.wire.android.ui.theme.WireTheme
-@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun ApiVersioningDialog(
title: String,
diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentConversationDetailsCache.kt b/app/src/main/kotlin/com/wire/android/util/CurrentConversationDetailsCache.kt
new file mode 100644
index 00000000000..8ac6dfc3aae
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/util/CurrentConversationDetailsCache.kt
@@ -0,0 +1,40 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.util
+
+import com.wire.android.util.ui.UIText
+import com.wire.android.util.ui.toUIText
+
+/**
+ * Cache for the current conversation details.
+ * This is used to display the conversation name in the toolbar or can be used for other purposes.
+ *
+ * TODO: This is temporary, when we have navigation for sketch, we might do it with navigation arguments.
+ * TODO: Anyway, this might be useful, and we might keep it or discuss it.
+ */
+object CurrentConversationDetailsCache {
+
+ @Volatile
+ var conversationName: UIText = "".toUIText()
+ private set
+
+ @Synchronized
+ fun updateConversationName(newName: UIText) {
+ conversationName = newName
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
index ba44de5a9e1..a2d69dc3e99 100644
--- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
+++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
@@ -38,19 +38,15 @@ import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination
import com.wire.android.ui.destinations.HomeScreenDestination
import com.wire.android.ui.destinations.ImportMediaScreenDestination
-import com.wire.android.ui.destinations.IncomingCallScreenDestination
import com.wire.android.ui.destinations.InitialSyncScreenDestination
-import com.wire.android.ui.destinations.InitiatingCallScreenDestination
import com.wire.android.ui.destinations.LoginScreenDestination
import com.wire.android.ui.destinations.MigrationScreenDestination
-import com.wire.android.ui.destinations.OngoingCallScreenDestination
import com.wire.android.ui.destinations.OtherUserProfileScreenDestination
import com.wire.android.ui.destinations.RegisterDeviceScreenDestination
import com.wire.android.ui.destinations.RemoveDeviceScreenDestination
import com.wire.android.ui.destinations.SelfDevicesScreenDestination
import com.wire.android.ui.destinations.WelcomeScreenDestination
import com.wire.kalium.logic.data.id.ConversationId
-import com.wire.kalium.logic.data.id.QualifiedID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -153,17 +149,6 @@ sealed class CurrentScreen {
// Another User Profile Screen is opened
data class OtherUserProfile(val id: ConversationId) : CurrentScreen()
- sealed class CallScreen(open val id: QualifiedID) : CurrentScreen()
-
- // Ongoing call screen is opened
- class OngoingCallScreen(override val id: QualifiedID) : CallScreen(id)
-
- // Incoming call screen is opened
- class IncomingCallScreen(override val id: QualifiedID) : CallScreen(id)
-
- // Initiating call screen is opened
- class InitiatingCallScreen(override val id: QualifiedID) : CallScreen(id)
-
// Import media screen is opened
object ImportMedia : CurrentScreen()
@@ -193,15 +178,6 @@ sealed class CurrentScreen {
is OtherUserProfileScreenDestination ->
destination.argsFrom(arguments).conversationId?.let { OtherUserProfile(it) } ?: SomeOther
- is OngoingCallScreenDestination ->
- OngoingCallScreen(destination.argsFrom(arguments).conversationId)
-
- is IncomingCallScreenDestination ->
- IncomingCallScreen(destination.argsFrom(arguments).conversationId)
-
- is InitiatingCallScreenDestination ->
- InitiatingCallScreen(destination.argsFrom(arguments).conversationId)
-
is ImportMediaScreenDestination -> ImportMedia
is SelfDevicesScreenDestination -> DeviceManager
diff --git a/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt b/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt
index 02ee90a3c5d..cae130be666 100644
--- a/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt
+++ b/app/src/main/kotlin/com/wire/android/util/debug/FeatureVisibilityFlags.kt
@@ -55,7 +55,7 @@ object FeatureVisibilityFlags {
const val UserProfileEditIcon = false
const val MessageEditIcon = true
const val SearchConversationMessages = true
- const val DrawingIcon = false
+ const val DrawingIcon = true
}
val LocalFeatureVisibilityFlags = staticCompositionLocalOf { FeatureVisibilityFlags }
diff --git a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt
index e8de020f3ac..5065fa3fd71 100644
--- a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt
+++ b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt
@@ -47,12 +47,17 @@ sealed class DeepLinkResult {
data class Failure(val ssoError: SSOFailureCodes) : SSOLogin()
}
- data class IncomingCall(val conversationsId: ConversationId, val switchedAccount: Boolean = false) : DeepLinkResult()
+ data class OpenConversation(
+ val conversationsId: ConversationId,
+ val switchedAccount: Boolean = false
+ ) : DeepLinkResult()
+
+ data class OpenOtherUserProfile(val userId: QualifiedID, val switchedAccount: Boolean = false) :
+ DeepLinkResult()
+
+ data class JoinConversation(val code: String, val key: String, val domain: String?) :
+ DeepLinkResult()
- data class OngoingCall(val conversationsId: ConversationId) : DeepLinkResult()
- data class OpenConversation(val conversationsId: ConversationId, val switchedAccount: Boolean = false) : DeepLinkResult()
- data class OpenOtherUserProfile(val userId: QualifiedID, val switchedAccount: Boolean = false) : DeepLinkResult()
- data class JoinConversation(val code: String, val key: String, val domain: String?) : DeepLinkResult()
data class MigrationLogin(val userHandle: String) : DeepLinkResult()
}
@@ -69,10 +74,12 @@ class DeepLinkProcessor @Inject constructor(
return when (uri.host) {
ACCESS_DEEPLINK_HOST -> getCustomServerConfigDeepLinkResult(uri)
SSO_LOGIN_DEEPLINK_HOST -> getSSOLoginDeepLinkResult(uri)
- INCOMING_CALL_DEEPLINK_HOST -> getIncomingCallDeepLinkResult(uri, switchedAccount)
- ONGOING_CALL_DEEPLINK_HOST -> getOngoingCallDeepLinkResult(uri)
CONVERSATION_DEEPLINK_HOST -> getOpenConversationDeepLinkResult(uri, switchedAccount)
- OTHER_USER_PROFILE_DEEPLINK_HOST -> getOpenOtherUserProfileDeepLinkResult(uri, switchedAccount)
+ OTHER_USER_PROFILE_DEEPLINK_HOST -> getOpenOtherUserProfileDeepLinkResult(
+ uri,
+ switchedAccount
+ )
+
MIGRATION_LOGIN_HOST -> getOpenMigrationLoginDeepLinkResult(uri)
JOIN_CONVERSATION_DEEPLINK_HOST -> getJoinConversationDeepLinkResult(uri)
else -> DeepLinkResult.Unknown
@@ -80,25 +87,32 @@ class DeepLinkProcessor @Inject constructor(
}
private suspend fun switchAccountIfNeeded(uri: Uri): Boolean {
- uri.getQueryParameter(USER_TO_USE_QUERY_PARAM)?.toQualifiedID(qualifiedIdMapper)?.let { userId ->
- val shouldSwitchAccount = when (val result = currentSession()) {
- is CurrentSessionResult.Failure.Generic -> true
- CurrentSessionResult.Failure.SessionNotFound -> true
- is CurrentSessionResult.Success -> result.accountInfo.userId != userId
+ uri.getQueryParameter(USER_TO_USE_QUERY_PARAM)?.toQualifiedID(qualifiedIdMapper)
+ ?.let { userId ->
+ val shouldSwitchAccount = when (val result = currentSession()) {
+ is CurrentSessionResult.Failure.Generic -> true
+ CurrentSessionResult.Failure.SessionNotFound -> true
+ is CurrentSessionResult.Success -> result.accountInfo.userId != userId
+ }
+ if (shouldSwitchAccount) {
+ return accountSwitch(SwitchAccountParam.SwitchToAccount(userId)) == SwitchAccountResult.SwitchedToAnotherAccount
+ }
}
- if (shouldSwitchAccount) {
- return accountSwitch(SwitchAccountParam.SwitchToAccount(userId)) == SwitchAccountResult.SwitchedToAnotherAccount
- }
- }
return false
}
- private fun getOpenConversationDeepLinkResult(uri: Uri, switchedAccount: Boolean): DeepLinkResult =
+ private fun getOpenConversationDeepLinkResult(
+ uri: Uri,
+ switchedAccount: Boolean
+ ): DeepLinkResult =
uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let { conversationId ->
DeepLinkResult.OpenConversation(conversationId, switchedAccount)
} ?: DeepLinkResult.Unknown
- private fun getOpenOtherUserProfileDeepLinkResult(uri: Uri, switchedAccount: Boolean): DeepLinkResult =
+ private fun getOpenOtherUserProfileDeepLinkResult(
+ uri: Uri,
+ switchedAccount: Boolean
+ ): DeepLinkResult =
uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let {
DeepLinkResult.OpenOtherUserProfile(it, switchedAccount)
} ?: DeepLinkResult.Unknown
@@ -109,18 +123,9 @@ class DeepLinkProcessor @Inject constructor(
else DeepLinkResult.MigrationLogin(it)
} ?: DeepLinkResult.Unknown
- private fun getCustomServerConfigDeepLinkResult(uri: Uri) = uri.getQueryParameter(SERVER_CONFIG_PARAM)?.let {
- DeepLinkResult.CustomServerConfig(it)
- } ?: DeepLinkResult.Unknown
-
- private fun getIncomingCallDeepLinkResult(uri: Uri, switchedAccount: Boolean) =
- uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let {
- DeepLinkResult.IncomingCall(it, switchedAccount)
- } ?: DeepLinkResult.Unknown
-
- private fun getOngoingCallDeepLinkResult(uri: Uri) =
- uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let {
- DeepLinkResult.OngoingCall(it)
+ private fun getCustomServerConfigDeepLinkResult(uri: Uri) =
+ uri.getQueryParameter(SERVER_CONFIG_PARAM)?.let {
+ DeepLinkResult.CustomServerConfig(it)
} ?: DeepLinkResult.Unknown
private fun getSSOLoginDeepLinkResult(uri: Uri): DeepLinkResult {
@@ -162,8 +167,6 @@ class DeepLinkProcessor @Inject constructor(
const val SSO_LOGIN_COOKIE_PARAM = "cookie"
const val SSO_LOGIN_ERROR_PARAM = "error"
const val SSO_LOGIN_SERVER_CONFIG_PARAM = "location"
- const val INCOMING_CALL_DEEPLINK_HOST = "incoming-call"
- const val ONGOING_CALL_DEEPLINK_HOST = "ongoing-call"
const val CONVERSATION_DEEPLINK_HOST = "conversation"
const val OTHER_USER_PROFILE_DEEPLINK_HOST = "other-user-profile"
const val MIGRATION_LOGIN_HOST = "migration-login"
@@ -176,7 +179,10 @@ class DeepLinkProcessor @Inject constructor(
}
enum class SSOFailureCodes(val label: String, val errorCode: Int) {
- ServerErrorUnsupportedSaml("server-error-unsupported-saml", SSOServerErrorCode.SERVER_ERROR_UNSUPPORTED_SAML),
+ ServerErrorUnsupportedSaml(
+ "server-error-unsupported-saml",
+ SSOServerErrorCode.SERVER_ERROR_UNSUPPORTED_SAML
+ ),
BadSuccessRedirect("bad-success-redirect", SSOServerErrorCode.BAD_SUCCESS_REDIRECT),
BadFailureRedirect("bad-failure-redirect", SSOServerErrorCode.BAD_FAILURE_REDIRECT),
BadUsername("bad-username", SSOServerErrorCode.BAD_USERNAME),
@@ -185,7 +191,10 @@ enum class SSOFailureCodes(val label: String, val errorCode: Int) {
NotFound("not-found", SSOServerErrorCode.NOT_FOUND),
Forbidden("forbidden", SSOServerErrorCode.FORBIDDEN),
NoMatchingAuthReq("no-matching-auth-req", SSOServerErrorCode.NO_MATCHING_AUTH_REQ),
- InsufficientPermissions("insufficient-permissions", SSOServerErrorCode.INSUFFICIENT_PERMISSIONS),
+ InsufficientPermissions(
+ "insufficient-permissions",
+ SSOServerErrorCode.INSUFFICIENT_PERMISSIONS
+ ),
Unknown("unknown", SSOServerErrorCode.UNKNOWN);
companion object {
diff --git a/app/src/main/kotlin/com/wire/android/util/ui/ScreenSettingsUtil.kt b/app/src/main/kotlin/com/wire/android/util/ui/ScreenSettingsUtil.kt
deleted file mode 100644
index 907702cb7bf..00000000000
--- a/app/src/main/kotlin/com/wire/android/util/ui/ScreenSettingsUtil.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Wire
- * Copyright (C) 2024 Wire Swiss GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see http://www.gnu.org/licenses/.
- */
-
-package com.wire.android.util.ui
-
-import android.app.Activity
-import android.os.Build
-import android.view.WindowManager
-import androidx.navigation.NavDestination
-import com.wire.android.navigation.style.ScreenMode
-import com.wire.android.navigation.style.ScreenModeStyle
-import com.wire.android.navigation.toDestination
-
-fun Activity.updateScreenSettings(navDestination: NavDestination) {
- val screenMode = (navDestination.toDestination()?.style as? ScreenModeStyle)?.screenMode() ?: ScreenMode.NONE
- updateScreenSettings(screenMode)
-}
-
-private fun Activity.updateScreenSettings(screenMode: ScreenMode?) {
- when (screenMode) {
- ScreenMode.WAKE_UP -> wakeUpDevice()
- ScreenMode.KEEP_ON -> addScreenOnFlags()
- else -> removeScreenOnFlags()
- }
-}
-
-private fun Activity.wakeUpDevice() {
-
- addScreenOnFlags()
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
- setShowWhenLocked(true)
- setTurnScreenOn(true)
- } else {
- window.addFlags(
- WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
- WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- )
- }
-}
-
-private fun Activity.addScreenOnFlags() {
- window.addFlags(
- WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
- )
-}
-
-private fun Activity.removeScreenOnFlags() {
- window.clearFlags(
- WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
- )
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
- setShowWhenLocked(false)
- setTurnScreenOn(false)
- } else {
- window.clearFlags(
- WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
- WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- )
- }
-}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cbf1236e7a2..860d6d27dd7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -201,6 +201,7 @@
API VERSIONING
E2EI Manual Enrollment
Force API versioning update
+ Dependencies:
Update
Support
Back up & Restore Conversations
diff --git a/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt
index 0e8364e5bbb..d2551459b28 100644
--- a/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt
+++ b/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt
@@ -149,7 +149,6 @@ class AccountSwitchUseCaseTest {
MockKAnnotations.init(this, relaxUnitFun = true)
}
- @OptIn(ExperimentalCoroutinesApi::class)
var accountSwitchUseCase: AccountSwitchUseCase = AccountSwitchUseCase(
updateCurrentSession,
getSessions,
diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt
index 4e3905c7ba6..41c7c7b5df1 100644
--- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt
@@ -232,35 +232,6 @@ class WireActivityViewModelTest {
verify(exactly = 1) { arrangement.onDeepLinkResult(result) }
}
- @Test
- fun `given Intent with IncomingCall, when currentSession is present, then initialAppState is LOGGED_IN and result IncomingCall`() =
- runTest {
- val result = DeepLinkResult.IncomingCall(ConversationId("val", "dom"))
- val (arrangement, viewModel) = Arrangement()
- .withSomeCurrentSession()
- .withDeepLinkResult(result)
- .arrange()
-
- viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult)
-
- assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState)
- verify(exactly = 1) { arrangement.onDeepLinkResult(result) }
- }
-
- @Test
- fun `given Intent with IncomingCall, when currentSession is absent, then initialAppState is NOT_LOGGED_IN`() = runTest {
- val result = DeepLinkResult.IncomingCall(ConversationId("val", "dom"))
- val (arrangement, viewModel) = Arrangement()
- .withNoCurrentSession()
- .withDeepLinkResult(result)
- .arrange()
-
- viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult)
-
- assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState)
- verify(exactly = 0) { arrangement.onDeepLinkResult(any()) }
- }
-
@Test
fun `given Intent with OpenConversation, when currentSession is present, then initialAppState is LOGGED_IN and result OpenConversation`() =
runTest {
diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt
index ddb7a36e4ff..50404c04250 100644
--- a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt
@@ -18,14 +18,12 @@
package com.wire.android.ui.calling
-import androidx.lifecycle.SavedStateHandle
import com.wire.android.config.CoroutineTestExtension
-import com.wire.android.datastore.GlobalDataStore
import com.wire.android.config.NavigationTestExtension
+import com.wire.android.datastore.GlobalDataStore
import com.wire.android.ui.calling.model.UICallParticipant
import com.wire.android.ui.calling.ongoing.OngoingCallViewModel
import com.wire.android.ui.home.conversationslist.model.Membership
-import com.wire.android.ui.navArgs
import com.wire.android.util.CurrentScreen
import com.wire.android.util.CurrentScreenManager
import com.wire.kalium.logic.data.call.Call
@@ -42,7 +40,6 @@ import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.coVerify
-import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -50,17 +47,14 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(NavigationTestExtension::class)
@ExtendWith(CoroutineTestExtension::class)
class OngoingCallViewModelTest {
- @MockK
- private lateinit var savedStateHandle: SavedStateHandle
-
@MockK
private lateinit var establishedCall: ObserveEstablishedCallsUseCase
@@ -81,14 +75,15 @@ class OngoingCallViewModelTest {
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
- every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = conversationId)
coEvery { establishedCall.invoke() } returns flowOf(listOf(provideCall()))
- coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther)
+ coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(
+ CurrentScreen.SomeOther
+ )
coEvery { globalDataStore.getShouldShowDoubleTapToast(any()) } returns false
coEvery { setVideoSendState.invoke(any(), any()) } returns Unit
ongoingCallViewModel = OngoingCallViewModel(
- savedStateHandle = savedStateHandle,
+ conversationId = conversationId,
establishedCalls = establishedCall,
requestVideoStreams = requestVideoStreams,
currentScreenManager = currentScreenManager,
@@ -113,26 +108,42 @@ class OngoingCallViewModelTest {
}
@Test
- fun givenParticipantsList_WhenRequestingVideoStream_ThenRequestItForOnlyParticipantsWithVideoEnabled() = runTest {
- val expectedClients = listOf(
- CallClient(participant1.id.toString(), participant1.clientId),
- CallClient(participant3.id.toString(), participant3.clientId)
- )
- coEvery { requestVideoStreams(conversationId = conversationId, expectedClients) } returns Unit
-
- ongoingCallViewModel.requestVideoStreams(participants)
-
- coVerify(exactly = 1) { requestVideoStreams(conversationId, expectedClients) }
- }
+ fun givenParticipantsList_WhenRequestingVideoStream_ThenRequestItForOnlyParticipantsWithVideoEnabled() =
+ runTest {
+ val expectedClients = listOf(
+ CallClient(participant1.id.toString(), participant1.clientId),
+ CallClient(participant3.id.toString(), participant3.clientId)
+ )
+ coEvery {
+ requestVideoStreams(
+ conversationId = conversationId,
+ expectedClients
+ )
+ } returns Unit
+
+ ongoingCallViewModel.requestVideoStreams(participants)
+
+ coVerify(exactly = 1) { requestVideoStreams(conversationId, expectedClients) }
+ }
@Test
fun givenDoubleTabIndicatorIsDisplayed_whenUserTapsOnIt_thenHideIt() = runTest {
- coEvery { globalDataStore.setShouldShowDoubleTapToastStatus(currentUserId.toString(), false) } returns Unit
+ coEvery {
+ globalDataStore.setShouldShowDoubleTapToastStatus(
+ currentUserId.toString(),
+ false
+ )
+ } returns Unit
ongoingCallViewModel.hideDoubleTapToast()
assertEquals(false, ongoingCallViewModel.shouldShowDoubleTapToast)
- coVerify(exactly = 1) { globalDataStore.setShouldShowDoubleTapToastStatus(currentUserId.toString(), false) }
+ coVerify(exactly = 1) {
+ globalDataStore.setShouldShowDoubleTapToastStatus(
+ currentUserId.toString(),
+ false
+ )
+ }
}
companion object {
@@ -174,7 +185,12 @@ class OngoingCallViewModelTest {
val participants = listOf(participant1, participant2, participant3)
}
- private fun provideCall(id: ConversationId = ConversationId("some-dummy-value", "some.dummy.domain")) = Call(
+ private fun provideCall(
+ id: ConversationId = ConversationId(
+ "some-dummy-value",
+ "some.dummy.domain"
+ )
+ ) = Call(
conversationId = id,
status = CallStatus.ESTABLISHED,
callerId = "caller_id",
diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt
index 82e7ffe8208..e1fa9e5325d 100644
--- a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt
@@ -19,14 +19,12 @@
package com.wire.android.ui.calling
import android.view.View
-import androidx.lifecycle.SavedStateHandle
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.NavigationTestExtension
import com.wire.android.config.TestDispatcherProvider
import com.wire.android.mapper.UICallParticipantMapper
import com.wire.android.mapper.UserTypeMapper
import com.wire.android.media.CallRinger
-import com.wire.android.ui.navArgs
import com.wire.android.util.CurrentScreen
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.ui.WireSessionImageLoader
@@ -63,9 +61,6 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(NavigationTestExtension::class)
class SharedCallingViewModelTest {
- @MockK
- private lateinit var savedStateHandle: SavedStateHandle
-
@MockK
private lateinit var allCalls: GetAllCallsWithSortedParticipantsUseCase
@@ -131,9 +126,7 @@ class SharedCallingViewModelTest {
@BeforeEach
fun setup() {
- val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain")
MockKAnnotations.init(this)
- every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = dummyConversationId)
coEvery { allCalls.invoke() } returns emptyFlow()
coEvery { observeConversationDetails.invoke(any()) } returns emptyFlow()
coEvery { observeSpeaker.invoke() } returns emptyFlow()
@@ -142,7 +135,7 @@ class SharedCallingViewModelTest {
)
sharedCallingViewModel = SharedCallingViewModel(
- savedStateHandle = savedStateHandle,
+ conversationId = conversationId,
conversationDetails = observeConversationDetails,
allCalls = allCalls,
endCall = endCall,
diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt
index 7b6ae3ab5db..a180a28366b 100644
--- a/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt
@@ -18,12 +18,10 @@
package com.wire.android.ui.calling.incoming
-import androidx.lifecycle.SavedStateHandle
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.NavigationTestExtension
import com.wire.android.media.CallRinger
-import com.wire.android.ui.calling.CallingNavArgs
-import com.wire.android.ui.navArgs
+import com.wire.android.ui.home.appLock.LockCodeTimeManager
import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.logic.data.conversation.Conversation
@@ -59,9 +57,6 @@ class IncomingCallViewModelTest {
class Arrangement {
- @MockK
- private lateinit var savedStateHandle: SavedStateHandle
-
@MockK
lateinit var rejectCall: RejectCallUseCase
@@ -83,9 +78,11 @@ class IncomingCallViewModelTest {
@MockK
lateinit var muteCall: MuteCallUseCase
+ @MockK
+ lateinit var lockCodeTimeManager: LockCodeTimeManager
+
init {
MockKAnnotations.init(this)
- every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = dummyConversationId)
// Default empty values
coEvery { rejectCall(any()) } returns Unit
@@ -97,6 +94,13 @@ class IncomingCallViewModelTest {
coEvery { muteCall(any(), any()) } returns Unit
}
+ fun withAppNotLocked() = apply {
+ every { lockCodeTimeManager.observeAppLock() } returns flowOf(false)
+ }
+ fun withAppLocked() = apply {
+ every { lockCodeTimeManager.observeAppLock() } returns flowOf(true)
+ }
+
fun withEstablishedCalls(flow: Flow>) = apply {
coEvery { observeEstablishedCalls.invoke() } returns flow
}
@@ -106,33 +110,62 @@ class IncomingCallViewModelTest {
}
fun arrange() = this to IncomingCallViewModel(
- savedStateHandle = savedStateHandle,
+ conversationId = dummyConversationId,
incomingCalls = incomingCalls,
rejectCall = rejectCall,
acceptCall = acceptCall,
callRinger = callRinger,
observeEstablishedCalls = observeEstablishedCalls,
endCall = endCall,
- muteCall = muteCall
+ muteCall = muteCall,
+ lockCodeTimeManager = lockCodeTimeManager
)
}
+ @Test
+ fun `given app Locked, when the user decline the call, then do not reject the call`() = runTest {
+ val (arrangement, viewModel) = Arrangement()
+ .withAppLocked()
+ .arrange()
+
+ viewModel.declineCall({}, {})
+
+ coVerify(inverse = true) { arrangement.rejectCall(conversationId = any()) }
+ verify(inverse = true) { arrangement.callRinger.stop() }
+ }
+
@Test
fun `given an incoming call, when the user decline the call, then the reject call use case is called`() = runTest {
- val (arrangement, viewModel) = Arrangement().arrange()
+ val (arrangement, viewModel) = Arrangement()
+ .withAppNotLocked()
+ .arrange()
- viewModel.declineCall()
+ viewModel.declineCall({}, {})
coVerify(exactly = 1) { arrangement.rejectCall(conversationId = any()) }
verify(exactly = 1) { arrangement.callRinger.stop() }
assertTrue { viewModel.incomingCallState.flowState is IncomingCallState.FlowState.CallClosed }
}
+ @Test
+ fun `given app locked, when user tries to accept an incoming call, then do not accept the call`() = runTest {
+ val (arrangement, viewModel) = Arrangement()
+ .withAppLocked()
+ .arrange()
+
+ viewModel.acceptCall({})
+
+ coVerify(inverse = true) { arrangement.acceptCall(conversationId = any()) }
+ verify(inverse = true) { arrangement.callRinger.stop() }
+ }
+
@Test
fun `given no ongoing call, when user tries to accept an incoming call, then invoke answerCall call use case`() = runTest {
- val (arrangement, viewModel) = Arrangement().arrange()
+ val (arrangement, viewModel) = Arrangement()
+ .withAppNotLocked()
+ .arrange()
- viewModel.acceptCall()
+ viewModel.acceptCall({})
advanceUntilIdle()
coVerify(exactly = 1) { arrangement.acceptCall(conversationId = any()) }
@@ -145,10 +178,11 @@ class IncomingCallViewModelTest {
@Test
fun `given an ongoing call, when user tries to accept an incoming call, then show JoinCallAnywayDialog`() = runTest {
val (arrangement, viewModel) = Arrangement()
+ .withAppNotLocked()
.withEstablishedCalls(flowOf(listOf(provideCall(ConversationId("value", "Domain")))))
.arrange()
- viewModel.acceptCall()
+ viewModel.acceptCall({})
assertTrue { viewModel.incomingCallState.flowState is IncomingCallState.FlowState.Default }
assertEquals(true, viewModel.incomingCallState.shouldShowJoinCallAnywayDialog)
@@ -162,11 +196,12 @@ class IncomingCallViewModelTest {
val establishedCallsChannel = Channel>(capacity = Channel.UNLIMITED)
.also { it.send(listOf(provideCall(ConversationId("value", "Domain")))) }
val (arrangement, viewModel) = Arrangement()
+ .withAppNotLocked()
.withEstablishedCalls(establishedCallsChannel.consumeAsFlow())
.withEndCall { establishedCallsChannel.send(listOf()) }
.arrange()
- viewModel.acceptCallAnyway()
+ viewModel.acceptCallAnyway({})
advanceUntilIdle()
coVerify(exactly = 1) { arrangement.endCall(any()) }
@@ -177,10 +212,11 @@ class IncomingCallViewModelTest {
@Test
fun `given join dialog displayed, when user dismisses it, then hide it`() = runTest {
val (arrangement, viewModel) = Arrangement()
+ .withAppNotLocked()
.withEstablishedCalls(flowOf(listOf(provideCall())))
.arrange()
- viewModel.acceptCall()
+ viewModel.acceptCall({})
viewModel.dismissJoinCallAnywayDialog()
diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt
index 34987789c08..9f44e203da9 100644
--- a/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt
@@ -18,12 +18,9 @@
package com.wire.android.ui.calling.initiating
-import androidx.lifecycle.SavedStateHandle
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.NavigationTestExtension
import com.wire.android.media.CallRinger
-import com.wire.android.ui.calling.CallingNavArgs
-import com.wire.android.ui.navArgs
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase
import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase
@@ -48,47 +45,46 @@ import org.junit.jupiter.api.extension.ExtendWith
class InitiatingCallViewModelTest {
@Test
- fun `given an outgoing call, when the user ends call, then invoke endCall useCase and close the screen`() = runTest {
- // Given
- val (arrangement, viewModel) = Arrangement()
- .withEndingCall()
- .withStartCallSucceeding()
- .arrange()
-
- // When
- viewModel.hangUpCall()
- advanceUntilIdle()
-
- // Then
- with(arrangement) {
- coVerify(exactly = 1) { endCall(any()) }
- coVerify(exactly = 1) { callRinger.stop() }
+ fun `given an outgoing call, when the user ends call, then invoke endCall useCase and close the screen`() =
+ runTest {
+ // Given
+ val (arrangement, viewModel) = Arrangement()
+ .withEndingCall()
+ .withStartCallSucceeding()
+ .arrange()
+
+ // When
+ viewModel.hangUpCall()
+ advanceUntilIdle()
+
+ // Then
+ with(arrangement) {
+ coVerify(exactly = 1) { endCall(any()) }
+ coVerify(exactly = 1) { callRinger.stop() }
+ }
+ assertTrue { viewModel.state.flowState is InitiatingCallState.FlowState.CallClosed }
}
- assertTrue { viewModel.state.flowState is InitiatingCallState.FlowState.CallClosed }
- }
@Test
- fun `given a start call error, when user tries to start a call, call ring tone is not called`() = runTest {
- // Given
- val (arrangement, viewModel) = Arrangement()
- .withNoInternetConnection()
- .withStartCallSucceeding()
- .arrange()
-
- // When
- viewModel.initiateCall()
-
- // Then
- with(arrangement) {
- coVerify(exactly = 0) { callRinger.ring(any()) }
+ fun `given a start call error, when user tries to start a call, call ring tone is not called`() =
+ runTest {
+ // Given
+ val (arrangement, viewModel) = Arrangement()
+ .withNoInternetConnection()
+ .withStartCallSucceeding()
+ .arrange()
+
+ // When
+ viewModel.initiateCall()
+
+ // Then
+ with(arrangement) {
+ coVerify(exactly = 0) { callRinger.ring(any()) }
+ }
}
- }
private class Arrangement {
- @MockK
- private lateinit var savedStateHandle: SavedStateHandle
-
@MockK
private lateinit var establishedCalls: ObserveEstablishedCallsUseCase
@@ -104,9 +100,11 @@ class InitiatingCallViewModelTest {
@MockK
lateinit var endCall: EndCallUseCase
+ val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain")
+
val initiatingCallViewModel by lazy {
InitiatingCallViewModel(
- savedStateHandle = savedStateHandle,
+ conversationId = dummyConversationId,
observeEstablishedCalls = establishedCalls,
startCall = startCall,
endCall = endCall,
@@ -116,9 +114,7 @@ class InitiatingCallViewModelTest {
}
init {
- val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain")
MockKAnnotations.init(this)
- every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = dummyConversationId)
coEvery { isLastCallClosed.invoke(any(), any()) } returns flowOf(false)
coEvery { establishedCalls() } returns flowOf(emptyList())
every { callRinger.ring(any(), any(), any()) } returns Unit
diff --git a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt
index 7ad54890d65..7f5b25832c5 100644
--- a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt
@@ -103,11 +103,11 @@ class CommonTopAppBarViewModelTest {
}
@Test
- fun givenActiveCallAndCallScreenAndConnectivityIssues_whenGettingState_thenShouldHaveConnectivityInfo() = runTest {
+ fun givenActiveCallAndConnectivityIssues_whenGettingState_thenShouldHaveConnectivityInfo() = runTest {
val (_, commonTopAppBarViewModel) = Arrangement()
.withCurrentSessionExist()
.withActiveCall()
- .withCurrentScreen(CurrentScreen.OngoingCallScreen(mockk()))
+ .withCurrentScreen(CurrentScreen.Home)
.withSyncState(SyncState.Waiting)
.arrange()
@@ -115,7 +115,7 @@ class CommonTopAppBarViewModelTest {
val state = commonTopAppBarViewModel.state
val info = state.connectivityState
- info shouldBeInstanceOf ConnectivityUIState.WaitingConnection::class
+ info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class
}
@Test
diff --git a/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt
index 3c6cc6fb916..d918fc74744 100644
--- a/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelTest.kt
@@ -24,16 +24,20 @@ import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.auth.AccountInfo
import com.wire.kalium.logic.data.user.UserId
+import com.wire.kalium.logic.feature.UserSessionScope
import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase
import com.wire.kalium.logic.feature.legalhold.ApproveLegalHoldRequestUseCase
import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase
+import io.mockk.Called
import io.mockk.MockKAnnotations
import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
+import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
@@ -114,6 +118,7 @@ class LegalHoldRequestedViewModelTest {
state shouldBeInstanceOf LegalHoldRequestedState.Visible::class
state as LegalHoldRequestedState.Visible
state.requiresPassword shouldBeEqualTo true
+ state.acceptEnabled shouldBeEqualTo false
}
@Test
@@ -129,6 +134,7 @@ class LegalHoldRequestedViewModelTest {
state shouldBeInstanceOf LegalHoldRequestedState.Visible::class
state as LegalHoldRequestedState.Visible
state.requiresPassword shouldBeEqualTo false
+ state.acceptEnabled shouldBeEqualTo true
}
private fun arrangeWithLegalHoldRequest(isPasswordRequired: Boolean = true) = Arrangement()
@@ -234,18 +240,75 @@ class LegalHoldRequestedViewModelTest {
viewModel.state shouldBeInstanceOf LegalHoldRequestedState.Hidden::class
}
+ @Test
+ fun givenPasswordNotRequired_whenApproving_thenShouldNotValidatePasswordAndExecuteWithEmptyPassword() = runTest {
+ val (arrangement, viewModel) = arrangeWithLegalHoldRequest(isPasswordRequired = false)
+ .withApproveLegalHoldRequestResult(ApproveLegalHoldRequestUseCase.Result.Success)
+ .arrange()
+ advanceUntilIdle()
+ viewModel.acceptClicked()
+ verify { arrangement.validatePassword(any()) wasNot Called }
+ coVerify { arrangement.userSessionScope.approveLegalHoldRequest(matchNullable { it == null }) }
+ }
+
+ @Test
+ fun givenPasswordRequired_whenApproving_thenShouldValidatePasswordAndNotExecuteWithEmptyPassword() = runTest {
+ val (arrangement, viewModel) = arrangeWithLegalHoldRequest(isPasswordRequired = true)
+ .withValidatePasswordResult(ValidatePasswordResult.Invalid())
+ .withApproveLegalHoldRequestResult(ApproveLegalHoldRequestUseCase.Result.Success)
+ .arrange()
+ val password = "invalidpassword"
+ viewModel.passwordChanged(TextFieldValue(password))
+ advanceUntilIdle()
+ viewModel.acceptClicked()
+ verify { arrangement.validatePassword(password) }
+ coVerify { arrangement.userSessionScope.approveLegalHoldRequest(matchNullable { it.isNullOrEmpty() }) wasNot Called }
+ }
+
+ @Test
+ fun givenPasswordRequiredAndInvalidPassword_whenApproving_thenShouldNotExecuteWithInvalidPassword() = runTest {
+ val (arrangement, viewModel) = arrangeWithLegalHoldRequest(isPasswordRequired = true)
+ .withValidatePasswordResult(ValidatePasswordResult.Invalid())
+ .withApproveLegalHoldRequestResult(ApproveLegalHoldRequestUseCase.Result.Success)
+ .arrange()
+ val password = "invalidpassword"
+ viewModel.passwordChanged(TextFieldValue(password))
+ advanceUntilIdle()
+ viewModel.acceptClicked()
+ coVerify { arrangement.userSessionScope.approveLegalHoldRequest(password) wasNot Called }
+ }
+
+ @Test
+ fun givenPasswordRequiredAndValidPassword_whenApproving_thenShouldExecuteWithValidPassword() = runTest {
+ val (arrangement, viewModel) = arrangeWithLegalHoldRequest(isPasswordRequired = true)
+ .withValidatePasswordResult(ValidatePasswordResult.Valid)
+ .withApproveLegalHoldRequestResult(ApproveLegalHoldRequestUseCase.Result.Success)
+ .arrange()
+ val password = "ValidPassword123!"
+ viewModel.passwordChanged(TextFieldValue(password))
+ advanceUntilIdle()
+ viewModel.acceptClicked()
+ coVerify { arrangement.userSessionScope.approveLegalHoldRequest(password) }
+ }
+
private class Arrangement {
@MockK
- private lateinit var validatePassword: ValidatePasswordUseCase
+ lateinit var validatePassword: ValidatePasswordUseCase
+
+ @MockK
+ lateinit var coreLogic: CoreLogic
@MockK
- private lateinit var coreLogic: CoreLogic
+ lateinit var userSessionScope: UserSessionScope
+
+ val userId = UserId("userId", "domain")
val viewModel by lazy { LegalHoldRequestedViewModel(validatePassword, coreLogic) }
init {
MockKAnnotations.init(this)
+ every { coreLogic.getSessionScope(userId) } returns userSessionScope
}
fun withNotCurrentSession() = apply {
every { coreLogic.globalScope { session.currentSessionFlow() } } returns
@@ -257,19 +320,19 @@ class LegalHoldRequestedViewModelTest {
}
fun withCurrentSessionExists() = apply {
every { coreLogic.globalScope { session.currentSessionFlow() } } returns
- flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("userId", "domain"))))
+ flowOf(CurrentSessionResult.Success(AccountInfo.Valid(userId)))
}
fun withLegalHoldRequestResult(result: ObserveLegalHoldRequestUseCase.Result) = apply {
- every { coreLogic.getSessionScope(any()).observeLegalHoldRequest() } returns flowOf(result)
+ every { userSessionScope.observeLegalHoldRequest() } returns flowOf(result)
}
fun withIsPasswordRequiredResult(result: IsPasswordRequiredUseCase.Result) = apply {
- coEvery { coreLogic.getSessionScope(any()).users.isPasswordRequired() } returns result
+ coEvery { userSessionScope.users.isPasswordRequired() } returns result
}
fun withValidatePasswordResult(result: ValidatePasswordResult) = apply {
coEvery { validatePassword(any()) } returns result
}
fun withApproveLegalHoldRequestResult(result: ApproveLegalHoldRequestUseCase.Result) = apply {
- coEvery { coreLogic.getSessionScope(any()).approveLegalHoldRequest(any()) } returns result
+ coEvery { userSessionScope.approveLegalHoldRequest(any()) } returns result
}
fun arrange() = this to viewModel.apply { observeLegalHoldRequest() }
diff --git a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt
index 6fe11e9b580..f14c907c145 100644
--- a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt
+++ b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt
@@ -105,34 +105,6 @@ class DeepLinkProcessorTest {
)
}
- @Test
- fun `given a incoming call deeplink for current user, returns IncomingCall with conversationId and not switched account`() = runTest {
- val (arrangement, deepLinkProcessor) = Arrangement()
- .withIncomingCallDeepLink(CURRENT_USER_ID)
- .withCurrentSessionSuccess(CURRENT_USER_ID)
- .arrange()
- val incomingCallResult = deepLinkProcessor(arrangement.uri)
- assertInstanceOf(DeepLinkResult.IncomingCall::class.java, incomingCallResult)
- assertEquals(
- DeepLinkResult.IncomingCall(CONVERSATION_ID, false),
- incomingCallResult
- )
- }
-
- @Test
- fun `given a incoming call deeplink for other user, returns IncomingCall with conversationId and switched account`() = runTest {
- val (arrangement, deepLinkProcessor) = Arrangement()
- .withIncomingCallDeepLink(OTHER_USER_ID)
- .withCurrentSessionSuccess(CURRENT_USER_ID)
- .arrange()
- val incomingCallResult = deepLinkProcessor(arrangement.uri)
- assertInstanceOf(DeepLinkResult.IncomingCall::class.java, incomingCallResult)
- assertEquals(
- DeepLinkResult.IncomingCall(CONVERSATION_ID, true),
- incomingCallResult
- )
- }
-
@Test
fun `given a invalid deeplink, returns Unknown object`() = runTest {
val (arrangement, deepLinkProcessor) = Arrangement()
@@ -248,12 +220,6 @@ class DeepLinkProcessorTest {
coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns null
}
- fun withIncomingCallDeepLink(userId: UserId = CURRENT_USER_ID) = apply {
- coEvery { uri.host } returns DeepLinkProcessor.INCOMING_CALL_DEEPLINK_HOST
- coEvery { uri.lastPathSegment } returns CONVERSATION_ID.toString()
- coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns userId.toString()
- }
-
fun withConversationDeepLink(userId: UserId = CURRENT_USER_ID) = apply {
coEvery { uri.host } returns DeepLinkProcessor.CONVERSATION_DEEPLINK_HOST
coEvery { uri.lastPathSegment } returns CONVERSATION_ID.toString()
diff --git a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts
index ef75caaec30..fbd0013248d 100644
--- a/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts
+++ b/buildSrc/src/main/kotlin/scripts/compilation.gradle.kts
@@ -36,7 +36,7 @@ val dependenciesVersionTask = project.tasks.register("dependenciesVersionTask",
val catalog = catalogs.named("klibs")
val pairs = mapOf(
"avs" to catalog.findVersion("avs").get().requiredVersion,
- "core-crypto" to catalog.findVersion("core-crypto-multiplatform").get().requiredVersion
+ "core-crypto" to catalog.findVersion("core-crypto").get().requiredVersion
)
keyValues.set(pairs)
}
diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml
index 036cb7bb572..c7b56dc5bc3 100644
--- a/config/detekt/baseline.xml
+++ b/config/detekt/baseline.xml
@@ -433,7 +433,6 @@
MultiLineIfElse:WireTextField.kt$it
MultiLineIfElse:WriteStorageRequestFlow.kt$onGranted()
NoBlankLineBeforeRbrace:CreateAccountEmailViewState.kt$CreateAccountEmailViewState$
- NoBlankLineBeforeRbrace:DrawingCanvasBottomSheet.kt$
NoBlankLineBeforeRbrace:DrawingCanvasViewModel.kt$DrawingCanvasViewModel$
NoBlankLineBeforeRbrace:EmailComposer.kt$EmailComposer$
NoBlankLineBeforeRbrace:FakeKaliumFileSystem.kt$FakeKaliumFileSystem$
@@ -485,7 +484,6 @@
NoTrailingSpaces:ScalaServerConfigDAOTest.kt$ScalaServerConfigDAOTest.Arrangement$
NoUnusedImports:ApiVersioningDialogs.kt$com.wire.android.ui.server.ApiVersioningDialogs.kt
NoUnusedImports:CreateAccountDetailsScreen.kt$import com.wire.android.ui.common.scaffold.WireScaffold
- NoUnusedImports:DateTimeUtilKtTest.kt$com.wire.android.util.DateTimeUtilKtTest.kt
NoUnusedImports:SearchPeopleScreenState.kt$com.wire.android.ui.home.conversations.search.SearchPeopleScreenState.kt
NoUnusedImports:SpeakerButton.kt$com.wire.android.ui.calling.controlbuttons.SpeakerButton.kt
NoUnusedImports:ZoomableImage.kt$com.wire.android.ui.home.gallery.ZoomableImage.kt
@@ -642,6 +640,7 @@
SpacingBetweenDeclarationsWithAnnotations:UpdateReceiver.kt$UpdateReceiver$@Inject lateinit var migrationManager: MigrationManager
SpacingBetweenDeclarationsWithAnnotations:UpdateReceiver.kt$UpdateReceiver$@Inject lateinit var workManager: WorkManager
SpacingBetweenDeclarationsWithAnnotations:UriUtilTest.kt$UriUtilTest$@Test fun givenLinkWithoutParams_whenCallingFindParameterValue_thenReturnsParamValue()
+ SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val Chocolate = Color(0xFF622F00)
SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val DarkAmber100 = Color(0xFFFFF6D4)
SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val DarkAmber200 = Color(0xFFFFEEA8)
SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val DarkAmber300 = Color(0xFFFFE57D)
@@ -761,6 +760,9 @@
SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val LightRed700 = Color(0xFF74000B)
SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val LightRed800 = Color(0xFF4E0008)
SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val LightRed900 = Color(0xFF3A0006)
+ SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val Orange = Color(0xFFFD8312)
+ SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val Pink = Color(0xFFEB239B)
+ SpacingBetweenDeclarationsWithAnnotations:WireColorPalette.kt$WireColorPalette$@Stable val Turquoise = Color(0xFF01718E)
SpacingBetweenDeclarationsWithAnnotations:WirePrimaryIconButton.kt$@Preview @Composable fun PreviewWirePrimaryIconButtonLoading()
SpacingBetweenDeclarationsWithAnnotations:WirePrimaryIconButton.kt$@Preview @Composable fun PreviewWirePrimaryIconButtonRound()
SpreadOperator:ViewModelScopedPreview.kt$ViewModelScopedPreviewProcessor$(aggregating = true, *items.mapNotNull { it.containingFile }.toTypedArray())
@@ -779,7 +781,6 @@
UnusedParameter:LoginSSOScreen.kt$serverTitle: String
UnusedParameter:MentionScreen.kt$openConversationNotificationsSettings: (ConversationItem) -> Unit
UnusedParameter:ShouldTriggerMigrationForUserUserCaseTest.kt$ShouldTriggerMigrationForUserUserCaseTest.Arrangement$version: Int?
- UnusedParameter:SwipeableSnackbar.kt$onDismiss: () -> Unit = { hostState.currentSnackbarData?.dismiss() }
UnusedParameter:TrackingNavController.kt$nameFromRoute: (String) -> String?
UnusedParameter:WireButtonDefaults.kt$WireButtonColors$interactionSource: InteractionSource
UnusedParameter:WireItemLabel.kt$minHeight: Dp = dimensions().badgeSmallMinSize.height
@@ -796,9 +797,6 @@
UnusedPrivateProperty:SelfUserProfileViewModel.kt$SelfUserProfileViewModel$private val notificationChannelsManager: NotificationChannelsManager
UnusedPrivateProperty:SendMessageViewModel.kt$SendMessageViewModel.Companion$private const val sizeOf1MB = 1024 * 1024
UnusedPrivateProperty:SendMessageViewModelArrangement.kt$SendMessageViewModelArrangement$@MockK private lateinit var savedStateHandle: SavedStateHandle
- UnusedPrivateProperty:SwipeableSnackbar.kt$val currentScreenWidth = with(density) { configuration.screenWidthDp.dp.toPx() }
- UnusedPrivateProperty:SwipeableSnackbar.kt$val positionalThreshold: (Float) -> Float = { distance -> distance * 0.5f }
- UnusedPrivateProperty:SwipeableSnackbar.kt$val velocityThreshold: () -> Float = with(density) { { 125.dp.toPx() } }
UnusedPrivateProperty:WireNotificationManagerTest.kt$WireNotificationManagerTest.Companion$private val TEST_SERVER_CONFIG: ServerConfig = newServerConfig(1)
Wrapping:AddMembersToConversationViewModel.kt$AddMembersToConversationViewModel$(
Wrapping:AssetImageFetcherTest.kt$AssetImageFetcherTest.Arrangement$( data = imageData, options ?: Options( context = mockContext, parameters = Parameters.Builder().set(key = OPTION_PARAMETER_RETRY_KEY, value = 0, memoryCacheKey = null).build() ) )
diff --git a/core/.gitkeep b/core/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/Extensions.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/Extensions.kt
index e6af44b0eb5..55b4ba6dd17 100644
--- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/Extensions.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/Extensions.kt
@@ -18,6 +18,8 @@
package com.wire.android.ui.common
import android.widget.Toast
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -27,6 +29,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import com.wire.android.model.ClickBlockParams
+import com.wire.android.model.Clickable
import com.wire.android.util.LocalSyncStateObserver
@Composable
@@ -47,10 +50,26 @@ fun rememberClickBlockAction(clickBlockParams: ClickBlockParams, clickAction: ()
when {
clickBlockParams.blockWhenConnecting && syncStateObserver.isConnecting ->
Toast.makeText(context, context.getString(R.string.label_wait_until_connected), Toast.LENGTH_SHORT).show()
+
clickBlockParams.blockWhenSyncing && syncStateObserver.isSyncing ->
Toast.makeText(context, context.getString(R.string.label_wait_until_synchronised), Toast.LENGTH_SHORT).show()
+
else -> clickAction()
}
}
}
}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun Modifier.clickable(clickable: Clickable?) = clickable?.let {
+ val onClick = rememberClickBlockAction(clickable.clickBlockParams, clickable.onClick)
+ val onLongClick = clickable.onLongClick?.let { onLongClick ->
+ rememberClickBlockAction(clickable.clickBlockParams, onLongClick)
+ }
+ this.combinedClickable(
+ enabled = clickable.enabled,
+ onClick = onClick,
+ onLongClick = onLongClick
+ )
+} ?: this
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
similarity index 65%
rename from app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
index 14f24856b50..79abc2029a5 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
@@ -31,17 +31,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalUriHandler
@@ -49,9 +45,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.wire.android.ui.common.button.WireButtonState
@@ -59,9 +53,6 @@ import com.wire.android.ui.common.button.WirePrimaryButton
import com.wire.android.ui.common.button.WireSecondaryButton
import com.wire.android.ui.common.button.WireTertiaryButton
import com.wire.android.ui.common.progress.WireCircularProgressIndicator
-import com.wire.android.ui.common.textfield.WirePasswordTextField
-import com.wire.android.ui.markdown.MarkdownConstants
-import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
@@ -160,7 +151,7 @@ fun WireDialog(
}
@Composable
-private fun WireDialogContent(
+fun WireDialogContent(
title: String,
titleLoading: Boolean = false,
text: AnnotatedString? = null,
@@ -209,7 +200,7 @@ private fun WireDialogContent(
modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.dialogTextsSpacing),
onClick = { offset ->
text.getStringAnnotations(
- tag = MarkdownConstants.TAG_URL,
+ tag = TAG_URL,
start = offset,
end = offset,
).firstOrNull()?.let { result -> uriHandler.openUri(result.item) }
@@ -299,143 +290,6 @@ private fun WireDialogButtonProperties?.getButton(modifier: Modifier = Modifier)
}
}
-@OptIn(ExperimentalComposeUiApi::class)
-@Preview(showBackground = true)
-@Composable
-fun PreviewWireDialog() {
- var password by remember { mutableStateOf(TextFieldValue("")) }
- WireTheme {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier.fillMaxWidth()
- ) {
- WireDialogContent(
- optionButton1Properties = WireDialogButtonProperties(
- text = "OK",
- onClick = { },
- type = WireDialogButtonType.Primary,
- state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
- ),
- dismissButtonProperties = WireDialogButtonProperties(
- text = "Cancel",
- onClick = { }
- ),
- title = "title",
- text = buildAnnotatedString {
- val style = SpanStyle(
- color = colorsScheme().onBackground,
- fontWeight = MaterialTheme.wireTypography.body01.fontWeight,
- fontSize = MaterialTheme.wireTypography.body01.fontSize,
- fontFamily = MaterialTheme.wireTypography.body01.fontFamily,
- fontStyle = MaterialTheme.wireTypography.body01.fontStyle
- )
- withStyle(style) { append("text\nsecond line\nthirdLine\nfourth line\nfifth line\nsixth line\nseventh line") }
- },
- ) {
- WirePasswordTextField(
- value = password,
- onValueChange = { password = it },
- autofill = false
- )
- }
- }
- }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Preview(showBackground = true)
-@Composable
-fun PreviewWireDialogWith2OptionButtons() {
- var password by remember { mutableStateOf(TextFieldValue("")) }
- WireTheme {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier.fillMaxWidth()
- ) {
- WireDialogContent(
- optionButton1Properties = WireDialogButtonProperties(
- text = "OK",
- onClick = { },
- type = WireDialogButtonType.Primary,
- state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
- ),
- optionButton2Properties = WireDialogButtonProperties(
- text = "Later",
- onClick = { },
- type = WireDialogButtonType.Primary,
- state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
- ),
- dismissButtonProperties = WireDialogButtonProperties(
- text = "Cancel",
- onClick = { }
- ),
- title = "title",
- text = buildAnnotatedString {
- val style = SpanStyle(
- color = colorsScheme().onBackground,
- fontWeight = MaterialTheme.wireTypography.body01.fontWeight,
- fontSize = MaterialTheme.wireTypography.body01.fontSize,
- fontFamily = MaterialTheme.wireTypography.body01.fontFamily,
- fontStyle = MaterialTheme.wireTypography.body01.fontStyle
- )
- withStyle(style) { append("text") }
- },
- buttonsHorizontalAlignment = false
- ) {
- WirePasswordTextField(
- value = password,
- onValueChange = { password = it },
- autofill = true
- )
- }
- }
- }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Preview(showBackground = true)
-@Composable
-fun PreviewWireDialogCentered() {
- var password by remember { mutableStateOf(TextFieldValue("")) }
- WireTheme {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier.fillMaxWidth()
- ) {
- WireDialogContent(
- optionButton1Properties = WireDialogButtonProperties(
- text = "OK",
- onClick = { },
- type = WireDialogButtonType.Primary,
- state = if (password.text.isEmpty()) WireButtonState.Disabled else WireButtonState.Error,
- ),
- dismissButtonProperties = WireDialogButtonProperties(
- text = "Cancel",
- onClick = { }
- ),
- centerContent = true,
- title = "title",
- text = buildAnnotatedString {
- val style = SpanStyle(
- color = colorsScheme().onBackground,
- fontWeight = MaterialTheme.wireTypography.body01.fontWeight,
- fontSize = MaterialTheme.wireTypography.body01.fontSize,
- fontFamily = MaterialTheme.wireTypography.body01.fontFamily,
- fontStyle = MaterialTheme.wireTypography.body01.fontStyle
- )
- withStyle(style) { append("text\nsecond line\nthirdLine\nfourth line\nfifth line\nsixth line\nseventh line") }
- },
- ) {
- WirePasswordTextField(
- value = password,
- onValueChange = { password = it },
- autofill = false
- )
- }
- }
- }
-}
-
enum class WireDialogButtonType { Primary, Secondary, Tertiary }
data class WireDialogButtonProperties(
@@ -445,3 +299,6 @@ data class WireDialogButtonProperties(
val type: WireDialogButtonType = WireDialogButtonType.Secondary,
val loading: Boolean = false
)
+
+// todo, replace when markdown is moved to commons
+private const val TAG_URL = "linkTag"
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt
similarity index 80%
rename from app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt
index 3ec23ab13fe..0d4a3b67566 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/MenuBottomSheetItem.kt
@@ -37,16 +37,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import com.wire.android.R
import com.wire.android.model.ClickBlockParams
import com.wire.android.model.Clickable
-import com.wire.android.ui.common.ArrowRightIcon
import com.wire.android.ui.common.clickable
-import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.divider.WireDivider
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
@@ -130,30 +125,3 @@ fun MenuItemIcon(
.then(modifier)
)
}
-
-@Preview
-@Composable
-fun PreviewMenuBottomSheetItem() {
- MenuBottomSheetItem(
- title = "very long looooooong title",
- icon = {
- MenuItemIcon(
- id = R.drawable.ic_erase,
- contentDescription = "",
- )
- },
- action = {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = "very long looooooong action",
- style = MaterialTheme.wireTypography.body01,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- modifier = Modifier.weight(weight = 1f, fill = false)
- )
- Spacer(modifier = Modifier.size(dimensions().spacing16x))
- ArrowRightIcon()
- }
- }
- )
-}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt
similarity index 100%
rename from app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/ModalSheetHeaderItem.kt
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetDefaults.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetDefaults.kt
similarity index 100%
rename from app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetDefaults.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetDefaults.kt
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetScaffold.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetScaffold.kt
similarity index 100%
rename from app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetScaffold.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireBottomSheetScaffold.kt
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt
similarity index 82%
rename from app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt
index 22d45749928..e8de411621f 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetLayout.kt
@@ -30,13 +30,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import com.wire.android.ui.common.colorsScheme
-import com.wire.android.ui.common.dimensions
-import com.wire.android.ui.edit.ReactionOption
-import com.wire.android.ui.home.conversationslist.common.GroupConversationAvatar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -98,21 +93,3 @@ fun MenuModalSheetContent(
buildMenuSheetItems(items = menuItems)
}
}
-
-@Preview
-@Composable
-fun PreviewMenuModalSheetContentWithoutHeader() {
- MenuModalSheetContent(
- MenuModalSheetHeader.Gone,
- listOf { ReactionOption({}) }
- )
-}
-
-@Preview
-@Composable
-fun PreviewMenuModalSheetContentWithHeader() {
- MenuModalSheetContent(
- MenuModalSheetHeader.Visible("Title", { GroupConversationAvatar(colorsScheme().primary) }, dimensions().spacing8x),
- listOf { ReactionOption({}) }
- )
-}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt
similarity index 100%
rename from app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/bottomsheet/WireModalSheetState.kt
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/divider/WireDivider.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/divider/WireDivider.kt
similarity index 100%
rename from app/src/main/kotlin/com/wire/android/ui/common/divider/WireDivider.kt
rename to core/ui-common/src/main/kotlin/com/wire/android/ui/common/divider/WireDivider.kt
diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt
index 4fb5a1af31a..189689e2e7c 100644
--- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt
@@ -309,4 +309,15 @@ object WireColorPalette {
@Stable
val BlackAlpha55 = Color(0x8C000000)
+
+ @Stable
+ val Brown = Color(0xFFA25915)
+ @Stable
+ val Chocolate = Color(0xFF622F00)
+ @Stable
+ val Orange = Color(0xFFFD8312)
+ @Stable
+ val Pink = Color(0xFFEB239B)
+ @Stable
+ val Turquoise = Color(0xFF01718E)
}
diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt
index 5c06715738b..6977a7ba6c8 100644
--- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt
+++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt
@@ -104,7 +104,8 @@ data class WireColorScheme(
val validE2eiStatusColor: Color,
val mlsVerificationTextColor: Color,
val wireAccentColors: WireAccentColors,
- val checkboxTextDisabled: Color
+ val checkboxTextDisabled: Color,
+ val sketchColorPalette: List
) {
fun toColorScheme(): ColorScheme = ColorScheme(
primary = primary,
@@ -251,7 +252,27 @@ private val LightWireColorScheme = WireColorScheme(
Accent.Unknown -> WireColorPalette.LightBlue500
}
},
- checkboxTextDisabled = WireColorPalette.Gray70
+ checkboxTextDisabled = WireColorPalette.Gray70,
+ sketchColorPalette = listOf(
+ Color.Black,
+ Color.White,
+ WireColorPalette.LightBlue500,
+ WireColorPalette.LightGreen550,
+ WireColorPalette.DarkAmber500,
+ WireColorPalette.LightRed500,
+ WireColorPalette.Orange,
+ WireColorPalette.LightPurple600,
+ WireColorPalette.Brown,
+ WireColorPalette.Turquoise,
+ WireColorPalette.DarkBlue500,
+ WireColorPalette.DarkGreen500,
+ WireColorPalette.DarkPetrol500,
+ WireColorPalette.DarkPurple500,
+ WireColorPalette.DarkRed500,
+ WireColorPalette.Pink,
+ WireColorPalette.Chocolate,
+ WireColorPalette.Gray70,
+ )
)
// Dark WireColorScheme
@@ -372,7 +393,27 @@ private val DarkWireColorScheme = WireColorScheme(
Accent.Unknown -> WireColorPalette.DarkBlue500
}
},
- checkboxTextDisabled = WireColorPalette.Gray70
+ checkboxTextDisabled = WireColorPalette.Gray70,
+ sketchColorPalette = listOf(
+ Color.Black,
+ Color.White,
+ WireColorPalette.LightBlue500,
+ WireColorPalette.LightGreen550,
+ WireColorPalette.DarkAmber500,
+ WireColorPalette.LightRed500,
+ WireColorPalette.Orange,
+ WireColorPalette.LightPurple600,
+ WireColorPalette.Brown,
+ WireColorPalette.Turquoise,
+ WireColorPalette.DarkBlue500,
+ WireColorPalette.DarkGreen500,
+ WireColorPalette.DarkPetrol500,
+ WireColorPalette.DarkPurple500,
+ WireColorPalette.DarkRed500,
+ WireColorPalette.Pink,
+ WireColorPalette.Chocolate,
+ WireColorPalette.Gray70,
+ )
)
@PackagePrivate
diff --git a/features/sketch/build.gradle.kts b/features/sketch/build.gradle.kts
index 69fd7f00ca7..5f7e54c713a 100644
--- a/features/sketch/build.gradle.kts
+++ b/features/sketch/build.gradle.kts
@@ -4,9 +4,11 @@ plugins {
}
dependencies {
+ implementation(project(":core:ui-common"))
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.material)
+ implementation(libs.ktx.immutableCollections)
val composeBom = platform(libs.compose.bom)
implementation(composeBom)
@@ -15,9 +17,12 @@ dependencies {
implementation(libs.compose.ui.graphics)
implementation(libs.compose.material.core)
implementation(libs.compose.material3)
+ implementation(libs.compose.material.icons)
implementation(libs.androidx.lifecycle.viewModelCompose)
+ implementation(libs.compose.ui.preview)
testImplementation(libs.junit4)
+ testImplementation(libs.coroutines.test)
androidTestImplementation(libs.androidx.test.extJunit)
androidTestImplementation(libs.androidx.espresso.core)
}
diff --git a/features/sketch/src/main/AndroidManifest.xml b/features/sketch/src/main/AndroidManifest.xml
index 44b6ed94db1..bb266d68bbf 100644
--- a/features/sketch/src/main/AndroidManifest.xml
+++ b/features/sketch/src/main/AndroidManifest.xml
@@ -15,6 +15,12 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see http://www.gnu.org/licenses/.
-->
-
+
+
+
diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasBottomSheet.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasBottomSheet.kt
index 614e5b3f945..15d60f7361a 100644
--- a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasBottomSheet.kt
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasBottomSheet.kt
@@ -17,70 +17,228 @@
*/
package com.wire.android.feature.sketch
+import android.net.Uri
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CutCornerShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Send
+import androidx.compose.material.icons.filled.Circle
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.window.SecureFlagPolicy
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.wire.android.feature.sketch.model.DrawingState
+import com.wire.android.model.ClickBlockParams
+import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState
+import com.wire.android.ui.common.button.IconAlignment
+import com.wire.android.ui.common.button.WireButtonState
+import com.wire.android.ui.common.button.WirePrimaryIconButton
+import com.wire.android.ui.common.button.WireSecondaryButton
+import com.wire.android.ui.common.button.WireSecondaryIconButton
+import com.wire.android.ui.common.button.WireTertiaryIconButton
+import com.wire.android.ui.common.button.wireSendPrimaryButtonColors
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.theme.wireDimensions
+import com.wire.android.ui.theme.wireTypography
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawingCanvasBottomSheet(
onDismissSketch: () -> Unit,
- onSendSketch: () -> Unit
+ onSendSketch: (Uri) -> Unit,
+ tempWritableImageUri: Uri?,
+ conversationTitle: String = "",
+ viewModel: DrawingCanvasViewModel = viewModel(),
) {
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true, confirmValueChange = { false })
+ val onDismissEvent: () -> Unit = remember {
+ {
+ if (viewModel.state.paths.isNotEmpty()) {
+ viewModel.onShowConfirmationDialog()
+ } else {
+ scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissSketch() }
+ }
+ }
+ }
+ val dismissEvent: () -> Unit = remember {
+ {
+ viewModel.initializeCanvas()
+ onDismissSketch()
+ }
+ }
+
ModalBottomSheet(
+ shape = CutCornerShape(dimensions().spacing0x),
+ containerColor = colorsScheme().background,
dragHandle = {
- Row(
- Modifier
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- val scope = rememberCoroutineScope()
- IconButton(
- onClick = {
- scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissSketch() }
- },
- ) {
- Icon(
- Icons.Filled.Close,
- contentDescription = stringResource(
- com.google.android.material.R.string.mtrl_picker_cancel
- )
- )
- }
- IconButton(
- onClick = {
- onSendSketch()
- },
- ) {
- Icon(
- Icons.Filled.Send,
- contentDescription = stringResource(
- com.google.android.material.R.string.mtrl_picker_cancel
- )
- )
- }
-
- }
+ DrawingTopBar(conversationTitle, onDismissEvent, viewModel::onUndoLastStroke, viewModel.state)
},
sheetState = sheetState,
- onDismissRequest = {
- onDismissSketch()
+ onDismissRequest = onDismissEvent,
+ properties = ModalBottomSheetProperties(
+ isFocusable = true,
+ securePolicy = SecureFlagPolicy.SecureOn,
+ shouldDismissOnBackPress = false
+ )
+ ) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .weight(weight = 1f, fill = true)
+ ) {
+ DrawingCanvasComponent(
+ state = viewModel.state,
+ onStartDrawingEvent = viewModel::onStartDrawingEvent,
+ onDrawEvent = viewModel::onDrawEvent,
+ onStopDrawingEvent = viewModel::onStopDrawingEvent,
+ onSizeChanged = viewModel::onSizeChanged,
+ onStartDrawing = viewModel::onStartDrawing,
+ onDraw = viewModel::onDraw,
+ onStopDrawing = viewModel::onStopDrawing
+ )
}
+ DrawingToolbar(
+ state = viewModel.state,
+ onColorChanged = viewModel::onColorChanged,
+ onSendSketch = {
+ scope.launch { onSendSketch(viewModel.saveImage(context, tempWritableImageUri)) }
+ .invokeOnCompletion { scope.launch { sheetState.hide() } }
+ }
+ )
+ }
+
+ if (viewModel.state.showConfirmationDialog) {
+ DiscardDialogConfirmation(scope, sheetState, dismissEvent, viewModel::onHideConfirmationDialog)
+ }
+}
+
+@Composable
+internal fun DrawingTopBar(
+ conversationTitle: String,
+ dismissAction: () -> Unit,
+ onUndoStroke: () -> Unit,
+ state: DrawingState
+) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = dimensions().spacing8x),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ WireTertiaryIconButton(
+ onButtonClicked = dismissAction,
+ iconResource = R.drawable.ic_close,
+ contentDescription = R.string.content_description_close_button,
+ minSize = MaterialTheme.wireDimensions.buttonCircleMinSize,
+ minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize,
+ )
+ Text(
+ text = conversationTitle,
+ style = MaterialTheme.wireTypography.title01,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ maxLines = MAX_LINES_TOPBAR,
+ overflow = TextOverflow.Ellipsis
+ )
+ WireSecondaryIconButton(
+ onButtonClicked = onUndoStroke,
+ iconResource = R.drawable.ic_undo,
+ contentDescription = R.string.content_description_undo_button,
+ state = if (state.paths.isNotEmpty()) WireButtonState.Default else WireButtonState.Disabled,
+ minSize = MaterialTheme.wireDimensions.buttonCircleMinSize,
+ minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize,
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DrawingToolbar(
+ state: DrawingState,
+ onColorChanged: (Color) -> Unit,
+ onSendSketch: () -> Unit = {},
+) {
+ val scope = rememberCoroutineScope()
+ val sheetState = rememberWireModalSheetState()
+ val openColorPickerSheet: () -> Unit = remember { { scope.launch { sheetState.show() } } }
+ val closeColorPickerSheet: () -> Unit = remember { { scope.launch { sheetState.hide() } } }
+ Row(
+ Modifier
+ .height(dimensions().spacing80x)
+ .padding(horizontal = dimensions().spacing8x)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
) {
- DrawingCanvasComponent()
+ WireSecondaryButton(
+ onClick = openColorPickerSheet,
+ leadingIcon = {
+ Icon(
+ Icons.Default.Circle,
+ null,
+ tint = state.currentPath.color,
+ modifier = Modifier
+ .border(
+ shape = CircleShape,
+ border = BorderStroke(dimensions().spacing1x, colorsScheme().secondaryText)
+ )
+ )
+ },
+ leadingIconAlignment = IconAlignment.Center,
+ fillMaxWidth = false,
+ minSize = dimensions().buttonSmallMinSize,
+ minClickableSize = dimensions().buttonMinClickableSize,
+ shape = RoundedCornerShape(dimensions().spacing12x),
+ contentPadding = PaddingValues(horizontal = dimensions().spacing8x, vertical = dimensions().spacing4x),
+ )
+ Spacer(Modifier.size(dimensions().spacing2x))
+ WirePrimaryIconButton(
+ onButtonClicked = onSendSketch,
+ iconResource = R.drawable.ic_send,
+ contentDescription = R.string.content_description_send_button,
+ state = if (state.paths.isNotEmpty()) WireButtonState.Default else WireButtonState.Disabled,
+ shape = RoundedCornerShape(dimensions().spacing20x),
+ colors = wireSendPrimaryButtonColors(),
+ clickBlockParams = ClickBlockParams(blockWhenSyncing = true, blockWhenConnecting = true),
+ minSize = MaterialTheme.wireDimensions.buttonCircleMinSize,
+ minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize,
+ )
}
+
+ DrawingToolPicker(
+ sheetState = sheetState,
+ currentColor = state.currentPath.color,
+ onColorSelected = {
+ onColorChanged(it)
+ closeColorPickerSheet()
+ }
+ )
}
+
+private const val MAX_LINES_TOPBAR = 1
diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasComponent.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasComponent.kt
index 7268c8c62fa..91b7520cc2a 100644
--- a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasComponent.kt
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasComponent.kt
@@ -24,60 +24,134 @@ import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.VectorPainter
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
-import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextMeasurer
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.unit.toSize
import com.wire.android.feature.sketch.model.DrawingMotionEvent
+import com.wire.android.feature.sketch.model.DrawingState
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.theme.wireTypography
@Composable
internal fun DrawingCanvasComponent(
- viewModel: DrawingCanvasViewModel = viewModel()
+ state: DrawingState,
+ onStartDrawingEvent: () -> Unit,
+ onDrawEvent: () -> Unit,
+ onStopDrawingEvent: () -> Unit,
+ onSizeChanged: (Size) -> Unit,
+ onStartDrawing: (Offset) -> Unit,
+ onDraw: (Offset) -> Unit,
+ onStopDrawing: () -> Unit
) {
- with(viewModel.state) {
- val drawModifier = Modifier
- .fillMaxSize()
- .clipToBounds() // necessary to draw inside the canvas.
- .background(MaterialTheme.colorScheme.background)
- .pointerInput(Unit) {
- awaitEachGesture {
- handleGestures(
- viewModel::onStartDrawing,
- viewModel::onDraw,
- viewModel::onStopDrawing
+ CanvasLayout(
+ state = state,
+ onStartDrawingEvent = onStartDrawingEvent,
+ onDrawEvent = onDrawEvent,
+ onStopDrawingEvent = onStopDrawingEvent,
+ onSizeChanged = onSizeChanged,
+ onStartDrawing = onStartDrawing,
+ onDraw = onDraw,
+ onStopDrawing = onStopDrawing
+ )
+}
+
+@Composable
+private fun CanvasLayout(
+ state: DrawingState,
+ onStartDrawingEvent: () -> Unit,
+ onDrawEvent: () -> Unit,
+ onStopDrawingEvent: () -> Unit,
+ onSizeChanged: (Size) -> Unit,
+ onStartDrawing: (Offset) -> Unit,
+ onDraw: (Offset) -> Unit,
+ onStopDrawing: () -> Unit
+) = with(state) {
+ val textMeasurer = rememberTextMeasurer()
+ val emptyCanvasText = stringResource(id = R.string.sketch_details_empty_text)
+ val emptyCanvasStyle = MaterialTheme.wireTypography.body01.copy(color = colorsScheme().secondaryText)
+ val textLayoutResult = remember(emptyCanvasText) {
+ textMeasurer.measure(emptyCanvasText, emptyCanvasStyle)
+ }
+ val vector = ImageVector.vectorResource(id = R.drawable.ic_long_arrow)
+ val painter = rememberVectorPainter(image = vector)
+ val drawModifier = Modifier
+ .fillMaxSize()
+ .clipToBounds() // necessary to draw inside the canvas.
+ .background(Color.White)
+ .onSizeChanged { onSizeChanged(it.toSize()) }
+ .pointerInput(Unit) { awaitEachGesture { handleGestures(onStartDrawing, onDraw, onStopDrawing) } }
+ Canvas(modifier = drawModifier) {
+ with(drawContext.canvas.nativeCanvas) {
+ val checkPoint = saveLayer(null, null)
+ when (drawingMotionEvent) {
+ DrawingMotionEvent.Idle -> Unit
+ DrawingMotionEvent.Down -> onStartDrawingEvent()
+ DrawingMotionEvent.Move -> {
+ onDrawEvent()
+ // todo: draw with selected properties, out of scope for this first ticket.
+ drawCircle(
+ center = currentPosition,
+ color = Color.Gray,
+ radius = currentPath.strokeWidth / 2,
+ style = Stroke(width = 1f)
)
}
- }
- Canvas(modifier = drawModifier) {
- with(drawContext.canvas.nativeCanvas) {
- val checkPoint = saveLayer(null, null)
- when (drawingMotionEvent) {
- DrawingMotionEvent.Idle -> Unit
- DrawingMotionEvent.Down -> viewModel.onStartDrawingEvent()
- DrawingMotionEvent.Move -> {
- viewModel.onDrawEvent()
- // todo: draw with selected properties, out of scope for this first ticket.
- drawCircle(
- center = currentPosition,
- color = Color.Gray,
- radius = currentPath.strokeWidth / 2,
- style = Stroke(width = 1f)
- )
- }
- DrawingMotionEvent.Up -> viewModel.onStopDrawingEvent()
- }
- paths.forEach { path ->
- path.draw(this@Canvas /*, bitmap*/)
- }
- restoreToCount(checkPoint)
+ DrawingMotionEvent.Up -> onStopDrawingEvent()
}
+ paths.forEach { path ->
+ path.draw(this@Canvas /*, bitmap*/)
+ }
+ restoreToCount(checkPoint)
+ }
+ if (paths.isEmpty()) {
+ emptyCanvasState(textMeasurer, emptyCanvasText, emptyCanvasStyle, textLayoutResult, painter)
+ }
+ }
+}
+
+private fun DrawScope.emptyCanvasState(
+ textMeasurer: TextMeasurer,
+ emptyCanvasText: String,
+ emptyCanvasStyle: TextStyle,
+ textLayoutResult: TextLayoutResult,
+ painter: VectorPainter
+) {
+ drawText(
+ textMeasurer = textMeasurer,
+ text = emptyCanvasText,
+ style = emptyCanvasStyle,
+ topLeft = Offset(
+ x = center.x - textLayoutResult.size.width / 2,
+ y = center.y - textLayoutResult.size.height / 2
+ )
+ )
+ // todo. uncomment when figure it out how to position this correctly on the canvas.
+ val enabled = false
+ if (enabled) {
+ with(painter) {
+ draw(painter.intrinsicSize, alpha = .5f)
}
}
}
@@ -98,13 +172,9 @@ private suspend fun AwaitPointerEventScope.handleGestures(
do {
val event = awaitPointerEvent()
onDraw(event.changes.first().position)
- val hasNewLineDraw = event.changes
- .first()
- .positionChange() != Offset.Zero
+ val hasNewLineDraw = event.changes.first().positionChange() != Offset.Zero
if (hasNewLineDraw) {
- event.changes
- .first()
- .consume()
+ event.changes.first().consume()
}
} while (event.changes.any { it.pressed })
onStopDrawing()
diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt
index 74fe6f6cc2c..f3183943b26 100644
--- a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt
@@ -17,20 +17,53 @@
*/
package com.wire.android.feature.sketch
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.net.Uri
+import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.wire.android.feature.sketch.model.DrawingMotionEvent
import com.wire.android.feature.sketch.model.DrawingPathProperties
import com.wire.android.feature.sketch.model.DrawingState
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileOutputStream
-internal class DrawingCanvasViewModel : ViewModel() {
+@Suppress("TooManyFunctions")
+class DrawingCanvasViewModel : ViewModel() {
- var state: DrawingState by mutableStateOf(DrawingState())
+ internal var state: DrawingState by mutableStateOf(DrawingState())
private set
+ init {
+ initializeCanvas()
+ }
+
+ fun initializeCanvas() {
+ state = DrawingState(currentPath = DrawingPathProperties())
+ }
+
+ fun onShowConfirmationDialog() {
+ state = state.copy(showConfirmationDialog = true)
+ }
+
+ fun onHideConfirmationDialog() {
+ state = state.copy(showConfirmationDialog = false)
+ }
+
/**
* Marks the start of the drawing.
*/
@@ -84,4 +117,80 @@ internal class DrawingCanvasViewModel : ViewModel() {
)
}
+ /**
+ * Sets the canvas size or modifies it if zoom is implemented.
+ */
+ fun onSizeChanged(canvasSize: Size) {
+ state = state.copy(canvasSize = canvasSize)
+ }
+
+ /**
+ * Undoes the last stroke.
+ */
+ fun onUndoLastStroke() {
+ if (state.paths.isNotEmpty()) {
+ state = state.copy(
+ paths = state.paths.dropLast(1),
+ pathsUndone = state.pathsUndone + state.paths.last()
+ )
+ }
+ }
+
+ /**
+ * Saves the image to the provided URI and resets the canvas.
+ *
+ * @param context The context to use to open the file descriptor.
+ * @param tempWritableImageUri The URI to save the image to.
+ *
+ * @return The [Uri] of the saved image.
+ */
+ suspend fun saveImage(context: Context, tempWritableImageUri: Uri?): Uri {
+ val tempSketchFile = tempWritableImageUri.orTempUri(context)
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ with(state) {
+ if (canvasSize == null || state.paths.isEmpty()) return@withContext
+
+ val bitmap = Bitmap.createBitmap(
+ canvasSize!!.width.toInt(),
+ canvasSize!!.height.toInt(),
+ Bitmap.Config.ARGB_8888
+ )
+ val canvas = Canvas(bitmap).apply { drawPaint(Paint().apply { color = Color.White.toArgb() }) }
+ context.contentResolver.openFileDescriptor(tempSketchFile, "rw")?.use { fileDescriptor ->
+ FileOutputStream(fileDescriptor.fileDescriptor).use { fileOutputStream ->
+ paths.forEach { path -> path.drawNative(canvas) }
+ bitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY, fileOutputStream)
+ fileOutputStream.flush()
+ }.also {
+ Log.d("DrawingCanvasViewModel", "Image written to: $tempSketchFile")
+ }
+ }
+ }
+ }
+ }.join()
+ initializeCanvas()
+ return tempSketchFile
+ }
+
+ fun onColorChanged(selectedColor: Color) {
+ state = state.copy(
+ currentPath = DrawingPathProperties().apply {
+ strokeWidth = state.currentPath.strokeWidth
+ color = selectedColor
+ drawMode = state.currentPath.drawMode
+ }
+ )
+ }
+
+ private fun Uri?.orTempUri(context: Context): Uri = this ?: run {
+ val tempFile = File.createTempFile("temp_sketch", ".jpg", context.cacheDir)
+ tempFile.deleteOnExit()
+ tempFile.toUri()
+ }
+
+ companion object {
+ private const val QUALITY = 50
+ }
+
}
diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingDiscardDialog.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingDiscardDialog.kt
new file mode 100644
index 00000000000..bd4747bf777
--- /dev/null
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingDiscardDialog.kt
@@ -0,0 +1,61 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.feature.sketch
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.SheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.window.DialogProperties
+import com.wire.android.ui.common.WireDialog
+import com.wire.android.ui.common.WireDialogButtonProperties
+import com.wire.android.ui.common.WireDialogButtonType
+import com.wire.android.ui.common.button.WireButtonState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DiscardDialogConfirmation(
+ scope: CoroutineScope,
+ sheetState: SheetState,
+ onDismissSketch: () -> Unit,
+ onHideConfirmationDialog: () -> Unit,
+) {
+ WireDialog(
+ title = stringResource(id = R.string.confirm_changes_title),
+ text = stringResource(id = R.string.confirm_changes_text),
+ onDismiss = onHideConfirmationDialog,
+ optionButton1Properties = WireDialogButtonProperties(
+ onClick = {
+ scope.launch { sheetState.hide() }.invokeOnCompletion {
+ onHideConfirmationDialog()
+ onDismissSketch()
+ }
+ },
+ text = stringResource(id = R.string.confirm_changes_dismiss),
+ type = WireDialogButtonType.Primary,
+ state = WireButtonState.Error
+ ),
+ dismissButtonProperties = WireDialogButtonProperties(
+ text = stringResource(id = R.string.confirm_changes_confirm),
+ onClick = onHideConfirmationDialog
+ ),
+ properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = false, dismissOnClickOutside = false)
+ )
+}
diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingToolPicker.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingToolPicker.kt
new file mode 100644
index 00000000000..0182f0f1b5d
--- /dev/null
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingToolPicker.kt
@@ -0,0 +1,205 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.feature.sketch
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.IconToggleButtonColors
+import androidx.compose.material3.OutlinedIconToggleButton
+import androidx.compose.material3.SheetValue
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.wire.android.ui.common.bottomsheet.MenuModalSheetContent
+import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader
+import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout
+import com.wire.android.ui.common.bottomsheet.WireModalSheetState
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.theme.WireColorPalette
+import com.wire.android.ui.theme.WireTheme
+
+@Composable
+fun DrawingToolPicker(
+ sheetState: WireModalSheetState,
+ currentColor: Color,
+ onColorSelected: (Color) -> Unit,
+) {
+ val scope = rememberCoroutineScope()
+ val colorPalette = colorsScheme().sketchColorPalette
+ WireModalSheetLayout(
+ dragHandle = null,
+ sheetState = sheetState,
+ coroutineScope = scope,
+ ) {
+ Column(
+ modifier = Modifier
+ .background(colorsScheme().surface)
+ .wrapContentSize()
+ ) {
+ Box(
+ Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(vertical = dimensions().spacing12x)
+ .size(width = dimensions().modalBottomSheetDividerWidth, height = dimensions().spacing4x)
+ .background(color = colorsScheme().background, shape = RoundedCornerShape(size = dimensions().spacing2x))
+
+ )
+ MenuModalSheetContent(
+ header = MenuModalSheetHeader.Visible(title = stringResource(id = R.string.title_color_picker)),
+ menuItems = listOf {
+ LazyVerticalGrid(
+ modifier = Modifier
+ .background(colorsScheme().surface)
+ .padding(PaddingValues(horizontal = dimensions().spacing8x)),
+ columns = GridCells.Fixed(GRID_CELLS),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalArrangement = Arrangement.SpaceEvenly,
+ ) {
+ items(colorPalette.size) { index ->
+ val color = colorPalette[index]
+ ColorOptionButton(
+ color = color,
+ selected = color == currentColor,
+ onColorSelected = { onColorSelected(color) }
+ )
+ }
+ }
+ }
+ )
+ }
+ }
+}
+
+@Composable
+private fun ColorOptionButton(color: Color, selected: Boolean = false, onColorSelected: () -> Unit) {
+ when (selected) {
+ true -> SelectedColor(color, onColorSelected)
+ false -> NonSelectedColor(color, onColorSelected)
+ }
+}
+
+@Composable
+private fun NonSelectedColor(color: Color, onColorSelected: () -> Unit) {
+ OutlinedIconToggleButton(
+ modifier = Modifier
+ .fillMaxSize()
+ .aspectRatio(1f)
+ .padding(dimensions().spacing12x),
+ checked = false,
+ border = if (color == Color.White) BorderStroke(dimensions().spacing1x, colorsScheme().onBackground) else null,
+ colors = IconToggleButtonColors(
+ containerColor = color,
+ checkedContainerColor = color,
+ checkedContentColor = color,
+ disabledContainerColor = color,
+ disabledContentColor = color,
+ contentColor = color,
+ ),
+ onCheckedChange = { onColorSelected() },
+ content = { }
+ )
+}
+
+@Composable
+private fun SelectedColor(color: Color, onColorSelected: () -> Unit) {
+ OutlinedIconToggleButton(
+ modifier = Modifier
+ .fillMaxSize()
+ .aspectRatio(1f)
+ .padding(if (color.isWhite()) dimensions().corner14x else dimensions().corner12x)
+ .background(colorsScheme().surface, CircleShape)
+ .border(dimensions().spacing2x, colorsScheme().secondaryText, CircleShape),
+ checked = true,
+ border = BorderStroke(
+ if (color.isWhite()) dimensions().spacing1x else dimensions().spacing2x,
+ if (color.isWhite()) colorsScheme().onSurface else colorsScheme().surface
+ ),
+ colors = IconToggleButtonColors(
+ containerColor = color,
+ checkedContainerColor = color,
+ checkedContentColor = color,
+ disabledContainerColor = color,
+ disabledContentColor = color,
+ contentColor = color,
+ ),
+ onCheckedChange = { onColorSelected() }
+ ) {
+ Icon(
+ imageVector = Icons.Default.CheckCircle,
+ contentDescription = null,
+ tint = if (color.isWhite()) Color.Black else Color.White,
+ modifier = Modifier.size(
+ dimensions().spacing16x
+ )
+ )
+ }
+}
+
+private const val GRID_CELLS = 6
+
+private fun Color.isWhite() = this == Color.White
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+fun PreviewDrawingToolPickerSelectedNonWhite() {
+ WireTheme {
+ DrawingToolPicker(
+ sheetState = WireModalSheetState(SheetValue.Expanded),
+ currentColor = WireColorPalette.LightBlue500,
+ onColorSelected = {}
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview(uiMode = UI_MODE_NIGHT_YES)
+@Composable
+fun PreviewDrawingToolPickerSelectedWhite() {
+ WireTheme {
+ DrawingToolPicker(
+ sheetState = WireModalSheetState(SheetValue.Expanded),
+ currentColor = Color.White,
+ onColorSelected = {}
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/navigation/style/ScreenModeStyle.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/PreviewDrawingComponents.kt
similarity index 57%
rename from app/src/main/kotlin/com/wire/android/navigation/style/ScreenModeStyle.kt
rename to features/sketch/src/main/java/com/wire/android/feature/sketch/PreviewDrawingComponents.kt
index 048828392b7..b7146cf9173 100644
--- a/app/src/main/kotlin/com/wire/android/navigation/style/ScreenModeStyle.kt
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/PreviewDrawingComponents.kt
@@ -15,14 +15,25 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
-package com.wire.android.navigation.style
+package com.wire.android.feature.sketch
-interface ScreenModeStyle {
- fun screenMode(): ScreenMode
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.wire.android.feature.sketch.model.DrawingState
+import com.wire.android.ui.theme.WireTheme
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewToolBar() {
+ WireTheme {
+ DrawingToolbar(DrawingState(), {}, {})
+ }
}
-enum class ScreenMode {
- KEEP_ON, // keep screen on while that NavigationItem is visible (i.e CallScreen)
- WAKE_UP, // wake up the device on navigating to that NavigationItem (i.e IncomingCall)
- NONE // do not wake up and allow device to sleep
+@Preview(showBackground = true)
+@Composable
+fun PreviewTopBar() {
+ WireTheme {
+ DrawingTopBar("Conversation Name", {}, {}, DrawingState())
+ }
}
diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingPathProperties.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingPathProperties.kt
index 39f8eb0fdf2..44e66765510 100644
--- a/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingPathProperties.kt
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingPathProperties.kt
@@ -19,20 +19,29 @@ package com.wire.android.feature.sketch.model
import android.graphics.Bitmap
import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.Paint
import android.graphics.Shader
+import android.os.Build
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.toArgb
+/**
+ * Represents the current path properties [DrawingState.currentPath]
+ * This can be extended in the future to [strokeWidth], [drawMode] ,etc.
+ */
internal class DrawingPathProperties(
var path: Path = Path(),
var strokeWidth: Float = 10f,
- var color: Color = Color.Blue,
+ var color: Color = Color.Black,
var drawMode: DrawMode = DrawMode.Pen
) {
fun draw(scope: DrawScope, bitmap: Bitmap? = null) {
@@ -80,4 +89,51 @@ internal class DrawingPathProperties(
DrawMode.None -> {}
}
}
+
+ fun drawNative(canvas: Canvas) {
+ when (drawMode) {
+ DrawMode.Pen -> {
+ canvas.drawPath(
+ androidPath,
+ paint
+ )
+ }
+
+ DrawMode.Eraser -> {
+ canvas.drawPath(
+ androidPath,
+ paint
+ )
+ }
+
+ DrawMode.None -> {}
+ }
+ }
+
+ private val androidPath
+ get() = path.asAndroidPath()
+
+ private val paint: Paint
+ get() {
+ return if (drawMode == DrawMode.Pen) {
+ Paint().apply {
+ color = this@DrawingPathProperties.color.toArgb()
+ style = Paint.Style.STROKE
+ strokeWidth = this@DrawingPathProperties.strokeWidth
+ strokeCap = Paint.Cap.ROUND
+ strokeJoin = Paint.Join.ROUND
+ }
+ } else {
+ Paint().apply {
+ color = Color.Transparent.toArgb()
+ style = Paint.Style.STROKE
+ strokeWidth = this@DrawingPathProperties.strokeWidth
+ strokeCap = Paint.Cap.ROUND
+ strokeJoin = Paint.Join.ROUND
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ blendMode = android.graphics.BlendMode.CLEAR
+ }
+ }
+ }
+ }
}
diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingState.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingState.kt
index 283797ea902..12de2bae850 100644
--- a/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingState.kt
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/model/DrawingState.kt
@@ -18,11 +18,14 @@
package com.wire.android.feature.sketch.model
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
internal data class DrawingState(
val paths: List = listOf(),
val pathsUndone: List = listOf(),
val drawingMotionEvent: DrawingMotionEvent = DrawingMotionEvent.Idle,
val currentPath: DrawingPathProperties = DrawingPathProperties(),
- val currentPosition: Offset = Offset.Unspecified
+ val currentPosition: Offset = Offset.Unspecified,
+ var canvasSize: Size? = null,
+ val showConfirmationDialog: Boolean = false
)
diff --git a/features/sketch/src/test/java/com/wire/android/feature/sketch/ExampleUnitTest.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/tools/DrawingToolsConfig.kt
similarity index 61%
rename from features/sketch/src/test/java/com/wire/android/feature/sketch/ExampleUnitTest.kt
rename to features/sketch/src/main/java/com/wire/android/feature/sketch/tools/DrawingToolsConfig.kt
index 0fe155d0de8..9c56240a6cc 100644
--- a/features/sketch/src/test/java/com/wire/android/feature/sketch/ExampleUnitTest.kt
+++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/tools/DrawingToolsConfig.kt
@@ -15,20 +15,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
-package com.wire.android.feature.sketch
+package com.wire.android.feature.sketch.tools
-import org.junit.Test
-
-import org.junit.Assert.*
+import androidx.compose.ui.graphics.Color
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
+ * Configuration for the drawing tools.
+ * ie. colors, stroke width, etc.
*/
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
+data class DrawingToolsConfig(
+ val colors: ImmutableList = persistentListOf(Color.Blue),
+ // todo. later add more configurations, like stroke width, etc.
+)
diff --git a/features/sketch/src/main/res/drawable/ic_close.xml b/features/sketch/src/main/res/drawable/ic_close.xml
new file mode 100644
index 00000000000..e88f1d6bc70
--- /dev/null
+++ b/features/sketch/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/features/sketch/src/main/res/drawable/ic_long_arrow.xml b/features/sketch/src/main/res/drawable/ic_long_arrow.xml
new file mode 100644
index 00000000000..2119c886028
--- /dev/null
+++ b/features/sketch/src/main/res/drawable/ic_long_arrow.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/features/sketch/src/main/res/drawable/ic_send.xml b/features/sketch/src/main/res/drawable/ic_send.xml
new file mode 100644
index 00000000000..537ef313c36
--- /dev/null
+++ b/features/sketch/src/main/res/drawable/ic_send.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
diff --git a/features/sketch/src/main/res/drawable/ic_undo.xml b/features/sketch/src/main/res/drawable/ic_undo.xml
new file mode 100644
index 00000000000..54e6d97a25e
--- /dev/null
+++ b/features/sketch/src/main/res/drawable/ic_undo.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/features/sketch/src/main/res/values/strings.xml b/features/sketch/src/main/res/values/strings.xml
index 9147ed8f0c8..af2e78af51c 100644
--- a/features/sketch/src/main/res/values/strings.xml
+++ b/features/sketch/src/main/res/values/strings.xml
@@ -17,4 +17,13 @@
-->
Sketch module
+ Send
+ Undo
+ Pick your favorite color and start sketching 🎨
+ close
+ Color
+ Discard drawing?
+ If you leave without sending, your drawing will be lost.
+ Cancel
+ Discard
diff --git a/features/sketch/src/test/java/com/wire/android/feature/sketch/DrawingCanvasViewModelTest.kt b/features/sketch/src/test/java/com/wire/android/feature/sketch/DrawingCanvasViewModelTest.kt
new file mode 100644
index 00000000000..c283683861a
--- /dev/null
+++ b/features/sketch/src/test/java/com/wire/android/feature/sketch/DrawingCanvasViewModelTest.kt
@@ -0,0 +1,170 @@
+package com.wire.android.feature.sketch
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import com.wire.android.feature.sketch.model.DrawingMotionEvent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DrawingCanvasViewModelTest {
+
+ @Test
+ fun givenOnStartDrawingIsCalled_WhenCallingTheAction_ThenUpdateStateWithEventDown() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+
+ // when
+ viewModel.onStartDrawing(INITIAL_OFFSET)
+
+ // then
+ assertEquals(DrawingMotionEvent.Down, viewModel.state.drawingMotionEvent)
+ }
+
+ @Test
+ fun givenOnDrawIsCalled_WhenCallingTheAction_ThenUpdateStateWithEventMove() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+
+ // when
+ viewModel.onDraw(INITIAL_OFFSET)
+
+ // then
+ assertEquals(DrawingMotionEvent.Move, viewModel.state.drawingMotionEvent)
+ }
+
+ @Test
+ fun givenOnStopDrawingIsCalled_WhenCallingTheAction_ThenUpdateStateWithEventUp() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+
+ // when
+ viewModel.onStopDrawing()
+
+ // then
+ assertEquals(DrawingMotionEvent.Up, viewModel.state.drawingMotionEvent)
+ }
+
+ @Test
+ fun givenStartDrawingEvent_WhenCallingTheAction_ThenUpdateTheStateWithTheInitialPathPosition() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+ assertEquals(viewModel.state.currentPosition, Offset.Unspecified)
+
+ // when
+ startDrawing(viewModel)
+
+ // then
+ with(viewModel.state) {
+ assertEquals(DrawingMotionEvent.Down, drawingMotionEvent)
+ assertEquals(currentPath.path, paths.first().path)
+ assertEquals(currentPosition, INITIAL_OFFSET)
+ }
+ }
+
+ @Test
+ fun givenDrawingEvent_WhenCallingTheAction_ThenUpdateTheStateWithTheCurrentMovingPathPosition() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+ assertEquals(viewModel.state.currentPosition, Offset.Unspecified)
+
+ // when
+ draw(viewModel)
+
+ // then
+ with(viewModel.state) {
+ assertEquals(DrawingMotionEvent.Move, drawingMotionEvent)
+ assertEquals(currentPath.path, paths.first().path)
+ assertEquals(currentPosition, MOVED_OFFSET)
+ }
+ }
+
+ @Test
+ fun givenStopDrawingEvent_WhenCallingTheAction_ThenUpdateTheStateWithTheFinalPathPosition() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+ assertEquals(viewModel.state.currentPosition, Offset.Unspecified)
+
+ // when
+ stopDrawing(viewModel)
+
+ // then
+ with(viewModel.state) {
+ assertEquals(DrawingMotionEvent.Idle, drawingMotionEvent)
+ assertEquals(currentPosition, Offset.Unspecified)
+ }
+ }
+
+ @Test
+ fun givenOnColorChanged_WhenCallingTheAction_ThenUpdateCurrentPathWithTheSelectedColor() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+ assertEquals(viewModel.state.currentPath.color, Color.Black)
+
+ // when
+ val newColor = Color.Magenta
+ viewModel.onColorChanged(newColor)
+
+ // then
+ with(viewModel.state) {
+ assertEquals(currentPath.color, newColor)
+ }
+ }
+
+ @Test
+ fun givenWeWantToDiscard_WhenCallingTheAction_ThenUpdateStateToShowConfirmation() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+
+ // when
+ viewModel.onShowConfirmationDialog()
+
+ // then
+ with(viewModel.state) {
+ assertEquals(true, showConfirmationDialog)
+ }
+ }
+
+ @Test
+ fun givenWeCancelToDiscard_WhenCallingTheAction_ThenUpdateStateToHideConfirmation() = runTest {
+ // given
+ val (_, viewModel) = Arrangement().arrange()
+
+ // when
+ viewModel.onHideConfirmationDialog()
+
+ // then
+ with(viewModel.state) {
+ assertEquals(false, showConfirmationDialog)
+ }
+ }
+
+ private fun stopDrawing(viewModel: DrawingCanvasViewModel) = with(viewModel) {
+ draw(viewModel)
+ onStopDrawing()
+ onStopDrawingEvent()
+ }
+
+ // simulates the drawing of strokes
+ private fun draw(viewModel: DrawingCanvasViewModel) = with(viewModel) {
+ startDrawing(viewModel)
+ onDraw(MOVED_OFFSET)
+ onDrawEvent()
+ }
+
+ // simulates the start of drawing of strokes
+ private fun startDrawing(viewModel: DrawingCanvasViewModel) = with(viewModel) {
+ onStartDrawing(INITIAL_OFFSET)
+ onStartDrawingEvent()
+ }
+
+ private class Arrangement {
+ val viewModel = DrawingCanvasViewModel()
+ fun arrange() = this to viewModel
+ }
+
+ private companion object {
+ val INITIAL_OFFSET = Offset(0f, 0f)
+ val MOVED_OFFSET = Offset(10f, 10f)
+ }
+}