diff --git a/.github/actions/deploy-to-s3/action.yml b/.github/actions/deploy-to-s3/action.yml index 1847a65456c..74e312278f5 100644 --- a/.github/actions/deploy-to-s3/action.yml +++ b/.github/actions/deploy-to-s3/action.yml @@ -40,8 +40,8 @@ runs: aws-bucket: ${{ inputs.aws-bucket }} destination-dir: "megazord/android/reloaded/${{ inputs.build-flavour }}/${{ inputs.build-variant }}/PR-${{ github.event.pull_request.number }}/" file-path: ${{ steps.path.outputs.apk_full_path }} - output-file-url: 'true' - public: true + output-file-url: 'false' + public: false - name: Upload to S3 from branch if: github.event.pull_request.number == '' id: upload-from-branch @@ -53,8 +53,8 @@ runs: aws-bucket: ${{ inputs.aws-bucket }} destination-dir: "megazord/android/reloaded/${{ inputs.build-flavour }}/${{ inputs.build-variant }}/" file-path: ${{ steps.path.outputs.apk_full_path }} - output-file-url: 'true' - public: true + output-file-url: 'false' + public: false - name: Show URL if: github.event.pull_request.number != '' shell: bash diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 3bdf323e37e..01e0ea54c89 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -498,6 +498,12 @@ class UseCaseModule { fun provideMigrateFromPersonalToTeamUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId - ) = - coreLogic.getSessionScope(currentAccount).migrateFromPersonalToTeam + ) = coreLogic.getSessionScope(currentAccount).migrateFromPersonalToTeam + + @ViewModelScoped + @Provides + fun provideGetTeamUrlUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId + ) = coreLogic.getSessionScope(currentAccount).getTeamUrlUseCase } diff --git a/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt b/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt index b3f3dab7579..3e8f8be58a9 100644 --- a/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt +++ b/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt @@ -87,3 +87,7 @@ interface ScopedArgs { @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class ViewModelScopedPreview + +interface AssistedViewModelFactory { + fun create(args: R): VM +} diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt index 244cd31a5cf..a85838d76b3 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt @@ -342,4 +342,9 @@ class ConversationModule { @Provides fun provideObserveUserFoldersUseCase(conversationScope: ConversationScope) = conversationScope.observeUserFolders + + @ViewModelScoped + @Provides + fun provideMoveConversationToFolderUseCase(conversationScope: ConversationScope) = + conversationScope.moveConversationToFolder } diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt index 2c8c7ac115c..e4ed7d01bfa 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt @@ -37,7 +37,6 @@ import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersona import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase -import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase import com.wire.kalium.logic.feature.user.DeleteAccountUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.GetUserInfoUseCase @@ -192,11 +191,6 @@ class UserModule { fun provideGetSelfUseCase(userScope: UserScope): GetSelfUserUseCase = userScope.getSelfUser - @ViewModelScoped - @Provides - fun provideGetTeamUrlUseCase(userScope: UserScope): GetTeamUrlUseCase = - userScope.getTeamUrl - @ViewModelScoped @Provides fun provideGetAvatarAssetUseCase(userScope: UserScope): GetAvatarAssetUseCase = diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index 60f329506fe..cc6e464beac 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -71,6 +71,7 @@ fun ConversationDetailsWithEvents.toConversationItem( hasNewActivitiesToShow = hasNewActivitiesToShow, searchQuery = searchQuery, isFavorite = conversationDetails.isFavorite, + folder = conversationDetails.folder, playingAudio = getPlayingAudioInConversation(playingAudioMessage, conversationDetails) ) } @@ -110,6 +111,7 @@ fun ConversationDetailsWithEvents.toConversationItem( hasNewActivitiesToShow = hasNewActivitiesToShow, searchQuery = searchQuery, isFavorite = conversationDetails.isFavorite, + folder = conversationDetails.folder, playingAudio = getPlayingAudioInConversation(playingAudioMessage, conversationDetails) ) } diff --git a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt index 8b8a67d1aaa..fbd1660dabd 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt @@ -21,6 +21,7 @@ package com.wire.android.navigation import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi import com.ramcosta.composedestinations.DestinationsNavHost @@ -44,6 +45,7 @@ import com.wire.android.ui.home.newconversation.NewConversationViewModel fun MainNavHost( navigator: Navigator, startDestination: Route, + modifier: Modifier = Modifier, ) { val navHostEngine = rememberAnimatedNavHostEngine( rootDefaultAnimations = DefaultRootNavGraphAnimations, @@ -55,6 +57,7 @@ fun MainNavHost( ) DestinationsNavHost( + modifier = modifier, navGraph = WireMainNavGraph, engine = navHostEngine, startRoute = startDestination, 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 36a9466e6a9..7ce3e80fe6c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt @@ -19,12 +19,12 @@ package com.wire.android.ui import android.os.Bundle import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember -import androidx.core.view.WindowCompat import com.wire.android.appLogger import com.wire.android.navigation.MainNavHost import com.wire.android.navigation.rememberNavigator @@ -39,7 +39,7 @@ import dagger.hilt.android.AndroidEntryPoint class AppLockActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() setContent { val snackbarHostState = remember { SnackbarHostState() } CompositionLocalProvider( 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 54d9e7ff6d6..3dc15193a48 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -26,11 +26,14 @@ import android.os.Bundle import android.view.WindowManager import android.widget.Toast import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.statusBars import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -47,7 +50,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope @@ -163,7 +165,7 @@ class WireActivity : AppCompatActivity() { super.onCreate(savedInstanceState) splashScreen.setKeepOnScreenCondition { shouldKeepSplashOpen } - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() lifecycleScope.launch { @@ -232,18 +234,17 @@ class WireActivity : AppCompatActivity() { WireTheme { Column( modifier = Modifier - .statusBarsPadding() .semantics { testTagsAsResourceId = true } ) { val navigator = rememberNavigator(this@WireActivity::finish) WireTopAppBar( - themeOption = viewModel.globalAppState.themeOption, commonTopAppBarState = commonTopAppBarViewModel.state, ) CompositionLocalProvider(LocalNavigator provides navigator) { MainNavHost( navigator = navigator, - startDestination = startDestination + startDestination = startDestination, + modifier = Modifier.consumeWindowInsets(WindowInsets.statusBars) ) } @@ -260,11 +261,11 @@ class WireActivity : AppCompatActivity() { @Composable private fun WireTopAppBar( - themeOption: ThemeOption, - commonTopAppBarState: CommonTopAppBarState + commonTopAppBarState: CommonTopAppBarState, + modifier: Modifier = Modifier, ) { CommonTopAppBar( - themeOption = themeOption, + modifier = modifier, commonTopAppBarState = commonTopAppBarState, onReturnToCallClick = { establishedCall -> getOngoingCallIntent( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt index 56cbe581ffc..c2fc5447e64 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedContent import androidx.compose.animation.togetherWith import androidx.compose.material3.SnackbarHostState @@ -33,7 +34,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.core.view.WindowCompat import com.wire.android.appLogger import com.wire.android.navigation.style.TransitionAnimationType import com.wire.android.ui.LocalActivity @@ -87,7 +87,7 @@ class StartingCallActivity : CallActivity() { setUpScreenshotPreventionFlag() setUpCallingFlags() - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() handleNewIntent(intent) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt index 4a407e84e1d..a0e4f7e1c17 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt @@ -26,6 +26,7 @@ import android.graphics.drawable.Icon import android.os.Bundle import android.util.Rational import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedContent import androidx.compose.animation.togetherWith import androidx.compose.material3.SnackbarHostState @@ -38,7 +39,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.core.view.WindowCompat import com.wire.android.R import com.wire.android.appLogger import com.wire.android.navigation.style.TransitionAnimationType @@ -90,7 +90,7 @@ class OngoingCallActivity : CallActivity() { setUpScreenshotPreventionFlag() setUpCallingFlags() - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() handleNewIntent(intent) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/RichMenuBottomSheetItem.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/RichMenuBottomSheetItem.kt index 031ae12e567..9277fa56e02 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/RichMenuBottomSheetItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/RichMenuBottomSheetItem.kt @@ -67,7 +67,13 @@ fun SelectableMenuBottomSheetItem( .wrapContentHeight() .wrapContentWidth() .defaultMinSize(minHeight = dimensions().spacing48x) - .let { if (isSelectedItem(state)) it.background(MaterialTheme.wireColorScheme.secondaryButtonSelected) else it } + .background( + if (isSelectedItem(state)) { + MaterialTheme.wireColorScheme.secondaryButtonSelected + } else { + MaterialTheme.wireColorScheme.surface + } + ) .clickable(onItemClick) .semantics { if (isSelectedItem(state)) selected = true } .padding(vertical = dimensions().spacing12x, horizontal = dimensions().spacing16x) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt index 98706114d96..afcd2d45bab 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt @@ -25,10 +25,12 @@ import com.wire.android.R import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.common.dialogs.UnblockUserDialogState +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavArgs import com.wire.android.ui.home.conversationslist.model.BlockingState import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.ConversationFolder import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId @@ -38,7 +40,7 @@ fun ConversationSheetContent( conversationSheetState: ConversationSheetState, onMutingConversationStatusChange: () -> Unit, changeFavoriteState: (GroupDialogState, addToFavorite: Boolean) -> Unit, - moveConversationToFolder: () -> Unit, + moveConversationToFolder: ((ConversationFoldersNavArgs) -> Unit)?, updateConversationArchiveStatus: (DialogState) -> Unit, clearConversationContent: (DialogState) -> Unit, blockUser: (BlockUserDialogState) -> Unit, @@ -56,8 +58,7 @@ fun ConversationSheetContent( ConversationMainSheetContent( conversationSheetContent = conversationSheetState.conversationSheetContent!!, changeFavoriteState = changeFavoriteState, -// TODO(profile): enable when implemented -// moveConversationToFolder = moveConversationToFolder, + moveConversationToFolder = moveConversationToFolder, updateConversationArchiveStatus = updateConversationArchiveStatus, clearConversationContent = clearConversationContent, blockUserClick = blockUser, @@ -128,6 +129,7 @@ data class ConversationSheetContent( val proteusVerificationStatus: Conversation.VerificationStatus, val isUnderLegalHold: Boolean, val isFavorite: Boolean?, + val folder: ConversationFolder?, val isDeletingConversationLocallyRunning: Boolean ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt index e0a8ec20bc6..1b8b4302c9c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt @@ -81,6 +81,7 @@ fun rememberConversationSheetState( proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, isUnderLegalHold = showLegalHoldIndicator, isFavorite = isFavorite, + folder = folder, isDeletingConversationLocallyRunning = isConversationDeletionLocallyRunning ) } @@ -108,6 +109,7 @@ fun rememberConversationSheetState( proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, isUnderLegalHold = showLegalHoldIndicator, isFavorite = isFavorite, + folder = folder, isDeletingConversationLocallyRunning = false ) } @@ -130,6 +132,7 @@ fun rememberConversationSheetState( proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, isUnderLegalHold = showLegalHoldIndicator, isFavorite = null, + folder = null, isDeletingConversationLocallyRunning = false ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt index fe26a89cd43..42cd48af564 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt @@ -43,6 +43,7 @@ import com.wire.android.ui.common.conversationColor import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.common.dialogs.UnblockUserDialogState import com.wire.android.ui.common.dimensions +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavArgs import com.wire.android.ui.home.conversationslist.common.GroupConversationAvatar import com.wire.android.ui.home.conversationslist.model.BlockingState import com.wire.android.ui.home.conversationslist.model.DialogState @@ -59,8 +60,7 @@ import com.wire.kalium.logic.data.user.ConnectionState internal fun ConversationMainSheetContent( conversationSheetContent: ConversationSheetContent, changeFavoriteState: (dialogState: GroupDialogState, addToFavorite: Boolean) -> Unit, - // TODO(profile): enable when implemented - // moveConversationToFolder: () -> Unit, + moveConversationToFolder: ((ConversationFoldersNavArgs) -> Unit)?, updateConversationArchiveStatus: (DialogState) -> Unit, clearConversationContent: (DialogState) -> Unit, blockUserClick: (BlockUserDialogState) -> Unit, @@ -142,19 +142,28 @@ internal fun ConversationMainSheetContent( } } } -// TODO(profile): enable when implemented -// add { -// MenuBottomSheetItem( -// icon = { -// MenuItemIcon( -// id = R.drawable.ic_folder, -// contentDescription = stringResource(R.string.content_description_move_to_folder), -// ) -// }, -// title = stringResource(R.string.label_move_to_folder), -// onItemClick = moveConversationToFolder -// ) -// } + if (moveConversationToFolder != null) { + add { + MenuBottomSheetItem( + leading = { + MenuItemIcon( + id = R.drawable.ic_folder, + contentDescription = null, + ) + }, + title = stringResource(R.string.label_move_to_folder), + onItemClick = { + moveConversationToFolder( + ConversationFoldersNavArgs( + conversationId = conversationSheetContent.conversationId, + conversationName = conversationSheetContent.title, + currentFolderId = conversationSheetContent.folder?.id + ) + ) + } + ) + } + } add { MenuBottomSheetItem( leading = { diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt index c22b5c60996..ce9ef4e88b8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt @@ -20,11 +20,10 @@ package com.wire.android.ui.common.topappbar -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColor import androidx.compose.animation.animateContentSize -import androidx.compose.animation.expandIn -import androidx.compose.animation.shrinkOut -import androidx.compose.foundation.background +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -32,27 +31,27 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntSize -import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.wire.android.BuildConfig import com.wire.android.R -import com.wire.android.ui.theme.ThemeOption +import com.wire.android.ui.theme.WireColorScheme import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.updateSystemBarIconsAppearance import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography @@ -62,114 +61,111 @@ import com.wire.kalium.network.NetworkState @Composable fun CommonTopAppBar( - themeOption: ThemeOption, commonTopAppBarState: CommonTopAppBarState, onReturnToCallClick: (ConnectivityUIState.Call.Established) -> Unit, onReturnToIncomingCallClick: (ConnectivityUIState.Call.Incoming) -> Unit, onReturnToOutgoingCallClick: (ConnectivityUIState.Call.Outgoing) -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { + val transition = updateTransition( + targetState = MaterialTheme.wireColorScheme to commonTopAppBarState.connectivityState.toColorType(), + label = "connectivity state transition" + ) + val backgroundColor = transition.animateColor(label = "top app bar background color") { (colorScheme, colorType) -> + colorScheme.getBackgroundColor(colorType) + } + val systemBarIconsAppearance = transition.animateFloat(label = "system bar icons appearance") { (colorScheme, colorType) -> + if (colorScheme.getStatusBarIconsAppearance(colorType)) 1f else 0f + } + + updateSystemBarIconsAppearance(systemBarIconsAppearance.value > 0.5f) + + Column( + modifier = modifier + .drawBehind { drawRect(backgroundColor.value) } + .statusBarsPadding() + ) { ConnectivityStatusBar( - themeOption = themeOption, networkState = commonTopAppBarState.networkState, connectivityInfo = commonTopAppBarState.connectivityState, onReturnToCallClick = onReturnToCallClick, onReturnToIncomingCallClick = onReturnToIncomingCallClick, - onReturnToOutgoingCallClick = onReturnToOutgoingCallClick + onReturnToOutgoingCallClick = onReturnToOutgoingCallClick, + modifier = modifier, ) } } -@Composable -fun getBackgroundColor(connectivityInfo: ConnectivityUIState): Color { - return when (connectivityInfo) { - is ConnectivityUIState.Calls -> MaterialTheme.wireColorScheme.positive +private enum class ConnectivityStatusColorType { Calls, Connection, None } - is ConnectivityUIState.WaitingConnection, - ConnectivityUIState.Connecting -> MaterialTheme.wireColorScheme.primary +private fun ConnectivityUIState.toColorType() = when (this) { + is ConnectivityUIState.Calls -> ConnectivityStatusColorType.Calls + is ConnectivityUIState.Connecting, + is ConnectivityUIState.WaitingConnection -> ConnectivityStatusColorType.Connection + is ConnectivityUIState.None -> ConnectivityStatusColorType.None +} - ConnectivityUIState.None -> MaterialTheme.wireColorScheme.background - } +private fun WireColorScheme.getBackgroundColor(statusColorType: ConnectivityStatusColorType): Color = when (statusColorType) { + ConnectivityStatusColorType.Calls -> positive + ConnectivityStatusColorType.Connection -> primary + ConnectivityStatusColorType.None -> background +} + +private fun WireColorScheme.getStatusBarIconsAppearance(statusColorType: ConnectivityStatusColorType): Boolean = when (statusColorType) { + ConnectivityStatusColorType.Calls, + ConnectivityStatusColorType.Connection -> connectivityBarShouldUseDarkIcons + ConnectivityStatusColorType.None -> useDarkSystemBarIcons } @Composable private fun ConnectivityStatusBar( - themeOption: ThemeOption, connectivityInfo: ConnectivityUIState, networkState: NetworkState, onReturnToCallClick: (ConnectivityUIState.Call.Established) -> Unit, onReturnToIncomingCallClick: (ConnectivityUIState.Call.Incoming) -> Unit, onReturnToOutgoingCallClick: (ConnectivityUIState.Call.Outgoing) -> Unit, + modifier: Modifier = Modifier, ) { - val isVisible = connectivityInfo !is ConnectivityUIState.None - val backgroundColor = getBackgroundColor(connectivityInfo) - - if (isVisible) { - val darkIcons = MaterialTheme.wireColorScheme.connectivityBarShouldUseDarkIcons - val systemUiController = rememberSystemUiController() - systemUiController.setStatusBarColor( - color = backgroundColor, - darkIcons = darkIcons - ) - LaunchedEffect(themeOption) { - systemUiController.setStatusBarColor( - color = backgroundColor, - darkIcons = darkIcons - ) - } - } else { - ClearStatusBarColor() - } - - AnimatedVisibility( - visible = isVisible, - enter = expandIn(initialSize = { fullSize -> IntSize(fullSize.width, 0) }), - exit = shrinkOut(targetSize = { fullSize -> IntSize(fullSize.width, 0) }) + Column( + modifier = modifier + .animateContentSize() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Column( - modifier = Modifier - .animateContentSize() - .fillMaxWidth() - .heightIn(min = MaterialTheme.wireDimensions.ongoingCallLabelHeight) - .background(backgroundColor), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - when (connectivityInfo) { - is ConnectivityUIState.Calls -> - CallsContent( - calls = connectivityInfo.calls, - onReturnToCallClick = onReturnToCallClick, - onReturnToIncomingCallClick = onReturnToIncomingCallClick, - onReturnToOutgoingCallClick = onReturnToOutgoingCallClick - ) + when (connectivityInfo) { + is ConnectivityUIState.Calls -> + CallsContent( + calls = connectivityInfo.calls, + onReturnToCallClick = onReturnToCallClick, + onReturnToIncomingCallClick = onReturnToIncomingCallClick, + onReturnToOutgoingCallClick = onReturnToOutgoingCallClick + ) - ConnectivityUIState.Connecting -> + ConnectivityUIState.Connecting -> + StatusLabel( + R.string.connectivity_status_bar_connecting, + MaterialTheme.wireColorScheme.onPrimary + ) + + is ConnectivityUIState.WaitingConnection -> { + val color = MaterialTheme.wireColorScheme.onPrimary + val waitingStatus: @Composable () -> Unit = { StatusLabel( - R.string.connectivity_status_bar_connecting, - MaterialTheme.wireColorScheme.onPrimary + stringResource = R.string.connectivity_status_bar_waiting_for_network, + color ) + } - is ConnectivityUIState.WaitingConnection -> { - val color = MaterialTheme.wireColorScheme.onPrimary - val waitingStatus: @Composable () -> Unit = { - StatusLabel( - stringResource = R.string.connectivity_status_bar_waiting_for_network, - color - ) - } - - if (!BuildConfig.PRIVATE_BUILD) { - waitingStatus() - return@Column - } - - WaitingStatusLabelInternal(connectivityInfo, networkState, waitingStatus) + if (!BuildConfig.PRIVATE_BUILD) { + waitingStatus() + return@Column } - ConnectivityUIState.None -> {} + WaitingStatusLabelInternal(connectivityInfo, networkState, waitingStatus) } + + ConnectivityUIState.None -> {} } } } @@ -381,20 +377,9 @@ private fun MicrophoneIcon( ) } -@Composable -private fun ClearStatusBarColor() { - val backgroundColor = MaterialTheme.wireColorScheme.background - val darkIcons = MaterialTheme.wireColorScheme.useDarkSystemBarIcons - - rememberSystemUiController().setSystemBarsColor( - color = backgroundColor, - darkIcons = darkIcons - ) -} - @Composable private fun PreviewCommonTopAppBar(connectivityUIState: ConnectivityUIState) = WireTheme { - CommonTopAppBar(ThemeOption.SYSTEM, CommonTopAppBarState(connectivityUIState), {}, {}, {}) + CommonTopAppBar(CommonTopAppBarState(connectivityUIState), {}, {}, {}) } @PreviewMultipleThemes 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 0e58a698ce1..7bc542bea46 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 @@ -29,18 +29,22 @@ import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallStatus -import com.wire.kalium.logic.data.sync.SyncState +import com.wire.kalium.logic.data.sync.SyncState.Failed +import com.wire.kalium.logic.data.sync.SyncState.GatheringPendingEvents +import com.wire.kalium.logic.data.sync.SyncState.Live +import com.wire.kalium.logic.data.sync.SyncState.SlowSync +import com.wire.kalium.logic.data.sync.SyncState.Waiting import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.network.NetworkState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting import javax.inject.Inject @@ -59,12 +63,19 @@ class CommonTopAppBarViewModel @Inject constructor( private fun connectivityFlow(userId: UserId): Flow = coreLogic.sessionScope(userId) { - observeSyncState().map { - when (it) { - SyncState.Waiting -> Connectivity.WaitingConnection(null, null) - is SyncState.Failed -> Connectivity.WaitingConnection(it.cause, it.retryDelay) - SyncState.GatheringPendingEvents, SyncState.SlowSync -> Connectivity.Connecting - SyncState.Live -> Connectivity.Connected + combine(observeSyncState(), coreLogic.networkStateObserver.observeNetworkState()) { syncState, networkState -> + when (syncState) { + is Waiting -> Connectivity.WaitingConnection(null, null) + is Failed -> Connectivity.WaitingConnection(syncState.cause, syncState.retryDelay) + is GatheringPendingEvents, + is SlowSync -> Connectivity.Connecting + + is Live -> + if (networkState is NetworkState.ConnectedWithInternet) { + Connectivity.Connected + } else { + Connectivity.WaitingConnection(null, null) + } } } } @@ -84,37 +95,44 @@ class CommonTopAppBarViewModel @Inject constructor( init { viewModelScope.launch { coreLogic.globalScope { - session.currentSessionFlow().flatMapLatest { - when (it) { - is CurrentSessionResult.Failure.Generic, - is CurrentSessionResult.Failure.SessionNotFound -> flowOf( - ConnectivityUIState.None - ) + session.currentSessionFlow() + .flatMapLatest { + when (it) { + is CurrentSessionResult.Failure.Generic, + is CurrentSessionResult.Failure.SessionNotFound -> flowOf( + ConnectivityUIState.None + ) - is CurrentSessionResult.Success -> { - val userId = it.accountInfo.userId - combine( - activeCallsFlow(userId), - currentScreenFlow(), - connectivityFlow(userId), - ) { activeCalls, currentScreen, connectivity -> - mapToConnectivityUIState(currentScreen, connectivity, activeCalls) + is CurrentSessionResult.Success -> { + val userId = it.accountInfo.userId + combine( + activeCallsFlow(userId), + currentScreenFlow(), + connectivityFlow(userId), + ) { activeCalls, currentScreen, connectivity -> + mapToConnectivityUIState(currentScreen, connectivity, activeCalls) + } } } } - }.collectLatest { connectivityUIState -> - /** - * Adding some delay here to avoid some bad UX : ongoing call banner displayed and - * hided in a short time when the user hangs up the call - * Call events could take some time to be received and this function - * could be called when the screen is changed, so we delayed - * showing the banner until getting the correct calling values - */ - if (connectivityUIState is ConnectivityUIState.Calls && connectivityUIState.hasOngoingCall) { - delay(WAITING_TIME_TO_SHOW_ONGOING_CALL_BANNER) + .debounce { state -> + /** + * Adding some debounce here to avoid some bad UX and prevent from having blinking effect when the state changes + * quickly, e.g. when displaying ongoing call banner and hiding it in a short time when the user hangs up the call. + * Call events could take some time to be received and this function could be called when the screen is changed, + * so we delayed showing the banner until getting the correct calling values and for calls this debounce is bigger + * than for other states in order to allow for the correct handling of hanging up a call. + * When state changes to None, handle it immediately, that's why we return 0L debounce time in this case. + */ + when { + state is ConnectivityUIState.None -> 0L + state is ConnectivityUIState.Calls && state.hasOngoingCall -> CONNECTIVITY_STATE_DEBOUNCE_ONGOING_CALL + else -> CONNECTIVITY_STATE_DEBOUNCE_DEFAULT + } + } + .collectLatest { connectivityUIState -> + state = state.copy(connectivityState = connectivityUIState) } - state = state.copy(connectivityState = connectivityUIState) - } } coreLogic.networkStateObserver.observeNetworkState().collectLatest { state = state.copy(networkState = it) @@ -160,6 +178,7 @@ class CommonTopAppBarViewModel @Inject constructor( } private companion object { - const val WAITING_TIME_TO_SHOW_ONGOING_CALL_BANNER = 600L + const val CONNECTIVITY_STATE_DEBOUNCE_ONGOING_CALL = 600L + const val CONNECTIVITY_STATE_DEBOUNCE_DEFAULT = 200L } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt index d87a1193371..8646ca95f5c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt @@ -29,11 +29,12 @@ import androidx.compose.runtime.setValue @Composable fun rememberSearchbarState( + initialIsSearchActive: Boolean = false, searchQueryTextState: TextFieldState = rememberTextFieldState() ): SearchBarState = rememberSaveable( - saver = SearchBarState.saver(searchQueryTextState) + saver = SearchBarState.saver() ) { - SearchBarState(searchQueryTextState = searchQueryTextState) + SearchBarState(isSearchActive = initialIsSearchActive, searchQueryTextState = searchQueryTextState) } class SearchBarState( @@ -57,14 +58,23 @@ class SearchBarState( } companion object { - fun saver(searchQueryTextState: TextFieldState): Saver = Saver( + fun saver(): Saver = Saver( save = { - listOf(it.isSearchActive) + listOf( + it.isSearchActive, + with(TextFieldState.Saver) { + save(it.searchQueryTextState) + } + ) }, restore = { SearchBarState( - isSearchActive = it[0], - searchQueryTextState = searchQueryTextState + isSearchActive = (it.getOrNull(0) as? Boolean) ?: false, + searchQueryTextState = it.getOrNull(1)?.let { + with(TextFieldState.Saver) { + restore(it) + } + } ?: TextFieldState() ) } ) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt index 123a72f9995..7d24e0e236e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt @@ -22,6 +22,7 @@ package com.wire.android.ui.common.topappbar.search import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -144,6 +145,7 @@ fun SearchTopBar( interactionSource = interactionSource, modifier = Modifier .padding(dimensions().spacing8x) + .focusable(true) .focusRequester(focusRequester) .onFocusEvent { onActiveChanged(it.isFocused) } ) diff --git a/app/src/main/kotlin/com/wire/android/ui/emoji/EmojiPicker.kt b/app/src/main/kotlin/com/wire/android/ui/emoji/EmojiPicker.kt index 5205500d865..a72f61f61a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/emoji/EmojiPicker.kt +++ b/app/src/main/kotlin/com/wire/android/ui/emoji/EmojiPicker.kt @@ -17,12 +17,13 @@ */ package com.wire.android.ui.emoji -import android.widget.LinearLayout +import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.emoji2.emojipicker.EmojiPickerView import com.google.android.material.bottomsheet.BottomSheetDragHandleView +import com.wire.android.R @Composable fun EmojiPickerBottomSheet( @@ -33,21 +34,19 @@ fun EmojiPickerBottomSheet( val context = LocalContext.current val dialog = remember { HandleDraggableBottomSheetDialog(context).apply { - setContentView( - LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - val handle = BottomSheetDragHandleView(context) + setContentView(R.layout.view_emoji_picker).run { + findViewById(R.id.emoji_picker_back_button)?.setOnClickListener { + dismiss() + onDismiss.invoke() + } + findViewById(R.id.emoji_picker)?.setOnEmojiPickedListener { emojiViewItem -> + dismiss() + onEmojiSelected(emojiViewItem.emoji) + } + findViewById(R.id.handle)?.let { handle -> getBehavior().dragHandle = handle - addView(handle) - addView( - EmojiPickerView(context).apply { - setOnEmojiPickedListener { emojiViewItem -> - onEmojiSelected(emojiViewItem.emoji) - } - } - ) } - ) + } setOnCancelListener { onDismiss.invoke() } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/emoji/HandleDraggableBottomSheetDialog.kt b/app/src/main/kotlin/com/wire/android/ui/emoji/HandleDraggableBottomSheetDialog.kt index adc6212903e..ff109265ffc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/emoji/HandleDraggableBottomSheetDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/emoji/HandleDraggableBottomSheetDialog.kt @@ -16,14 +16,11 @@ package com.wire.android.ui.emoji import android.content.Context -import android.os.Build -import android.os.Build.VERSION_CODES import android.os.Bundle import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.Window -import android.view.WindowManager import android.widget.FrameLayout import androidx.annotation.LayoutRes import androidx.annotation.StyleRes @@ -84,21 +81,7 @@ class HandleDraggableBottomSheetDialog : AppCompatDialog { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val window = window - if (window != null) { - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - // The status bar should always be transparent because of the window animation. - window.statusBarColor = 0 - - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - if (Build.VERSION.SDK_INT < VERSION_CODES.M) { - // It can be transparent for API 23 and above because we will handle switching the status - // bar icons to light or dark as appropriate. For API 21 and API 22 we just set the - // translucent status bar. - window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - } - } - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - } + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) } override fun setContentView(view: View) { @@ -269,18 +252,5 @@ class HandleDraggableBottomSheetDialog : AppCompatDialog { } return themeId } - - @Deprecated("use {@link EdgeToEdgeUtils#setLightStatusBar(Window, boolean)} instead") - fun setLightStatusBar(view: View, isLight: Boolean) { - if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { - var flags = view.systemUiVisibility - flags = if (isLight) { - flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - } else { - flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() - } - view.systemUiVisibility = flags - } - } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 3b48dd13629..9f82289be6e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -62,7 +62,6 @@ import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.R import com.wire.android.appLogger -import com.wire.android.di.hiltViewModelScoped import com.wire.android.navigation.HomeDestination import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -79,6 +78,7 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.search.SearchTopBar import com.wire.android.ui.common.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.ConversationFoldersScreenDestination import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.NewConversationSearchPeopleScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination @@ -86,6 +86,7 @@ import com.wire.android.ui.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.android.ui.home.conversations.details.GroupConversationActionType import com.wire.android.ui.home.conversations.details.GroupConversationDetailsNavBackArgs +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavBackArgs import com.wire.android.ui.home.conversations.folder.ConversationFoldersStateArgs import com.wire.android.ui.home.conversations.folder.ConversationFoldersVM import com.wire.android.ui.home.conversations.folder.ConversationFoldersVMImpl @@ -108,16 +109,19 @@ fun HomeScreen( navigator: Navigator, groupDetailsScreenResultRecipient: ResultRecipient, otherUserProfileScreenResultRecipient: ResultRecipient, + conversationFoldersScreenResultRecipient: + ResultRecipient, homeViewModel: HomeViewModel = hiltViewModel(), appSyncViewModel: AppSyncViewModel = hiltViewModel(), homeDrawerViewModel: HomeDrawerViewModel = hiltViewModel(), analyticsUsageViewModel: AnalyticsUsageViewModel = hiltViewModel(), foldersViewModel: ConversationFoldersVM = - hiltViewModelScoped( - ConversationFoldersStateArgs + hiltViewModel( + creationCallback = { it.create(ConversationFoldersStateArgs(null)) } ) ) { homeViewModel.checkRequirements { it.navigate(navigator::navigate) } + val context = LocalContext.current val homeScreenState = rememberHomeScreenState(navigator) val notificationsPermissionDeniedDialogState = rememberVisibilityState() @@ -137,7 +141,6 @@ fun HomeScreen( ) val lifecycleOwner = LocalLifecycleOwner.current - val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current val coroutineScope = rememberCoroutineScope() @@ -237,6 +240,17 @@ fun HomeScreen( } } + conversationFoldersScreenResultRecipient.onNavResult { result -> + when (result) { + NavResult.Canceled -> {} + is NavResult.Value -> { + coroutineScope.launch { + snackbarHostState.showSnackbar(result.value.message) + } + } + } + } + PermissionPermanentlyDeniedDialog( dialogState = notificationsPermissionDeniedDialogState, hideDialog = notificationsPermissionDeniedDialogState::dismiss diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt index cc0ea36b60d..cdcfc18687f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt @@ -71,11 +71,11 @@ fun HomeTopBar( } if (navigationItem.withUserAvatar) { val openLabel = stringResource(R.string.content_description_open_label) - val contentDescription = if (shouldShowCreateTeamUnreadIndicator) { - stringResource(R.string.content_description_home_profile_btn_with_notification) - } else { - stringResource(R.string.content_description_home_profile_btn) - } + val contentDescription = if (shouldShowCreateTeamUnreadIndicator) { + stringResource(R.string.content_description_home_profile_btn_with_notification) + } else { + stringResource(R.string.content_description_home_profile_btn) + } UserProfileAvatar( avatarData = userAvatarData, clickable = remember { @@ -88,7 +88,7 @@ fun HomeTopBar( legalHoldIndicatorVisible = withLegalHoldIndicator ), shouldShowCreateTeamUnreadIndicator = shouldShowCreateTeamUnreadIndicator, - contentDescription = contentDescription + contentDescription = contentDescription ) } }, @@ -115,6 +115,29 @@ fun PreviewTopBar() { } } +@PreviewMultipleThemes +@Composable +fun PreviewTopBarWithSelectedFilter() { + WireTheme { + HomeTopBar( + title = "Conversations", + currentFilter = ConversationFilter.Groups, + navigationItem = HomeDestination.Conversations, + userAvatarData = UserAvatarData( + asset = null, + availabilityStatus = UserAvailabilityStatus.AVAILABLE, + nameBasedAvatar = NameBasedAvatar("Jon Doe", -1) + ), + elevation = 0.dp, + withLegalHoldIndicator = false, + shouldShowCreateTeamUnreadIndicator = false, + onHamburgerMenuClick = {}, + onNavigateToSelfUserProfile = {}, + onOpenConversationFilter = {} + ) + } +} + @PreviewMultipleThemes @Composable fun PreviewSettingsTopBarWithoutAvatar() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt index 597f2d26390..07a1c578927 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/archive/ArchiveScreen.kt @@ -32,7 +32,6 @@ import com.wire.android.ui.home.conversationslist.common.previewConversationFold import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes -import kotlinx.coroutines.flow.flowOf @HomeNavGraph @WireDestination @@ -57,7 +56,7 @@ fun PreviewArchiveEmptyScreen() = WireTheme { searchBarState = rememberSearchbarState(), conversationsSource = ConversationsSource.ARCHIVE, emptyListContent = { ArchiveEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(list = listOf())), ) } @@ -66,10 +65,10 @@ fun PreviewArchiveEmptyScreen() = WireTheme { fun PreviewArchiveEmptySearchScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.ARCHIVE, emptyListContent = { ArchiveEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er", list = listOf())), ) } @@ -78,7 +77,7 @@ fun PreviewArchiveEmptySearchScreen() = WireTheme { fun PreviewArchiveScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.ARCHIVE, emptyListContent = { ArchiveEmptyContent() }, conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er")), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt index 4e9564c8b4d..fded5b8f484 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt @@ -94,6 +94,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.WireTopAppBarTitle import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.AddMembersSearchScreenDestination +import com.wire.android.ui.destinations.ConversationFoldersScreenDestination import com.wire.android.ui.destinations.ConversationMediaScreenDestination import com.wire.android.ui.destinations.EditConversationNameScreenDestination import com.wire.android.ui.destinations.EditGuestAccessScreenDestination @@ -111,6 +112,8 @@ import com.wire.android.ui.home.conversations.details.options.GroupConversationO import com.wire.android.ui.home.conversations.details.participants.GroupConversationParticipants import com.wire.android.ui.home.conversations.details.participants.GroupConversationParticipantsState import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavArgs +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavBackArgs import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectConversationDialog @@ -138,6 +141,8 @@ fun GroupConversationDetailsScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, groupConversationDetailResultRecipient: ResultRecipient, + conversationFoldersScreenResultRecipient: + ResultRecipient, viewModel: GroupConversationDetailsViewModel = hiltViewModel() ) { val scope = rememberCoroutineScope() @@ -246,8 +251,10 @@ fun GroupConversationDetailsScreen( onConversationMediaClick = onConversationMediaClick, isAbandonedOneOnOneConversation = viewModel.conversationSheetContent?.isAbandonedOneOnOneConversation( viewModel.groupParticipantsState.data.allCount - ) ?: false - + ) ?: false, + onMoveToFolder = { + navigator.navigate(NavigationCommand(ConversationFoldersScreenDestination(it))) + } ) val tryAgainSnackBarMessage = stringResource(id = R.string.error_unknown_message) @@ -270,6 +277,17 @@ fun GroupConversationDetailsScreen( } } } + + conversationFoldersScreenResultRecipient.onNavResult { result -> + when (result) { + NavResult.Canceled -> {} + is NavResult.Value -> { + scope.launch { + snackbarHostState.showSnackbar(result.value.message) + } + } + } + } } @OptIn(ExperimentalFoundationApi::class) @@ -290,6 +308,7 @@ private fun GroupConversationDetailsContent( isAbandonedOneOnOneConversation: Boolean, onSearchConversationMessagesClick: () -> Unit, onConversationMediaClick: () -> Unit, + onMoveToFolder: (ConversationFoldersNavArgs) -> Unit = {}, initialPageIndex: GroupConversationDetailsTabItem = GroupConversationDetailsTabItem.OPTIONS, changeConversationFavoriteStateViewModel: ChangeConversationFavoriteVM = hiltViewModelScoped( @@ -473,7 +492,7 @@ private fun GroupConversationDetailsContent( } }, changeFavoriteState = changeConversationFavoriteStateViewModel::changeFavoriteState, - moveConversationToFolder = bottomSheetEventsHandler::onMoveConversationToFolder, + moveConversationToFolder = onMoveToFolder, updateConversationArchiveStatus = { // Only show the confirmation dialog if the conversation is not archived if (!it.isArchived) { @@ -611,6 +630,7 @@ fun PreviewGroupConversationDetails() { isUnderLegalHold = false, proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, isFavorite = false, + folder = null, isDeletingConversationLocallyRunning = false ), bottomSheetEventsHandler = GroupConversationDetailsBottomSheetEventsHandler.PREVIEW, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt index 64dc6769a4b..496a5561b22 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt @@ -66,6 +66,7 @@ import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase import com.wire.kalium.logic.functional.getOrNull import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -122,16 +123,17 @@ class GroupConversationDetailsViewModel @Inject constructor( observeConversationDetails() } + private suspend fun groupDetailsFlow(): Flow = observeConversationDetails(conversationId) + .filterIsInstance() + .map { it.conversationDetails } + .filterIsInstance() + .distinctUntilChanged() + .flowOn(dispatcher.io()) + private fun observeConversationDetails() { viewModelScope.launch { - val groupDetailsFlow = - observeConversationDetails(conversationId) - .filterIsInstance() - .map { it.conversationDetails } - .filterIsInstance() - .distinctUntilChanged() - .flowOn(dispatcher.io()) - .shareIn(this, SharingStarted.WhileSubscribed(), 1) + val groupDetailsFlow = groupDetailsFlow() + .shareIn(this, SharingStarted.WhileSubscribed(), 1) val selfTeam = getSelfTeam().getOrNull() val selfUser = observerSelfUser().first() @@ -141,7 +143,6 @@ class GroupConversationDetailsViewModel @Inject constructor( groupDetailsFlow, observeSelfDeletionTimerSettingsForConversation(conversationId, considerSelfUserSettings = false), ) { groupDetails, selfDeletionTimer -> - val isSelfInOwnerTeam = selfTeam?.id != null && selfTeam.id == groupDetails.conversation.teamId?.value val isSelfExternalMember = selfUser.userType == UserType.EXTERNAL val isSelfAnAdmin = groupDetails.selfRole == Conversation.Member.Role.Admin @@ -163,6 +164,7 @@ class GroupConversationDetailsViewModel @Inject constructor( proteusVerificationStatus = groupDetails.conversation.proteusVerificationStatus, isUnderLegalHold = groupDetails.conversation.legalHoldStatus.showLegalHoldIndicator(), isFavorite = groupDetails.isFavorite, + folder = groupDetails.folder, isDeletingConversationLocallyRunning = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersNavArgs.kt new file mode 100644 index 00000000000..d44bbd68710 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersNavArgs.kt @@ -0,0 +1,31 @@ +/* + * 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.home.conversations.folder + +import android.os.Parcelable +import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.parcelize.Parcelize + +data class ConversationFoldersNavArgs( + val conversationId: ConversationId, + val conversationName: String, + val currentFolderId: String? +) + +@Parcelize +data class ConversationFoldersNavBackArgs(val message: String) : Parcelable diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersScreen.kt new file mode 100644 index 00000000000..1a786204a91 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersScreen.kt @@ -0,0 +1,193 @@ +/* + * 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.home.conversations.folder + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.ramcosta.composedestinations.result.ResultBackNavigator +import com.wire.android.R +import com.wire.android.model.Clickable +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.WireDestination +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.bottomsheet.RichMenuItemState +import com.wire.android.ui.common.bottomsheet.SelectableMenuBottomSheetItem +import com.wire.android.ui.common.button.WireButton +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.spacers.VerticalSpace +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.common.typography +import com.wire.kalium.logic.data.conversation.ConversationFolder + +@RootNavGraph +@WireDestination( + navArgsDelegate = ConversationFoldersNavArgs::class, + style = PopUpNavigationAnimation::class +) +@Composable +fun ConversationFoldersScreen( + args: ConversationFoldersNavArgs, + navigator: Navigator, + resultNavigator: ResultBackNavigator, + foldersViewModel: ConversationFoldersVM = + hiltViewModel( + creationCallback = { it.create(ConversationFoldersStateArgs(args.currentFolderId)) } + ), + moveToFolderVM: MoveConversationToFolderVM = + hiltViewModel( + creationCallback = { + it.create(MoveConversationToFolderArgs(args.conversationId, args.conversationName, args.currentFolderId)) + } + ) +) { + val resources = LocalContext.current.resources + + LaunchedEffect(Unit) { + moveToFolderVM.infoMessage.collect { + resultNavigator.setResult(ConversationFoldersNavBackArgs(message = it.asString(resources))) + resultNavigator.navigateBack() + } + } + + Content( + args = args, + foldersState = foldersViewModel.state(), + onNavigationPressed = { navigator.navigateBack() }, + moveConversationToFolder = moveToFolderVM::moveConversationToFolder, + onFolderSelected = foldersViewModel::onFolderSelected + ) +} + +@Composable +private fun Content( + args: ConversationFoldersNavArgs, + foldersState: ConversationFoldersState, + onNavigationPressed: () -> Unit = {}, + moveConversationToFolder: (folder: ConversationFolder) -> Unit = {}, + onFolderSelected: (folderId: String) -> Unit = {}, +) { + val context = LocalContext.current + + val lazyListState = rememberLazyListState() + WireScaffold( + modifier = Modifier + .background(color = colorsScheme().background), + + topBar = { + WireCenterAlignedTopAppBar( + elevation = dimensions().spacing0x, + title = stringResource(id = R.string.label_move_to_folder), + navigationIconType = NavigationIconType.Close(), + onNavigationPressed = onNavigationPressed, + ) + }, + bottomBar = { + Column(modifier = Modifier.padding(dimensions().spacing16x)) { + WireSecondaryButton( + state = WireButtonState.Default, + text = stringResource(id = R.string.label_new_folder), + onClick = { + Toast.makeText( + context, + "Not implemented yet", + Toast.LENGTH_SHORT + ).show() + } + ) + VerticalSpace.x8() + val state = if (foldersState.selectedFolderId != null + && foldersState.selectedFolderId != args.currentFolderId + ) { + WireButtonState.Default + } else { + WireButtonState.Disabled + } + WireButton( + state = state, + text = stringResource(id = R.string.label_done), + onClick = { + moveConversationToFolder( + foldersState.folders.first { it.id == foldersState.selectedFolderId!! } + ) + } + ) + } + } + ) { padding -> + Box( + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { + if (foldersState.folders.isEmpty()) { + Text( + stringResource(R.string.folder_create_description), + modifier = Modifier.align(Alignment.Center), + style = typography().body01, + color = colorsScheme().secondaryText + ) + } else { + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxHeight() + ) { + items(foldersState.folders) { folder -> + val state = if (foldersState.selectedFolderId == folder.id) { + RichMenuItemState.SELECTED + } else { + RichMenuItemState.DEFAULT + } + SelectableMenuBottomSheetItem( + title = folder.name, + onItemClick = Clickable( + enabled = state == RichMenuItemState.DEFAULT, + onClickDescription = stringResource(id = R.string.content_description_select_label), + onClick = { onFolderSelected(folder.id) } + ), + state = state, + modifier = Modifier.height(dimensions().spacing48x) + ) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt index b15fc52eef6..6e80b98aa48 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt @@ -22,29 +22,39 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ScopedArgs import com.wire.android.di.ViewModelScopedPreview import com.wire.kalium.logic.data.conversation.ConversationFolder import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import javax.inject.Inject @ViewModelScopedPreview interface ConversationFoldersVM { fun state(): ConversationFoldersState = ConversationFoldersState(persistentListOf()) + fun onFolderSelected(folderId: String) {} } -@HiltViewModel -class ConversationFoldersVMImpl @Inject constructor( +@HiltViewModel(assistedFactory = ConversationFoldersVMImpl.Factory::class) +class ConversationFoldersVMImpl @AssistedInject constructor( + @Assisted val args: ConversationFoldersStateArgs, private val observeUserFoldersUseCase: ObserveUserFoldersUseCase, ) : ConversationFoldersVM, ViewModel() { - private var state by mutableStateOf(ConversationFoldersState(persistentListOf())) + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(args: ConversationFoldersStateArgs): ConversationFoldersVMImpl + } + + private var state by mutableStateOf(ConversationFoldersState(persistentListOf(), args.selectedFolderId)) override fun state(): ConversationFoldersState = state @@ -55,14 +65,23 @@ class ConversationFoldersVMImpl @Inject constructor( private fun observeUserFolders() = viewModelScope.launch { observeUserFoldersUseCase() .collect { folders -> - state = ConversationFoldersState(folders.toPersistentList()) + state = state.copy(folders = folders.toPersistentList()) } } + + override fun onFolderSelected(folderId: String) { + state = state.copy(selectedFolderId = folderId) + } } -data class ConversationFoldersState(val folders: PersistentList) +data class ConversationFoldersState(val folders: PersistentList, val selectedFolderId: String? = null) @Serializable -object ConversationFoldersStateArgs : ScopedArgs { - override val key = "ConversationFoldersStateArgsKey" +data class ConversationFoldersStateArgs(val selectedFolderId: String?) : ScopedArgs { + + override val key = "$ARGS_KEY:$selectedFolderId" + + companion object { + const val ARGS_KEY = "ConversationFoldersStateArgsKey" + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderVM.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderVM.kt new file mode 100644 index 00000000000..bc21666954b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderVM.kt @@ -0,0 +1,118 @@ +/* + * 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.home.conversations.folder + +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.R +import com.wire.android.di.AssistedViewModelFactory +import com.wire.android.di.ScopedArgs +import com.wire.android.di.ViewModelScopedPreview +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable + +@ViewModelScopedPreview +interface MoveConversationToFolderVM { + val infoMessage: SharedFlow + get() = MutableSharedFlow() + + fun actionableState(): MoveConversationToFolderState = MoveConversationToFolderState() + fun moveConversationToFolder(folder: ConversationFolder) {} +} + +@HiltViewModel(assistedFactory = MoveConversationToFolderVMImpl.Factory::class) +class MoveConversationToFolderVMImpl @AssistedInject constructor( + private val dispatchers: DispatcherProvider, + @Assisted val args: MoveConversationToFolderArgs, + private val moveConversationToFolder: MoveConversationToFolderUseCase, +) : MoveConversationToFolderVM, ViewModel() { + + private var state: MoveConversationToFolderState by mutableStateOf(MoveConversationToFolderState()) + + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(args: MoveConversationToFolderArgs): MoveConversationToFolderVMImpl + } + + private val _infoMessage = MutableSharedFlow() + override val infoMessage = _infoMessage.asSharedFlow() + + override fun actionableState(): MoveConversationToFolderState = state + override fun moveConversationToFolder(folder: ConversationFolder) { + viewModelScope.launch { + state = state.copy(isPerformingAction = true) + val result = withContext(dispatchers.io()) { + moveConversationToFolder.invoke( + args.conversationId, + folder.id, + args.currentFolderId + ) + } + when (result) { + is MoveConversationToFolderUseCase.Result.Failure -> _infoMessage.emit( + UIText.StringResource( + R.string.move_to_folder_failed, + args.conversationName, + ) + ) + + MoveConversationToFolderUseCase.Result.Success -> _infoMessage.emit( + UIText.StringResource( + R.string.move_to_folder_success, + args.conversationName, + folder.name + ) + ) + } + state = state.copy(isPerformingAction = false) + } + } +} + +data class MoveConversationToFolderState( + val isPerformingAction: Boolean = false, +) + +@Serializable +data class MoveConversationToFolderArgs( + val conversationId: ConversationId, + val conversationName: String, + val currentFolderId: String?, +) : ScopedArgs { + override val key = "$ARGS_KEY:$conversationId$currentFolderId" + + companion object { + const val ARGS_KEY = "MoveConversationToFolderArgsKey" + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 2784cf03f0f..f832ae802fe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -76,7 +76,6 @@ import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCa import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import com.wire.kalium.logic.feature.team.Result import com.wire.kalium.logic.feature.user.GetSelfUserUseCase -import com.wire.kalium.logic.functional.combine import com.wire.kalium.util.DateTimeUtil import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -141,7 +140,7 @@ class ConversationListViewModelPreview( class ConversationListViewModelImpl @AssistedInject constructor( @Assisted val conversationsSource: ConversationsSource, @Assisted private val usePagination: Boolean = BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED, - dispatcher: DispatcherProvider, + private val dispatcher: DispatcherProvider, private val updateConversationMutedStatus: UpdateConversationMutedStatusUseCase, private val getConversationsPaginated: GetConversationsFromSearchUseCase, private val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase, @@ -178,6 +177,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( override val closeBottomSheet = MutableSharedFlow() private val searchQueryFlow: MutableStateFlow = MutableStateFlow("") + private val isSelfUserUnderLegalHoldFlow = MutableSharedFlow(replay = 1) private val containsNewActivitiesSection = when (conversationsSource) { ConversationsSource.MAIN, @@ -192,9 +192,12 @@ class ConversationListViewModelImpl @AssistedInject constructor( private val conversationsPaginatedFlow: Flow> = searchQueryFlow .debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE } .onStart { emit("") } + .combine(isSelfUserUnderLegalHoldFlow, ::Pair) .distinctUntilChanged() - .combine(audioMessagePlayer.playingAudioMessageFlow) - .flatMapLatest { (searchQuery, playingAudioMessage) -> + .combine(audioMessagePlayer.playingAudioMessageFlow) { (searchQuery, isSelfUserUnderLegalHold), playingAudioMessage -> + Triple(searchQuery, isSelfUserUnderLegalHold, playingAudioMessage) + } + .flatMapLatest { (searchQuery, isSelfUserUnderLegalHold, playingAudioMessage) -> getConversationsPaginated( searchQuery = searchQuery, fromArchive = conversationsSource == ConversationsSource.ARCHIVE, @@ -202,30 +205,30 @@ class ConversationListViewModelImpl @AssistedInject constructor( onlyInteractionEnabled = false, newActivitiesOnTop = containsNewActivitiesSection, playingAudioMessage = playingAudioMessage - ).combine(observeLegalHoldStateForSelfUser()) - .map { (conversations, selfUserLegalHoldStatus) -> - conversations.map { it.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus) } - .insertSeparators { before, after -> - when { - // do not add separators if the list shouldn't show conversations grouped into different folders - !containsNewActivitiesSection -> null - - before == null && after != null && after.hasNewActivitiesToShow -> - // list starts with items with "new activities" - ConversationFolder.Predefined.NewActivities - - before == null && after != null && !after.hasNewActivitiesToShow -> - // list doesn't contain any items with "new activities" - ConversationFolder.Predefined.Conversations - - before != null && before.hasNewActivitiesToShow && after != null && !after.hasNewActivitiesToShow -> - // end of "new activities" section and beginning of "conversations" section - ConversationFolder.Predefined.Conversations - - else -> null - } + ).map { pagingData -> + pagingData + .map { it.hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) } + .insertSeparators { before, after -> + when { + // do not add separators if the list shouldn't show conversations grouped into different folders + !containsNewActivitiesSection -> null + + before == null && after != null && after.hasNewActivitiesToShow -> + // list starts with items with "new activities" + ConversationFolder.Predefined.NewActivities + + before == null && after != null && !after.hasNewActivitiesToShow -> + // list doesn't contain any items with "new activities" + ConversationFolder.Predefined.Conversations + + before != null && before.hasNewActivitiesToShow && after != null && !after.hasNewActivitiesToShow -> + // end of "new activities" section and beginning of "conversations" section + ConversationFolder.Predefined.Conversations + + else -> null } - } + } + } } .flowOn(dispatcher.io()) .cachedIn(viewModelScope) @@ -239,50 +242,64 @@ class ConversationListViewModelImpl @AssistedInject constructor( private set init { + observeSelfUserLegalHoldState() if (!usePagination) { - viewModelScope.launch { - searchQueryFlow - .debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE } - .onStart { emit("") } - .distinctUntilChanged() - .flatMapLatest { searchQuery: String -> - combine( - observeConversationListDetailsWithEvents( - fromArchive = conversationsSource == ConversationsSource.ARCHIVE, - conversationFilter = conversationsSource.toFilter() - ), - observeLegalHoldStateForSelfUser(), - audioMessagePlayer.playingAudioMessageFlow - ) { conversations, selfUserLegalHoldStatus, playingAudioMessage -> - conversations.map { conversationDetails -> - conversationDetails.toConversationItem( - userTypeMapper = userTypeMapper, - searchQuery = searchQuery, - selfUserTeamId = observeSelfUser().firstOrNull()?.teamId, - playingAudioMessage = playingAudioMessage - ).hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus) - } to searchQuery - } - } - .map { (conversationItems, searchQuery) -> - if (searchQuery.isEmpty()) { - conversationItems.withFolders(source = conversationsSource).toImmutableMap() - } else { - searchConversation( - conversationDetails = conversationItems, - searchQuery = searchQuery - ).withFolders(source = conversationsSource).toImmutableMap() - } + observeNonPaginatedSearchConversationList() + } + } + + private fun observeSelfUserLegalHoldState() { + viewModelScope.launch { + observeLegalHoldStateForSelfUser() + .map { it is LegalHoldStateForSelfUser.Enabled } + .flowOn(dispatcher.io()) + .collect { isSelfUserUnderLegalHoldFlow.emit(it) } + } + } + + private fun observeNonPaginatedSearchConversationList() { + viewModelScope.launch { + searchQueryFlow + .debounce { if (it.isEmpty()) 0L else DEFAULT_SEARCH_QUERY_DEBOUNCE } + .onStart { emit("") } + .distinctUntilChanged() + .flatMapLatest { searchQuery: String -> + combine( + observeConversationListDetailsWithEvents( + fromArchive = conversationsSource == ConversationsSource.ARCHIVE, + conversationFilter = conversationsSource.toFilter() + ), + isSelfUserUnderLegalHoldFlow, + audioMessagePlayer.playingAudioMessageFlow + ) { conversations, isSelfUserUnderLegalHold, playingAudioMessage -> + conversations.map { conversationDetails -> + conversationDetails.toConversationItem( + userTypeMapper = userTypeMapper, + searchQuery = searchQuery, + selfUserTeamId = observeSelfUser().firstOrNull()?.teamId, + playingAudioMessage = playingAudioMessage + ).hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) + } to searchQuery } - .flowOn(dispatcher.io()) - .collect { - conversationListState = ConversationListState.NotPaginated( - isLoading = false, - conversations = it, - domain = currentAccount.domain - ) + } + .map { (conversationItems, searchQuery) -> + if (searchQuery.isEmpty()) { + conversationItems.withFolders(source = conversationsSource).toImmutableMap() + } else { + searchConversation( + conversationDetails = conversationItems, + searchQuery = searchQuery + ).withFolders(source = conversationsSource).toImmutableMap() } - } + } + .flowOn(dispatcher.io()) + .collect { + conversationListState = ConversationListState.NotPaginated( + isLoading = false, + conversations = it, + domain = currentAccount.domain + ) + } } } @@ -509,11 +526,13 @@ private fun ConversationsSource.toFilter(): ConversationFilter = when (this) { is ConversationsSource.FOLDER -> ConversationFilter.Folder(folderId = folderId, folderName = folderName) } -private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(selfUserLegalHoldStatus: LegalHoldStateForSelfUser) = -// if self user is under legal hold then we shouldn't show legal hold indicator next to every conversation - // the indication is shown in the header of the conversation list for self user in that case and it's enough - when (selfUserLegalHoldStatus) { - is LegalHoldStateForSelfUser.Enabled -> when (this) { +/** + * If self user is under legal hold then we shouldn't show legal hold indicator next to every conversation as in that case + * the legal hold indication is shown in the header of the conversation list for self user in that case and it's enough. + */ +private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold: Boolean) = + when (isSelfUserUnderLegalHold) { + true -> when (this) { is ConversationItem.ConnectionConversation -> this.copy(showLegalHoldIndicator = false) is ConversationItem.GroupConversation -> this.copy(showLegalHoldIndicator = false) is ConversationItem.PrivateConversation -> this.copy(showLegalHoldIndicator = false) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt index 95d4a32083c..a152d8e8b9f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt @@ -58,6 +58,7 @@ import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.topappbar.search.SearchBarState import com.wire.android.ui.common.topappbar.search.rememberSearchbarState import com.wire.android.ui.common.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.ConversationFoldersScreenDestination import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.NewConversationSearchPeopleScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination @@ -90,7 +91,6 @@ fun ConversationsScreenContent( lazyListState: LazyListState = rememberLazyListState(), loadingListContent: @Composable (LazyListState) -> Unit = { ConversationListLoadingContent(it) }, conversationsSource: ConversationsSource = ConversationsSource.MAIN, - initiallyLoaded: Boolean = LocalInspectionMode.current, conversationListViewModel: ConversationListViewModel = when { LocalInspectionMode.current -> ConversationListViewModelPreview() else -> hiltViewModel( @@ -203,10 +203,7 @@ fun ConversationsScreenContent( when (val state = conversationListViewModel.conversationListState) { is ConversationListState.Paginated -> { val lazyPagingItems = state.conversations.collectAsLazyPagingItems() - var showLoading by remember(conversationsSource) { mutableStateOf(!initiallyLoaded) } - if (lazyPagingItems.loadState.refresh != LoadState.Loading && showLoading) { - showLoading = false - } + val showLoading = lazyPagingItems.loadState.refresh == LoadState.Loading && lazyPagingItems.itemCount == 0 when { // when conversation list is not yet fetched, show loading indicator @@ -344,7 +341,9 @@ fun ConversationsScreenContent( ) }, changeFavoriteState = changeConversationFavoriteStateViewModel::changeFavoriteState, - moveConversationToFolder = conversationListViewModel::moveConversationToFolder, + moveConversationToFolder = { navArgs -> + navigator.navigate(NavigationCommand(ConversationFoldersScreenDestination(navArgs))) + }, updateConversationArchiveStatus = showConfirmationDialogOrUnarchive(), clearConversationContent = clearContentDialogState::show, blockUser = blockUserDialogState::show, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt index 3f9ff8fe23a..30f77cecae3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt @@ -34,7 +34,6 @@ import com.wire.android.ui.home.conversationslist.model.ConversationsSource import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.conversation.ConversationFilter -import kotlinx.coroutines.flow.flowOf @HomeNavGraph(start = true) @WireDestination @@ -70,7 +69,7 @@ fun PreviewAllConversationsEmptyScreen() = WireTheme { searchBarState = rememberSearchbarState(), conversationsSource = ConversationsSource.MAIN, emptyListContent = { ConversationsEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(list = listOf())), ) } @@ -79,10 +78,10 @@ fun PreviewAllConversationsEmptyScreen() = WireTheme { fun PreviewAllConversationsEmptySearchScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.MAIN, emptyListContent = { ConversationsEmptyContent() }, - conversationListViewModel = ConversationListViewModelPreview(flowOf()), + conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow(searchQuery = "er", list = listOf())), ) } @@ -91,7 +90,7 @@ fun PreviewAllConversationsEmptySearchScreen() = WireTheme { fun PreviewAllConversationsSearchScreen() = WireTheme { ConversationsScreenContent( navigator = rememberNavigator {}, - searchBarState = rememberSearchbarState(searchQueryTextState = TextFieldState(initialText = "er")), + searchBarState = rememberSearchbarState(initialIsSearchActive = true, searchQueryTextState = TextFieldState(initialText = "er")), conversationsSource = ConversationsSource.MAIN, emptyListContent = { ConversationsEmptyContent() }, conversationListViewModel = ConversationListViewModelPreview(previewConversationFoldersFlow("er")), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt index 02cfc1dad4c..c719b28acd8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt @@ -399,6 +399,7 @@ fun PreviewGroupConversationItemWithUnreadCount() = WireTheme { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, + folder = null, playingAudio = null ), modifier = Modifier, @@ -427,6 +428,7 @@ fun PreviewGroupConversationItemWithNoBadges() = WireTheme { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, + folder = null, playingAudio = null ), modifier = Modifier, @@ -457,6 +459,7 @@ fun PreviewGroupConversationItemWithLastDeletedMessage() = WireTheme { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, + folder = null, playingAudio = null ), modifier = Modifier, @@ -485,6 +488,7 @@ fun PreviewGroupConversationItemWithMutedBadgeAndUnreadMentionBadge() = WireThem mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, + folder = null, playingAudio = null ), modifier = Modifier, @@ -514,6 +518,7 @@ fun PreviewGroupConversationItemWithOngoingCall() = WireTheme { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, + folder = null, playingAudio = null ), modifier = Modifier, @@ -599,6 +604,7 @@ fun PreviewPrivateConversationItemWithBlockedBadge() = WireTheme { proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, isUserDeleted = false, + folder = null, playingAudio = null ), modifier = Modifier, @@ -627,6 +633,7 @@ fun PreviewPrivateConversationItemWithPlayingAudio() = WireTheme { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, + folder = null, playingAudio = PlayingAudioInConversation("some_id", true) ), modifier = Modifier, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt index 7d9577caba9..3f85762a720 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt @@ -30,6 +30,8 @@ import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode +import androidx.paging.LoadState +import androidx.paging.LoadStates import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -214,6 +216,7 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, searchQuery = searchQuery, isFavorite = false, + folder = null, playingAudio = null ) ) @@ -235,6 +238,7 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f searchQuery = searchQuery, isFavorite = false, isUserDeleted = false, + folder = null, playingAudio = null ) ) @@ -245,7 +249,16 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f fun previewConversationFoldersFlow( searchQuery: String = "", list: List = previewConversationFolders(searchQuery = searchQuery) -) = flowOf(PagingData.from(list)) +) = flowOf( + PagingData.from( + data = list, + sourceLoadStates = LoadStates( + prepend = LoadState.NotLoading(true), + append = LoadState.NotLoading(true), + refresh = LoadState.NotLoading(true), + ) + ) +) fun previewConversationFolders(withFolders: Boolean = true, searchQuery: String = "", unreadCount: Int = 3, readCount: Int = 6) = buildList { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationFolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationFolder.kt index af1daa69272..5c1882229e4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationFolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationFolder.kt @@ -21,6 +21,7 @@ package com.wire.android.ui.home.conversationslist.model import androidx.annotation.StringRes import com.wire.android.R +// TODO needs renaming sealed class ConversationFolder : ConversationFolderItem { sealed class Predefined(@StringRes val folderNameResId: Int) : ConversationFolder() { data object Conversations : Predefined(R.string.conversation_label_conversations) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt index 8c4e0763a50..5db257b9956 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt @@ -30,6 +30,7 @@ import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.isTeammate import kotlinx.serialization.Serializable +import com.wire.kalium.logic.data.conversation.ConversationFolder as CurrentFolder @Serializable sealed class ConversationItem : ConversationFolderItem { @@ -41,6 +42,7 @@ sealed class ConversationItem : ConversationFolderItem { abstract val teamId: TeamId? abstract val isArchived: Boolean abstract val isFavorite: Boolean + abstract val folder: CurrentFolder? abstract val mlsVerificationStatus: Conversation.VerificationStatus abstract val proteusVerificationStatus: Conversation.VerificationStatus abstract val hasNewActivitiesToShow: Boolean @@ -64,6 +66,7 @@ sealed class ConversationItem : ConversationFolderItem { override val teamId: TeamId?, override val isArchived: Boolean, override val isFavorite: Boolean, + override val folder: CurrentFolder?, override val mlsVerificationStatus: Conversation.VerificationStatus, override val proteusVerificationStatus: Conversation.VerificationStatus, override val hasNewActivitiesToShow: Boolean = false, @@ -86,6 +89,7 @@ sealed class ConversationItem : ConversationFolderItem { override val teamId: TeamId?, override val isArchived: Boolean, override val isFavorite: Boolean, + override val folder: CurrentFolder?, override val mlsVerificationStatus: Conversation.VerificationStatus, override val proteusVerificationStatus: Conversation.VerificationStatus, override val hasNewActivitiesToShow: Boolean = false, @@ -104,6 +108,7 @@ sealed class ConversationItem : ConversationFolderItem { override val badgeEventType: BadgeEventType, override val isArchived: Boolean = false, override val isFavorite: Boolean = false, + override val folder: CurrentFolder? = null, override val hasNewActivitiesToShow: Boolean = false, override val searchQuery: String = "", ) : ConversationItem() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt index 42b69b8585a..84f9b347b18 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.messagecomposer import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box @@ -292,6 +293,7 @@ private fun MessageComposerTextInput( state = WireTextFieldState.Default, keyboardOptions = KeyboardOptions.DefaultText.copy(imeAction = ImeAction.None), modifier = modifier + .focusable(true) .focusRequester(focusRequester) .onFocusChanged { focusState -> if (focusState.isFocused) { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index 6a869b01690..9ddd2dc33da 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -52,7 +52,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultBackNavigator +import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.R import com.wire.android.di.hiltViewModelScoped import com.wire.android.navigation.BackStackMode @@ -93,8 +95,11 @@ import com.wire.android.ui.destinations.ConversationMediaScreenDestination import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.DeviceDetailsScreenDestination import com.wire.android.ui.destinations.SearchConversationMessagesScreenDestination +import com.wire.android.ui.destinations.ConversationFoldersScreenDestination import com.wire.android.ui.home.conversations.details.SearchAndMediaRow import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavArgs +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavBackArgs import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.legalhold.banner.LegalHoldSubjectBanner @@ -128,6 +133,8 @@ fun OtherUserProfileScreen( navigator: Navigator, navArgs: OtherUserProfileNavArgs, resultNavigator: ResultBackNavigator, + conversationFoldersScreenResultRecipient: + ResultRecipient, viewModel: OtherUserProfileScreenViewModel = hiltViewModel() ) { val snackbarHostState = LocalSnackbarHostState.current @@ -200,6 +207,7 @@ fun OtherUserProfileScreen( navigateBack = navigator::navigateBack, onConversationMediaClick = onConversationMediaClick, onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, + onMoveToFolder = null // TODO implement when conversation details will be available in OtherUserProfileScreenViewModel ) LaunchedEffect(Unit) { @@ -224,6 +232,17 @@ fun OtherUserProfileScreen( if (viewModel.state.errorLoadingUser != null) { UserNotFoundDialog(onActionButtonClicked = navigator::navigateBack) } + + conversationFoldersScreenResultRecipient.onNavResult { result -> + when (result) { + NavResult.Canceled -> {} + is NavResult.Value -> { + scope.launch { + snackbarHostState.showSnackbar(result.value.message) + } + } + } + } } @SuppressLint("UnusedCrossfadeTargetStateParameter", "LongParameterList") @@ -244,6 +263,7 @@ fun OtherProfileScreenContent( onConversationMediaClick: () -> Unit = {}, navigateBack: () -> Unit = {}, onLegalHoldLearnMoreClick: () -> Unit = {}, + onMoveToFolder: ((ConversationFoldersNavArgs) -> Unit)? = null, changeConversationFavoriteViewModel: ChangeConversationFavoriteVM = hiltViewModelScoped( ChangeConversationFavoriteStateArgs @@ -372,6 +392,7 @@ fun OtherProfileScreenContent( archivingStatusState = archivingConversationDialogState::show, changeFavoriteState = changeConversationFavoriteViewModel::changeFavoriteState, closeBottomSheet = closeBottomSheet, + onMoveToFolder = onMoveToFolder ) } ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index d1828b55a25..f868c94b1e4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -417,7 +417,8 @@ class OtherUserProfileScreenViewModel @Inject constructor( mlsVerificationStatus = conversation.mlsVerificationStatus, proteusVerificationStatus = conversation.proteusVerificationStatus, isUnderLegalHold = conversation.legalHoldStatus.showLegalHoldIndicator(), - isFavorite = null, + isFavorite = null, // TODO check if we need to pass isFavorite + folder = null, // TODO check if we need to pass folder isDeletingConversationLocallyRunning = false ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt index d5b9203eef4..0dd5c6dd88b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/bottomsheet/OtherUserProfileBottomSheet.kt @@ -23,6 +23,7 @@ import com.wire.android.ui.common.bottomsheet.conversation.ConversationSheetCont import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationSheetState import com.wire.android.ui.common.dialogs.BlockUserDialogState import com.wire.android.ui.common.dialogs.UnblockUserDialogState +import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavArgs import com.wire.android.ui.home.conversationslist.model.DialogState import com.wire.android.ui.home.conversationslist.model.GroupDialogState import com.wire.android.ui.userprofile.other.OtherUserProfileBottomSheetEventsHandler @@ -37,7 +38,8 @@ fun OtherUserProfileBottomSheetContent( unblockUser: (UnblockUserDialogState) -> Unit, changeFavoriteState: (GroupDialogState, addToFavorite: Boolean) -> Unit, closeBottomSheet: () -> Unit, - getBottomSheetVisibility: () -> Boolean + getBottomSheetVisibility: () -> Boolean, + onMoveToFolder: ((ConversationFoldersNavArgs) -> Unit)? ) { when (val state = bottomSheetState.bottomSheetContentState) { is BottomSheetContent.Conversation -> { @@ -53,7 +55,7 @@ fun OtherUserProfileBottomSheetContent( ) }, changeFavoriteState = changeFavoriteState, - moveConversationToFolder = eventsHandler::onMoveConversationToFolder, + moveConversationToFolder = onMoveToFolder, updateConversationArchiveStatus = { if (!it.isArchived) { archivingStatusState(it) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index 60d081bf260..c562b1ba5dc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -120,6 +120,11 @@ fun SelfUserProfileScreen( ) { val legalHoldSubjectDialogState = rememberVisibilityState() + LaunchedEffect(Unit) { + // Check if the user is able to migrate to a team account, every time the screen is shown + viewModelSelf.checkIfUserAbleToMigrateToTeamAccount() + } + SelfUserProfileContent( state = viewModelSelf.userProfileState, onCloseClick = navigator::navigateBack, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 2820cc43ae5..d3e812b466d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -112,7 +112,6 @@ class SelfUserProfileViewModel @Inject constructor( init { viewModelScope.launch { fetchSelfUser() - checkIfUserAbleToMigrateToTeamAccount() observeEstablishedCall() fetchIsReadOnlyAccount() observeLegalHoldStatus() @@ -120,9 +119,8 @@ class SelfUserProfileViewModel @Inject constructor( } } - private suspend fun checkIfUserAbleToMigrateToTeamAccount() { - val isAbleToMigrateToTeamAccount = canMigrateFromPersonalToTeam() && userProfileState.teamName.isNullOrBlank() - userProfileState = userProfileState.copy(isAbleToMigrateToTeamAccount = isAbleToMigrateToTeamAccount) + suspend fun checkIfUserAbleToMigrateToTeamAccount() { + userProfileState = userProfileState.copy(isAbleToMigrateToTeamAccount = canMigrateFromPersonalToTeam()) } private suspend fun fetchIsReadOnlyAccount() { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt index be2d3d9625e..925b5b0a19a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationState.kt @@ -23,6 +23,9 @@ import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamFai data class TeamMigrationState( val teamNameTextState: TextFieldState = TextFieldState(), val shouldShowMigrationLeaveDialog: Boolean = false, + val isMigrating: Boolean = false, val currentStep: Int = 0, + val username: String = "", + val teamUrl: String = "", val migrationFailure: MigrateFromPersonalToTeamFailure? = null ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt index 818a70b9b27..f4c900f4491 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt @@ -24,6 +24,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamFailure import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamResult import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCase @@ -34,12 +36,19 @@ import javax.inject.Inject @HiltViewModel class TeamMigrationViewModel @Inject constructor( private val anonymousAnalyticsManager: AnonymousAnalyticsManager, - private val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase + private val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase, + private val getSelfUser: GetSelfUserUseCase, + private val getTeamUrl: GetTeamUrlUseCase ) : ViewModel() { var teamMigrationState by mutableStateOf(TeamMigrationState()) private set + init { + setUsername() + setTeamUrl() + } + fun showMigrationLeaveDialog() { teamMigrationState = teamMigrationState.copy(shouldShowMigrationLeaveDialog = true) } @@ -92,6 +101,10 @@ class TeamMigrationViewModel @Inject constructor( ) } + fun setIsMigratingState(isMigrating: Boolean) { + teamMigrationState = teamMigrationState.copy(isMigrating = isMigrating) + } + fun migrateFromPersonalToTeamAccount(onSuccess: () -> Unit) { viewModelScope.launch { migrateFromPersonalToTeam.invoke( @@ -117,4 +130,20 @@ class TeamMigrationViewModel @Inject constructor( private fun onMigrationFailure(failure: MigrateFromPersonalToTeamFailure) { teamMigrationState = teamMigrationState.copy(migrationFailure = failure) } + + private fun setUsername() { + viewModelScope.launch { + getSelfUser().collect { selfUser -> + selfUser.name?.let { + teamMigrationState = teamMigrationState.copy(username = it) + } + } + } + } + + private fun setTeamUrl() { + viewModelScope.launch { + teamMigrationState = teamMigrationState.copy(teamUrl = getTeamUrl()) + } + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt index 65ef95d4bac..1d901d895ba 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/common/BottomLineButtons.kt @@ -42,6 +42,7 @@ import com.wire.android.ui.theme.WireTheme fun BottomLineButtons( isContinueButtonEnabled: Boolean, modifier: Modifier = Modifier, + isMigrating: Boolean = false, isBackButtonVisible: Boolean = true, backButtonContentDescription: String = stringResource(R.string.personal_to_team_migration_back_button_label), onBack: () -> Unit = { }, @@ -67,7 +68,12 @@ fun BottomLineButtons( .fillMaxWidth() .semantics(true) { contentDescription = backButtonContentDescription }, text = stringResource(R.string.personal_to_team_migration_back_button_label), - onClick = onBack + onClick = onBack, + state = if (isMigrating) { + WireButtonState.Disabled + } else { + WireButtonState.Default + } ) } @@ -77,7 +83,8 @@ fun BottomLineButtons( .padding(top = dimensions().spacing6x), text = stringResource(R.string.label_continue), onClick = onContinue, - state = if (isContinueButtonEnabled) { + loading = isMigrating, + state = if (isContinueButtonEnabled && !isMigrating) { WireButtonState.Default } else { WireButtonState.Disabled @@ -91,6 +98,7 @@ fun BottomLineButtons( private fun BottomLineButtonsPreview() { WireTheme { BottomLineButtons( + isMigrating = false, isContinueButtonEnabled = true ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt index 996e6a2a4fc..5d0d2e32dc5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt @@ -78,11 +78,14 @@ fun TeamMigrationConfirmationStepScreen( val state = teamMigrationViewModel.teamMigrationState TeamMigrationConfirmationStepScreenContent( + isMigrating = state.isMigrating, onContinueButtonClicked = { + teamMigrationViewModel.setIsMigratingState(true) teamMigrationViewModel.migrateFromPersonalToTeamAccount( onSuccess = { + teamMigrationViewModel.setIsMigratingState(false) navigator.navigate(TeamMigrationDoneStepScreenDestination) - }, + } ) }, onBackPressed = { @@ -179,6 +182,7 @@ private fun ErrorDialog( @Composable private fun TeamMigrationConfirmationStepScreenContent( modifier: Modifier = Modifier, + isMigrating: Boolean = false, onContinueButtonClicked: () -> Unit = { }, onBackPressed: () -> Unit = { } ) { @@ -254,6 +258,7 @@ private fun TeamMigrationConfirmationStepScreenContent( } val isContinueButtonEnabled = agreedToMigrationTerms.value && acceptedWireTermsOfUse.value BottomLineButtons( + isMigrating = isMigrating, isContinueButtonEnabled = isContinueButtonEnabled, onContinue = onContinueButtonClicked, backButtonContentDescription = stringResource(R.string.personal_to_team_migration_back_button_confirmation_content_description), diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt index 2f582d61aa3..9cfbd4bc63e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt @@ -65,7 +65,6 @@ fun TeamMigrationDoneStepScreen( ) { val context = LocalContext.current - val teamManagementUrl = stringResource(R.string.url_team_management) TeamMigrationDoneStepContent( onBackToWireClicked = { teamMigrationViewModel.sendPersonalTeamCreationFlowCompletedEvent( @@ -79,11 +78,14 @@ fun TeamMigrationDoneStepScreen( ) }, onOpenTeamManagementClicked = { + val teamManagementUrl = teamMigrationViewModel.teamMigrationState.teamUrl + teamMigrationViewModel.sendPersonalTeamCreationFlowCompletedEvent( modalOpenTeamManagementButtonClicked = true ) CustomTabsHelper.launchUrl(context, teamManagementUrl) }, + username = teamMigrationViewModel.teamMigrationState.username, teamName = teamMigrationViewModel.teamMigrationState.teamNameTextState.text.toString() ) @@ -98,6 +100,7 @@ fun TeamMigrationDoneStepScreen( private fun TeamMigrationDoneStepContent( onBackToWireClicked: () -> Unit, onOpenTeamManagementClicked: () -> Unit, + username: String, teamName: String, modifier: Modifier = Modifier ) { @@ -130,7 +133,7 @@ private fun TeamMigrationDoneStepContent( bottom = dimensions().spacing56x ) .align(alignment = Alignment.CenterHorizontally), - text = stringResource(R.string.personal_to_team_migration_done_step, teamName), + text = stringResource(R.string.personal_to_team_migration_done_step, username), style = MaterialTheme.wireTypography.title01, color = colorsScheme().onBackground ) @@ -187,6 +190,6 @@ private fun TeamMigrationDoneStepContent( @Composable private fun TeamMigrationDoneStepScreenPreview() { WireTheme { - TeamMigrationDoneStepContent({}, {}, teamName = "teamName") + TeamMigrationDoneStepContent({}, {}, username = "John", teamName = "teamName") } } diff --git a/app/src/main/res/layout/view_emoji_picker.xml b/app/src/main/res/layout/view_emoji_picker.xml new file mode 100644 index 00000000000..0cb5edd7798 --- /dev/null +++ b/app/src/main/res/layout/view_emoji_picker.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index be377ca64bc..b88bf72821e 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -21,6 +21,9 @@ #17181A #EDEFF0 + #C5C6CF + #C5C6CF + #17181A diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 60279fa17be..46e28ece026 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -23,8 +23,6 @@ --> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5fc9bc9b7ef..607c6077505 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -21,6 +21,9 @@ #EDEFF0 #34373D + #34373D + #34373D + #FFFFFF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9aa87f0d0cd..7aa6a00cf9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Service Deleted Try Again + Done Message could not be sent due to connectivity issues. The edited message could not be sent due to connectivity issues. Message could not be sent, as the backend of %s could not be reached. @@ -284,7 +285,6 @@ https://support.wire.com/hc/articles/360002855557 https://support.wire.com/hc/articles/360002855817 https://wire.com/pricing - https://teams.wire.com/ https://teams.wire.com/login https://wire.com/en/enterprise @@ -663,7 +663,9 @@ Notifications Add to Favorites Remove from Favorites - Move to Folder + Move to Folder... + New Folder + Create a new folder by pressing the\n“New Folder” button Move to Archive Unarchive Clear Content… @@ -1676,4 +1678,9 @@ In group conversations, the group admin can overwrite this setting. Wire could not complete your team creation due to a slow internet connection. Wire could not complete your team creation due to an unknown error. You + Select Reaction + + + “%1$s” was moved to “%2$s” + “%1$s” could not be moved diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 564774fecbf..d35c62cd6a2 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -21,15 +21,16 @@ + +