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 b3f3dab757..3e8f8be58a 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 244cd31a5c..a85838d76b 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/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index abda5b931a..39b48db293 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -65,7 +65,8 @@ fun ConversationDetailsWithEvents.toConversationItem( proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus, hasNewActivitiesToShow = hasNewActivitiesToShow, searchQuery = searchQuery, - isFavorite = conversationDetails.isFavorite + isFavorite = conversationDetails.isFavorite, + folder = conversationDetails.folder ) } @@ -103,7 +104,8 @@ fun ConversationDetailsWithEvents.toConversationItem( proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus, hasNewActivitiesToShow = hasNewActivitiesToShow, searchQuery = searchQuery, - isFavorite = conversationDetails.isFavorite + isFavorite = conversationDetails.isFavorite, + folder = conversationDetails.folder ) } 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 031ae12e56..9277fa56e0 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 98706114d9..afcd2d45ba 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 e0a8ec20bc..1b8b4302c9 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 fe26a89cd4..42cd48af56 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/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 3b48dd1362..9f82289be6 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/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt index 4e9564c8b4..fded5b8f48 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 64dc6769a4..496a5561b2 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 0000000000..d44bbd6871 --- /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 0000000000..1a786204a9 --- /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 b15fc52eef..6e80b98aa4 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 0000000000..bc21666954 --- /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/ConversationsScreenContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt index 6f099dfd3b..8c5eb0945c 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 @@ -323,7 +324,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/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt index d462bc9023..a697397f46 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 @@ -322,7 +322,8 @@ fun PreviewGroupConversationItemWithUnreadCount() = WireTheme { isFromTheSameTeam = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + folder = null ), modifier = Modifier, isSelectableItem = false, @@ -349,7 +350,8 @@ fun PreviewGroupConversationItemWithNoBadges() = WireTheme { isFromTheSameTeam = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + folder = null ), modifier = Modifier, isSelectableItem = false, @@ -378,7 +380,8 @@ fun PreviewGroupConversationItemWithLastDeletedMessage() = WireTheme { isFromTheSameTeam = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + folder = null ), modifier = Modifier, isSelectableItem = false, @@ -405,7 +408,8 @@ fun PreviewGroupConversationItemWithMutedBadgeAndUnreadMentionBadge() = WireThem isFromTheSameTeam = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + folder = null ), modifier = Modifier, isSelectableItem = false, @@ -433,7 +437,8 @@ fun PreviewGroupConversationItemWithOngoingCall() = WireTheme { isFromTheSameTeam = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + folder = null ), modifier = Modifier, isSelectableItem = false, @@ -517,7 +522,8 @@ fun PreviewPrivateConversationItemWithBlockedBadge() = WireTheme { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, - isUserDeleted = false + isUserDeleted = false, + folder = null ), modifier = Modifier, isSelectableItem = false, 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 27e2b7566f..9f768c8c4c 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 @@ -207,7 +207,8 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, searchQuery = searchQuery, - isFavorite = false + isFavorite = false, + folder = null ) ) @@ -227,7 +228,8 @@ fun previewConversationList(count: Int, startIndex: Int = 0, unread: Boolean = f proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, searchQuery = searchQuery, isFavorite = false, - isUserDeleted = false + isUserDeleted = false, + folder = null ) ) } 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 af1daa6927..5c1882229e 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 66bced8c77..3928dcb63f 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 @@ -63,6 +65,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, @@ -84,6 +87,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, @@ -101,6 +105,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/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index 6a869b0169..9ddd2dc33d 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 d1828b55a2..f868c94b1e 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 d5b9203eef..0dd5c6dd88 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/res/values/strings.xml b/app/src/main/res/values/strings.xml index d7a7382787..f1fe9cd595 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. @@ -662,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… @@ -1675,4 +1678,8 @@ In group conversations, the group admin can overwrite this setting. 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/test/kotlin/com/wire/android/framework/TestConversationItem.kt b/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt index 848fb68370..7092e0b5b7 100644 --- a/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt +++ b/app/src/test/kotlin/com/wire/android/framework/TestConversationItem.kt @@ -46,7 +46,8 @@ object TestConversationItem { mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isFavorite = false, - isUserDeleted = false + isUserDeleted = false, + folder = null ) val GROUP = ConversationItem.GroupConversation( @@ -63,7 +64,8 @@ object TestConversationItem { isArchived = false, mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, - isFavorite = false + isFavorite = false, + folder = null ) val CONNECTION = ConversationItem.ConnectionConversation( diff --git a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt index 557b004125..a8f77e2bd3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContentTest.kt @@ -216,7 +216,8 @@ class ConversationSheetContentTest { proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isUnderLegalHold = false, isFavorite = false, - isDeletingConversationLocallyRunning = false + isDeletingConversationLocallyRunning = false, + folder = null ) } @@ -240,7 +241,8 @@ class ConversationSheetContentTest { proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isUnderLegalHold = false, isFavorite = false, - isDeletingConversationLocallyRunning = conversationDeletionLocallyRunning + isDeletingConversationLocallyRunning = conversationDeletionLocallyRunning, + folder = null ) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt index edf4b90c13..3e2fe2a8ce 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt @@ -453,7 +453,8 @@ class GroupConversationDetailsViewModelTest { proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, isUnderLegalHold = true, isFavorite = false, - isDeletingConversationLocallyRunning = false + isDeletingConversationLocallyRunning = false, + folder = null ) // When - Then assertEquals(expected, viewModel.conversationSheetContent) diff --git a/kalium b/kalium index 6ddef736ac..4362a2300c 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 6ddef736ac6399a4e849daa618bc941591e859ea +Subproject commit 4362a2300c265ad69d9d86756ce9099953035b17